From 1d7bb6a587927b56a6ee73dc964969e75b589b07 Mon Sep 17 00:00:00 2001 From: Azat Khuzhin Date: Sun, 29 Mar 2026 16:16:29 +0200 Subject: [PATCH 1/7] Add settings dialog accessible via 'S' key and Ctrl+P fuzzy actions Shows all current chdig options grouped by section (ClickHouse, View, Service, Perfetto, Runtime) in a dialog overlay, following the same pattern as the help dialog. Fixes: https://github.com/azat/chdig/issues/188 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/view/navigation.rs | 123 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/src/view/navigation.rs b/src/view/navigation.rs index 653e472..584add0 100644 --- a/src/view/navigation.rs +++ b/src/view/navigation.rs @@ -47,6 +47,7 @@ pub trait Navigation { fn chdig(&mut self, context: ContextArc); fn show_help_dialog(&mut self); + fn show_settings_dialog(&mut self); fn show_views(&mut self); fn show_actions(&mut self); fn show_fuzzy_actions(&mut self); @@ -254,6 +255,7 @@ impl Navigation for Cursive { let mut context = context.lock().unwrap(); context.add_global_action(self, "Show help", Key::F1, |siv| siv.show_help_dialog()); + context.add_global_action(self, "Show settings", 'S', |siv| siv.show_settings_dialog()); context.add_global_action(self, "Views", Key::F2, |siv| siv.show_views()); context.add_global_action(self, "Show actions", Key::F8, |siv| siv.show_actions()); @@ -374,6 +376,127 @@ impl Navigation for Cursive { self.add_layer(Dialog::info(text).with_name("help")); } + fn show_settings_dialog(&mut self) { + if self.has_view("settings") { + self.pop_layer(); + return; + } + + let mut text = StyledString::default(); + text.append_styled("Settings\n", Effect::Bold); + + { + let context = self.user_data::().unwrap().lock().unwrap(); + let opts = &context.options; + + text.append_styled("\nClickHouse:\n", Effect::Bold); + text.append_plain(format!( + " url: {}\n", + opts.clickhouse.url_safe + )); + if let Some(ref cluster) = opts.clickhouse.cluster { + text.append_plain(format!(" cluster: {}\n", cluster)); + } + text.append_plain(format!( + " history: {}\n", + opts.clickhouse.history + )); + text.append_plain(format!( + " internal_queries: {}\n", + opts.clickhouse.internal_queries + )); + text.append_plain(format!( + " limit: {}\n", + opts.clickhouse.limit + )); + text.append_plain(format!( + " skip_unavailable_shards: {}\n", + opts.clickhouse.skip_unavailable_shards + )); + text.append_plain(format!( + " server_version: {}\n", + context.server_version + )); + + text.append_styled("\nView:\n", Effect::Bold); + text.append_plain(format!( + " delay_interval: {}ms\n", + opts.view.delay_interval.as_millis() + )); + text.append_plain(format!( + " group_by: {}\n", + opts.view.group_by + )); + text.append_plain(format!( + " no_subqueries: {}\n", + opts.view.no_subqueries + )); + text.append_plain(format!(" start: {}\n", opts.view.start)); + text.append_plain(format!(" end: {}\n", opts.view.end)); + text.append_plain(format!(" wrap: {}\n", opts.view.wrap)); + text.append_plain(format!( + " no_strip_hostname_suffix: {}\n", + opts.view.no_strip_hostname_suffix + )); + + text.append_styled("\nService:\n", Effect::Bold); + text.append_plain(format!( + " log: {}\n", + opts.service.log.as_deref().unwrap_or("(none)") + )); + text.append_plain(format!( + " chdig_config: {}\n", + opts.service.chdig_config.as_deref().unwrap_or("(none)") + )); + + text.append_styled("\nPerfetto:\n", Effect::Bold); + let fmt_opt = |v: Option| match v { + Some(b) => b.to_string(), + None => "(default)".to_string(), + }; + text.append_plain(format!( + " opentelemetry_span_log: {}\n", + fmt_opt(opts.perfetto.opentelemetry_span_log) + )); + text.append_plain(format!( + " trace_log: {}\n", + fmt_opt(opts.perfetto.trace_log) + )); + text.append_plain(format!( + " query_metric_log: {}\n", + fmt_opt(opts.perfetto.query_metric_log) + )); + text.append_plain(format!( + " part_log: {}\n", + fmt_opt(opts.perfetto.part_log) + )); + text.append_plain(format!( + " query_thread_log: {}\n", + fmt_opt(opts.perfetto.query_thread_log) + )); + text.append_plain(format!( + " text_log: {}\n", + fmt_opt(opts.perfetto.text_log) + )); + text.append_plain(format!( + " per_server: {}\n", + fmt_opt(opts.perfetto.per_server) + )); + + text.append_styled("\nRuntime:\n", Effect::Bold); + text.append_plain(format!( + " selected_host: {}\n", + context.selected_host.as_deref().unwrap_or("(all)") + )); + text.append_plain(format!( + " current_view: {:?}\n", + context.current_view.unwrap_or(ChDigViews::Queries) + )); + } + + self.add_layer(Dialog::info(text).with_name("settings")); + } + fn show_views(&mut self) { let mut has_views = false; let context = self.user_data::().unwrap().clone(); From 299b1ccf6dd57932fb0470794589131e90f1ae32 Mon Sep 17 00:00:00 2001 From: Azat Khuzhin Date: Sun, 29 Mar 2026 16:28:32 +0200 Subject: [PATCH 2/7] Move Perfetto config defaults from worker.rs to options.rs Replace Option fields in ChDigPerfettoConfig with bool and a manual Default impl that provides the actual defaults (previously scattered as unwrap_or calls in worker.rs). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/interpreter/options.rs | 60 +++++++++++++++++++++++--------------- src/interpreter/worker.rs | 16 +++++----- src/view/navigation.rs | 18 +++++------- 3 files changed, 52 insertions(+), 42 deletions(-) diff --git a/src/interpreter/options.rs b/src/interpreter/options.rs index af25ca0..df91be9 100644 --- a/src/interpreter/options.rs +++ b/src/interpreter/options.rs @@ -286,16 +286,30 @@ pub struct ServiceOptions { pub chdig_config: Option, } -#[derive(Deserialize, Default, Clone)] +#[derive(Deserialize, Clone)] #[serde(default)] pub struct ChDigPerfettoConfig { - pub opentelemetry_span_log: Option, - pub trace_log: Option, - pub query_metric_log: Option, - pub part_log: Option, - pub query_thread_log: Option, - pub text_log: Option, - pub per_server: Option, + pub opentelemetry_span_log: bool, + pub trace_log: bool, + pub query_metric_log: bool, + pub part_log: bool, + pub query_thread_log: bool, + pub text_log: bool, + pub per_server: bool, +} + +impl Default for ChDigPerfettoConfig { + fn default() -> Self { + Self { + opentelemetry_span_log: true, + trace_log: true, + query_metric_log: false, + part_log: true, + query_thread_log: true, + text_log: true, + per_server: true, + } + } } #[derive(Deserialize, Default)] @@ -1289,31 +1303,31 @@ mod tests { fn test_chdig_config_perfetto() { let config = read_chdig_config("tests/configs/chdig_basic.yaml").unwrap(); - assert_eq!(config.perfetto.opentelemetry_span_log, Some(true)); - assert_eq!(config.perfetto.trace_log, Some(true)); - assert_eq!(config.perfetto.query_metric_log, Some(true)); - assert_eq!(config.perfetto.part_log, Some(false)); - assert_eq!(config.perfetto.query_thread_log, Some(true)); - assert_eq!(config.perfetto.text_log, Some(false)); + assert_eq!(config.perfetto.opentelemetry_span_log, true); + assert_eq!(config.perfetto.trace_log, true); + assert_eq!(config.perfetto.query_metric_log, true); + assert_eq!(config.perfetto.part_log, false); + assert_eq!(config.perfetto.query_thread_log, true); + assert_eq!(config.perfetto.text_log, false); let mut options = ChDigOptions::parse_from(["chdig"]); apply_chdig_config(&mut options, &config); - assert_eq!(options.perfetto.opentelemetry_span_log, Some(true)); - assert_eq!(options.perfetto.part_log, Some(false)); - assert_eq!(options.perfetto.query_metric_log, Some(true)); + assert_eq!(options.perfetto.opentelemetry_span_log, true); + assert_eq!(options.perfetto.part_log, false); + assert_eq!(options.perfetto.query_metric_log, true); } #[test] fn test_chdig_config_perfetto_defaults() { let config = read_chdig_config("tests/configs/chdig_empty.yaml").unwrap(); - assert!(config.perfetto.opentelemetry_span_log.is_none()); - assert!(config.perfetto.trace_log.is_none()); - assert!(config.perfetto.query_metric_log.is_none()); - assert!(config.perfetto.part_log.is_none()); - assert!(config.perfetto.query_thread_log.is_none()); - assert!(config.perfetto.text_log.is_none()); + assert_eq!(config.perfetto.opentelemetry_span_log, true); + assert_eq!(config.perfetto.trace_log, true); + assert_eq!(config.perfetto.query_metric_log, false); + assert_eq!(config.perfetto.part_log, true); + assert_eq!(config.perfetto.query_thread_log, true); + assert_eq!(config.perfetto.text_log, true); } #[test] diff --git a/src/interpreter/worker.rs b/src/interpreter/worker.rs index ed571d5..e6655dd 100644 --- a/src/interpreter/worker.rs +++ b/src/interpreter/worker.rs @@ -774,7 +774,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) } Event::PerfettoExport(queries, query_ids, start, end) => { let perfetto_cfg = context.lock().unwrap().options.perfetto.clone(); - let mut builder = PerfettoTraceBuilder::new(perfetto_cfg.per_server.unwrap_or(true)); + let mut builder = PerfettoTraceBuilder::new(perfetto_cfg.per_server); for q in &queries { log::info!( @@ -795,7 +795,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) let (otel, trace_log, metrics, parts, threads, stack_traces, text_logs) = tokio::join!( async { - if perfetto_cfg.opentelemetry_span_log.unwrap_or(true) { + if perfetto_cfg.opentelemetry_span_log { Some( clickhouse .get_otel_spans_for_perfetto(&query_ids, start, end_time) @@ -806,7 +806,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) } }, async { - if perfetto_cfg.trace_log.unwrap_or(true) { + if perfetto_cfg.trace_log { Some( clickhouse .get_trace_log_counters_for_perfetto(&query_ids, start, end_time) @@ -818,7 +818,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) }, async { // Consider using ProfileEvents instead - if perfetto_cfg.query_metric_log.unwrap_or(false) { + if perfetto_cfg.query_metric_log { Some( clickhouse .get_query_metrics_for_perfetto(&query_ids, start, end_time) @@ -829,7 +829,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) } }, async { - if perfetto_cfg.part_log.unwrap_or(true) { + if perfetto_cfg.part_log { Some( clickhouse .get_part_log_for_perfetto(&query_ids, start, end_time) @@ -840,7 +840,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) } }, async { - if perfetto_cfg.query_thread_log.unwrap_or(true) { + if perfetto_cfg.query_thread_log { Some( clickhouse .get_query_thread_log_for_perfetto(&query_ids, start, end_time) @@ -851,7 +851,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) } }, async { - if perfetto_cfg.trace_log.unwrap_or(true) { + if perfetto_cfg.trace_log { Some( clickhouse .get_stack_traces_for_perfetto(&query_ids, start, end_time) @@ -862,7 +862,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool) } }, async { - if perfetto_cfg.text_log.unwrap_or(true) { + if perfetto_cfg.text_log { Some( clickhouse .get_text_log_for_perfetto(&query_ids, start, end_time) diff --git a/src/view/navigation.rs b/src/view/navigation.rs index 584add0..7e00fb2 100644 --- a/src/view/navigation.rs +++ b/src/view/navigation.rs @@ -450,37 +450,33 @@ impl Navigation for Cursive { )); text.append_styled("\nPerfetto:\n", Effect::Bold); - let fmt_opt = |v: Option| match v { - Some(b) => b.to_string(), - None => "(default)".to_string(), - }; text.append_plain(format!( " opentelemetry_span_log: {}\n", - fmt_opt(opts.perfetto.opentelemetry_span_log) + opts.perfetto.opentelemetry_span_log )); text.append_plain(format!( " trace_log: {}\n", - fmt_opt(opts.perfetto.trace_log) + opts.perfetto.trace_log )); text.append_plain(format!( " query_metric_log: {}\n", - fmt_opt(opts.perfetto.query_metric_log) + opts.perfetto.query_metric_log )); text.append_plain(format!( " part_log: {}\n", - fmt_opt(opts.perfetto.part_log) + opts.perfetto.part_log )); text.append_plain(format!( " query_thread_log: {}\n", - fmt_opt(opts.perfetto.query_thread_log) + opts.perfetto.query_thread_log )); text.append_plain(format!( " text_log: {}\n", - fmt_opt(opts.perfetto.text_log) + opts.perfetto.text_log )); text.append_plain(format!( " per_server: {}\n", - fmt_opt(opts.perfetto.per_server) + opts.perfetto.per_server )); text.append_styled("\nRuntime:\n", Effect::Bold); From 324bab393db59192d0ab5eaae5a073dd7eb651d0 Mon Sep 17 00:00:00 2001 From: Azat Khuzhin Date: Sun, 29 Mar 2026 16:29:52 +0200 Subject: [PATCH 3/7] Make settings dialog interactive with editable fields Replace read-only text display with Checkbox widgets for boolean options and EditView for numeric fields (limit, delay_interval). Apply button writes values back to context and triggers a view refresh. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/view/navigation.rs | 358 +++++++++++++++++++++++++++++------------ 1 file changed, 253 insertions(+), 105 deletions(-) diff --git a/src/view/navigation.rs b/src/view/navigation.rs index 7e00fb2..7d0b72e 100644 --- a/src/view/navigation.rs +++ b/src/view/navigation.rs @@ -12,7 +12,10 @@ use cursive::{ theme::{BaseColor, Color, ColorStyle, Effect, PaletteColor, Style, Theme}, utils::{markup::StyledString, span::SpannedString}, view::{IntoBoxedView, Nameable, Resizable, View}, - views::{Dialog, DummyView, EditView, LinearLayout, OnEventView, SelectView, TextView}, + views::{ + Checkbox, Dialog, DummyView, EditView, LinearLayout, OnEventView, ScrollView, SelectView, + TextView, + }, }; use cursive_flexi_logger_view::toggle_flexi_logger_debug_console; @@ -382,115 +385,260 @@ impl Navigation for Cursive { return; } - let mut text = StyledString::default(); - text.append_styled("Settings\n", Effect::Bold); - - { - let context = self.user_data::().unwrap().lock().unwrap(); - let opts = &context.options; - - text.append_styled("\nClickHouse:\n", Effect::Bold); - text.append_plain(format!( - " url: {}\n", - opts.clickhouse.url_safe - )); - if let Some(ref cluster) = opts.clickhouse.cluster { - text.append_plain(format!(" cluster: {}\n", cluster)); - } - text.append_plain(format!( - " history: {}\n", - opts.clickhouse.history - )); - text.append_plain(format!( - " internal_queries: {}\n", - opts.clickhouse.internal_queries - )); - text.append_plain(format!( - " limit: {}\n", - opts.clickhouse.limit - )); - text.append_plain(format!( - " skip_unavailable_shards: {}\n", - opts.clickhouse.skip_unavailable_shards - )); - text.append_plain(format!( - " server_version: {}\n", - context.server_version - )); - - text.append_styled("\nView:\n", Effect::Bold); - text.append_plain(format!( - " delay_interval: {}ms\n", - opts.view.delay_interval.as_millis() - )); - text.append_plain(format!( - " group_by: {}\n", - opts.view.group_by - )); - text.append_plain(format!( - " no_subqueries: {}\n", - opts.view.no_subqueries - )); - text.append_plain(format!(" start: {}\n", opts.view.start)); - text.append_plain(format!(" end: {}\n", opts.view.end)); - text.append_plain(format!(" wrap: {}\n", opts.view.wrap)); - text.append_plain(format!( - " no_strip_hostname_suffix: {}\n", - opts.view.no_strip_hostname_suffix - )); + let context = self.user_data::().unwrap().clone(); + let (opts, server_version, selected_host, current_view) = { + let ctx = context.lock().unwrap(); + ( + ctx.options.clone(), + ctx.server_version.clone(), + ctx.selected_host.clone(), + ctx.current_view, + ) + }; - text.append_styled("\nService:\n", Effect::Bold); - text.append_plain(format!( - " log: {}\n", - opts.service.log.as_deref().unwrap_or("(none)") - )); - text.append_plain(format!( - " chdig_config: {}\n", - opts.service.chdig_config.as_deref().unwrap_or("(none)") - )); + let bold = |s: &str| TextView::new(StyledString::styled(s, Effect::Bold)); + let checkbox_row = |label: &str, name: &str, checked: bool| { + LinearLayout::horizontal() + .child(DummyView.fixed_width(2)) + .child(Checkbox::new().with_checked(checked).with_name(name)) + .child(TextView::new(format!(" {}", label))) + }; + let edit_row = |label: &str, name: &str, value: &str| { + LinearLayout::horizontal() + .child(TextView::new(format!(" {}: ", label))) + .child( + EditView::new() + .content(value) + .with_name(name) + .fixed_width(12), + ) + }; - text.append_styled("\nPerfetto:\n", Effect::Bold); - text.append_plain(format!( - " opentelemetry_span_log: {}\n", - opts.perfetto.opentelemetry_span_log - )); - text.append_plain(format!( - " trace_log: {}\n", - opts.perfetto.trace_log - )); - text.append_plain(format!( - " query_metric_log: {}\n", - opts.perfetto.query_metric_log - )); - text.append_plain(format!( - " part_log: {}\n", - opts.perfetto.part_log - )); - text.append_plain(format!( - " query_thread_log: {}\n", - opts.perfetto.query_thread_log - )); - text.append_plain(format!( - " text_log: {}\n", - opts.perfetto.text_log - )); - text.append_plain(format!( - " per_server: {}\n", - opts.perfetto.per_server - )); + let mut layout = LinearLayout::vertical(); - text.append_styled("\nRuntime:\n", Effect::Bold); - text.append_plain(format!( - " selected_host: {}\n", - context.selected_host.as_deref().unwrap_or("(all)") - )); - text.append_plain(format!( - " current_view: {:?}\n", - context.current_view.unwrap_or(ChDigViews::Queries) - )); + // ClickHouse + layout.add_child(bold("ClickHouse:")); + layout.add_child(TextView::new(format!( + " url: {}", + opts.clickhouse.url_safe + ))); + if let Some(ref cluster) = opts.clickhouse.cluster { + layout.add_child(TextView::new(format!(" cluster: {}", cluster))); } + layout.add_child(checkbox_row( + "history", + "set_history", + opts.clickhouse.history, + )); + layout.add_child(checkbox_row( + "internal_queries", + "set_internal_queries", + opts.clickhouse.internal_queries, + )); + layout.add_child(edit_row( + "limit", + "set_limit", + &opts.clickhouse.limit.to_string(), + )); + layout.add_child(checkbox_row( + "skip_unavailable_shards", + "set_skip_unavailable_shards", + opts.clickhouse.skip_unavailable_shards, + )); + layout.add_child(TextView::new(format!( + " server_version: {}", + server_version + ))); + layout.add_child(DummyView); + + // View + layout.add_child(bold("View:")); + layout.add_child(edit_row( + "delay_interval (ms)", + "set_delay_interval", + &opts.view.delay_interval.as_millis().to_string(), + )); + layout.add_child(checkbox_row("group_by", "set_group_by", opts.view.group_by)); + layout.add_child(checkbox_row( + "no_subqueries", + "set_no_subqueries", + opts.view.no_subqueries, + )); + layout.add_child(checkbox_row("wrap", "set_wrap", opts.view.wrap)); + layout.add_child(checkbox_row( + "no_strip_hostname_suffix", + "set_no_strip_hostname_suffix", + opts.view.no_strip_hostname_suffix, + )); + layout.add_child(DummyView); + + // Service (read-only) + layout.add_child(bold("Service:")); + layout.add_child(TextView::new(format!( + " log: {}", + opts.service.log.as_deref().unwrap_or("(none)") + ))); + layout.add_child(TextView::new(format!( + " chdig_config: {}", + opts.service.chdig_config.as_deref().unwrap_or("(none)") + ))); + layout.add_child(DummyView); + + // Perfetto + layout.add_child(bold("Perfetto:")); + layout.add_child(checkbox_row( + "opentelemetry_span_log", + "set_otel", + opts.perfetto.opentelemetry_span_log, + )); + layout.add_child(checkbox_row( + "trace_log", + "set_trace_log", + opts.perfetto.trace_log, + )); + layout.add_child(checkbox_row( + "query_metric_log", + "set_query_metric_log", + opts.perfetto.query_metric_log, + )); + layout.add_child(checkbox_row( + "part_log", + "set_part_log", + opts.perfetto.part_log, + )); + layout.add_child(checkbox_row( + "query_thread_log", + "set_query_thread_log", + opts.perfetto.query_thread_log, + )); + layout.add_child(checkbox_row( + "text_log", + "set_text_log", + opts.perfetto.text_log, + )); + layout.add_child(checkbox_row( + "per_server", + "set_per_server", + opts.perfetto.per_server, + )); + layout.add_child(DummyView); + + // Runtime (read-only) + layout.add_child(bold("Runtime:")); + layout.add_child(TextView::new(format!( + " selected_host: {}", + selected_host.as_deref().unwrap_or("(all)") + ))); + layout.add_child(TextView::new(format!( + " current_view: {:?}", + current_view.unwrap_or(ChDigViews::Queries) + ))); + + let context_for_apply = context; + let dialog = Dialog::new() + .title("Settings") + .content(ScrollView::new(layout)) + .button("Apply", move |siv| { + let history = siv + .call_on_name("set_history", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let internal_queries = siv + .call_on_name("set_internal_queries", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let limit_str = siv + .call_on_name("set_limit", |v: &mut EditView| v.get_content()) + .unwrap(); + let skip_unavailable_shards = siv + .call_on_name("set_skip_unavailable_shards", |v: &mut Checkbox| { + v.is_checked() + }) + .unwrap(); + + let delay_str = siv + .call_on_name("set_delay_interval", |v: &mut EditView| v.get_content()) + .unwrap(); + let group_by = siv + .call_on_name("set_group_by", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let no_subqueries = siv + .call_on_name("set_no_subqueries", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let wrap = siv + .call_on_name("set_wrap", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let no_strip = siv + .call_on_name("set_no_strip_hostname_suffix", |v: &mut Checkbox| { + v.is_checked() + }) + .unwrap(); + + let otel = siv + .call_on_name("set_otel", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let trace_log = siv + .call_on_name("set_trace_log", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let query_metric = siv + .call_on_name("set_query_metric_log", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let part_log = siv + .call_on_name("set_part_log", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let query_thread = siv + .call_on_name("set_query_thread_log", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let text_log = siv + .call_on_name("set_text_log", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + let per_server = siv + .call_on_name("set_per_server", |v: &mut Checkbox| v.is_checked()) + .unwrap(); + + let limit: u64 = match limit_str.parse() { + Ok(v) => v, + Err(_) => { + siv.add_layer(Dialog::info("Invalid limit value")); + return; + } + }; + let delay_ms: u64 = match delay_str.parse() { + Ok(v) => v, + Err(_) => { + siv.add_layer(Dialog::info("Invalid delay_interval value")); + return; + } + }; + + { + let mut ctx = context_for_apply.lock().unwrap(); + ctx.options.clickhouse.history = history; + ctx.options.clickhouse.internal_queries = internal_queries; + ctx.options.clickhouse.limit = limit; + ctx.options.clickhouse.skip_unavailable_shards = skip_unavailable_shards; + + ctx.options.view.delay_interval = std::time::Duration::from_millis(delay_ms); + ctx.options.view.group_by = group_by; + ctx.options.view.no_subqueries = no_subqueries; + ctx.options.view.wrap = wrap; + ctx.options.view.no_strip_hostname_suffix = no_strip; + + ctx.options.perfetto.opentelemetry_span_log = otel; + ctx.options.perfetto.trace_log = trace_log; + ctx.options.perfetto.query_metric_log = query_metric; + ctx.options.perfetto.part_log = part_log; + ctx.options.perfetto.query_thread_log = query_thread; + ctx.options.perfetto.text_log = text_log; + ctx.options.perfetto.per_server = per_server; + + ctx.trigger_view_refresh(); + } + siv.pop_layer(); + }) + .button("Cancel", |siv| { + siv.pop_layer(); + }); - self.add_layer(Dialog::info(text).with_name("settings")); + self.add_layer(dialog.with_name("settings")); } fn show_views(&mut self) { From f704d0fc2cdfd57f4935a9deab39a562fde0f99f Mon Sep 17 00:00:00 2001 From: Azat Khuzhin Date: Sun, 29 Mar 2026 16:34:50 +0200 Subject: [PATCH 4/7] Rename "Show settings" to "Settings" and use F3 for shortcut --- src/view/navigation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/navigation.rs b/src/view/navigation.rs index 7d0b72e..e7ff05a 100644 --- a/src/view/navigation.rs +++ b/src/view/navigation.rs @@ -258,7 +258,7 @@ impl Navigation for Cursive { let mut context = context.lock().unwrap(); context.add_global_action(self, "Show help", Key::F1, |siv| siv.show_help_dialog()); - context.add_global_action(self, "Show settings", 'S', |siv| siv.show_settings_dialog()); + context.add_global_action(self, "Settings", Key::F3, |siv| siv.show_settings_dialog()); context.add_global_action(self, "Views", Key::F2, |siv| siv.show_views()); context.add_global_action(self, "Show actions", Key::F8, |siv| siv.show_actions()); From 68bab85280018cc6a4deb87a6ba8189dfec04e16 Mon Sep 17 00:00:00 2001 From: Azat Khuzhin Date: Sun, 29 Mar 2026 16:37:54 +0200 Subject: [PATCH 5/7] Add start/end time range to settings dialog Show resolved DateTime values in editable fields, parsed back via parse_datetime_or_date on apply (same format as Alt+T dialog). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/view/navigation.rs | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/view/navigation.rs b/src/view/navigation.rs index e7ff05a..ee35c25 100644 --- a/src/view/navigation.rs +++ b/src/view/navigation.rs @@ -403,14 +403,14 @@ impl Navigation for Cursive { .child(Checkbox::new().with_checked(checked).with_name(name)) .child(TextView::new(format!(" {}", label))) }; - let edit_row = |label: &str, name: &str, value: &str| { + let edit_row = |label: &str, name: &str, value: &str, width: usize| { LinearLayout::horizontal() .child(TextView::new(format!(" {}: ", label))) .child( EditView::new() .content(value) .with_name(name) - .fixed_width(12), + .fixed_width(width), ) }; @@ -439,6 +439,7 @@ impl Navigation for Cursive { "limit", "set_limit", &opts.clickhouse.limit.to_string(), + 12, )); layout.add_child(checkbox_row( "skip_unavailable_shards", @@ -457,6 +458,7 @@ impl Navigation for Cursive { "delay_interval (ms)", "set_delay_interval", &opts.view.delay_interval.as_millis().to_string(), + 12, )); layout.add_child(checkbox_row("group_by", "set_group_by", opts.view.group_by)); layout.add_child(checkbox_row( @@ -470,6 +472,20 @@ impl Navigation for Cursive { "set_no_strip_hostname_suffix", opts.view.no_strip_hostname_suffix, )); + let start_dt: DateTime = opts.view.start.clone().into(); + let end_dt: DateTime = opts.view.end.clone().into(); + layout.add_child(edit_row( + "start", + "set_start", + &start_dt.format("%Y-%m-%dT%H:%M:%S").to_string(), + 22, + )); + layout.add_child(edit_row( + "end", + "set_end", + &end_dt.format("%Y-%m-%dT%H:%M:%S").to_string(), + 22, + )); layout.add_child(DummyView); // Service (read-only) @@ -571,6 +587,12 @@ impl Navigation for Cursive { v.is_checked() }) .unwrap(); + let start_str = siv + .call_on_name("set_start", |v: &mut EditView| v.get_content()) + .unwrap(); + let end_str = siv + .call_on_name("set_end", |v: &mut EditView| v.get_content()) + .unwrap(); let otel = siv .call_on_name("set_otel", |v: &mut Checkbox| v.is_checked()) @@ -608,6 +630,20 @@ impl Navigation for Cursive { return; } }; + let new_start = match parse_datetime_or_date(&start_str) { + Ok(dt) => dt, + Err(err) => { + siv.add_layer(Dialog::info(err)); + return; + } + }; + let new_end = match parse_datetime_or_date(&end_str) { + Ok(dt) => dt, + Err(err) => { + siv.add_layer(Dialog::info(err)); + return; + } + }; { let mut ctx = context_for_apply.lock().unwrap(); @@ -621,6 +657,8 @@ impl Navigation for Cursive { ctx.options.view.no_subqueries = no_subqueries; ctx.options.view.wrap = wrap; ctx.options.view.no_strip_hostname_suffix = no_strip; + ctx.options.view.start = new_start.into(); + ctx.options.view.end = new_end.into(); ctx.options.perfetto.opentelemetry_span_log = otel; ctx.options.perfetto.trace_log = trace_log; From d31a6c731185fa1a2822c493bd83f7bf291679a7 Mon Sep 17 00:00:00 2001 From: Azat Khuzhin Date: Sun, 29 Mar 2026 16:39:24 +0200 Subject: [PATCH 6/7] Support humantime format (e.g. '1hour') in settings start/end fields Parse via RelativeDateTime::from_str instead of parse_datetime_or_date so both relative durations and absolute datetimes work. Add to_editable_string() to display the original format round-trippably. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/common/relative_date_time.rs | 10 ++++++++++ src/view/navigation.rs | 22 ++++++++++------------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/common/relative_date_time.rs b/src/common/relative_date_time.rs index 5054fe5..524c751 100644 --- a/src/common/relative_date_time.rs +++ b/src/common/relative_date_time.rs @@ -58,6 +58,16 @@ impl RelativeDateTime { self.date_time } + pub fn to_editable_string(&self) -> String { + match (&self.date_time, &self.offset) { + (None, Some(offset)) => { + humantime::format_duration(offset.to_std().unwrap_or_default()).to_string() + } + (Some(dt), _) => dt.format("%Y-%m-%dT%H:%M:%S").to_string(), + (None, None) => String::new(), + } + } + pub fn to_sql_datetime_64(&self) -> Option { match (self.date_time, self.offset) { (Some(date_time), Some(offset)) => Some(format!( diff --git a/src/view/navigation.rs b/src/view/navigation.rs index ee35c25..03d01fe 100644 --- a/src/view/navigation.rs +++ b/src/view/navigation.rs @@ -472,18 +472,16 @@ impl Navigation for Cursive { "set_no_strip_hostname_suffix", opts.view.no_strip_hostname_suffix, )); - let start_dt: DateTime = opts.view.start.clone().into(); - let end_dt: DateTime = opts.view.end.clone().into(); layout.add_child(edit_row( "start", "set_start", - &start_dt.format("%Y-%m-%dT%H:%M:%S").to_string(), + &opts.view.start.to_editable_string(), 22, )); layout.add_child(edit_row( "end", "set_end", - &end_dt.format("%Y-%m-%dT%H:%M:%S").to_string(), + &opts.view.end.to_editable_string(), 22, )); layout.add_child(DummyView); @@ -630,17 +628,17 @@ impl Navigation for Cursive { return; } }; - let new_start = match parse_datetime_or_date(&start_str) { - Ok(dt) => dt, + let new_start = match start_str.parse::() { + Ok(v) => v, Err(err) => { - siv.add_layer(Dialog::info(err)); + siv.add_layer(Dialog::info(format!("Invalid start: {}", err))); return; } }; - let new_end = match parse_datetime_or_date(&end_str) { - Ok(dt) => dt, + let new_end = match end_str.parse::() { + Ok(v) => v, Err(err) => { - siv.add_layer(Dialog::info(err)); + siv.add_layer(Dialog::info(format!("Invalid end: {}", err))); return; } }; @@ -657,8 +655,8 @@ impl Navigation for Cursive { ctx.options.view.no_subqueries = no_subqueries; ctx.options.view.wrap = wrap; ctx.options.view.no_strip_hostname_suffix = no_strip; - ctx.options.view.start = new_start.into(); - ctx.options.view.end = new_end.into(); + ctx.options.view.start = new_start; + ctx.options.view.end = new_end; ctx.options.perfetto.opentelemetry_span_log = otel; ctx.options.perfetto.trace_log = trace_log; From d693a0c784518906735de7a5d911a4ff9ad7e033 Mon Sep 17 00:00:00 2001 From: Azat Khuzhin Date: Sun, 29 Mar 2026 16:42:16 +0200 Subject: [PATCH 7/7] Add F3/Settings to menu bar --- src/view/navigation.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/view/navigation.rs b/src/view/navigation.rs index 03d01fe..db3fd7b 100644 --- a/src/view/navigation.rs +++ b/src/view/navigation.rs @@ -28,6 +28,9 @@ fn make_menu_text() -> StyledString { // F2 text.append_plain("F2"); text.append_styled("Views", ColorStyle::highlight()); + // F3 + text.append_plain("F3"); + text.append_styled("Settings", ColorStyle::highlight()); // F8 text.append_plain("F8"); text.append_styled("Actions", ColorStyle::highlight()); @@ -673,7 +676,6 @@ impl Navigation for Cursive { .button("Cancel", |siv| { siv.pop_layer(); }); - self.add_layer(dialog.with_name("settings")); }