Skip to main content

tracexec_core/cli/
tui_theme.rs

1use std::collections::HashMap;
2
3use serde::{
4  Deserialize,
5  Serialize,
6};
7
8/// Invoke a callback macro with every style-valued TUI theme field.
9///
10/// Keeping this list in one place prevents theme validation and application
11/// from silently drifting apart when a field is added.
12#[macro_export]
13macro_rules! for_each_tui_theme_style {
14  ($callback:ident) => {
15    $callback!(
16      inactive_border,
17      active_border,
18      popup_border,
19      app_title,
20      help_popup,
21      inline_timestamp,
22      cli_flag,
23      help_key,
24      help_desc,
25      fancy_help_desc,
26      pid_success,
27      pid_failure,
28      pid_enoent,
29      pid_in_msg,
30      comm,
31      tracer_info,
32      tracer_warning,
33      tracer_error,
34      new_child_pid,
35      tracer_event,
36      inline_tracer_error,
37      partial_ok,
38      filename,
39      modified_fd_in_cmdline,
40      removed_fd_in_cmdline,
41      cloexec_fd_in_cmdline,
42      added_fd_in_cmdline,
43      arg0,
44      cwd,
45      deleted_env_var,
46      modified_env_var,
47      added_env_var,
48      argv,
49      search_match,
50      query_no_match,
51      query_match_current_no,
52      query_match_total_cnt,
53      empty_field,
54      uid_gid_name,
55      uid_gid_value,
56      exec_result_success,
57      exec_result_failure,
58      value_unknown,
59      fd_closed,
60      plus_sign,
61      minus_sign,
62      equal_sign,
63      added_env_key,
64      added_env_val,
65      removed_env_key,
66      removed_env_val,
67      unchanged_env_key,
68      unchanged_env_val,
69      fd_label,
70      fd_number_label,
71      sublabel,
72      selected_label,
73      label,
74      selection_indicator,
75      open_flag_cloexec,
76      open_flag_access_mode,
77      open_flag_creation,
78      open_flag_status,
79      open_flag_other,
80      visual_separator,
81      error_popup,
82      info_popup,
83      active_tab,
84      status_process_running,
85      status_process_paused,
86      status_process_detached,
87      status_exec_error,
88      status_process_exited_normally,
89      status_process_exited_abnormally,
90      status_process_killed,
91      status_process_terminated,
92      status_process_interrupted,
93      status_process_segfault,
94      status_process_aborted,
95      status_process_sigill,
96      status_process_signaled,
97      status_internal_failure,
98      breakpoint_title_selected,
99      breakpoint_title,
100      breakpoint_pattern_type_label,
101      breakpoint_pattern,
102      breakpoint_info_label,
103      breakpoint_info_label_active,
104      breakpoint_info_value,
105      hit_entry_pid,
106      hit_entry_plain_text,
107      hit_entry_breakpoint_stop,
108      hit_entry_breakpoint_pattern,
109      hit_entry_no_breakpoint_pattern,
110      hit_manager_default_command,
111      hit_manager_no_default_command,
112    );
113  };
114}
115
116macro_rules! define_theme_spec {
117  ($($style_field:ident),* $(,)?) => {
118    /// Partial theme specification loaded from a TOML file or inline config table.
119    /// Only explicitly set fields override the built-in theme.
120    #[derive(Debug, Default, Clone, Deserialize, Serialize)]
121    #[serde(rename_all = "kebab-case")]
122    pub struct ThemeSpec {
123      $(pub $style_field: Option<StyleSpec>,)*
124      pub backtrace_parent_spawns: Option<SpanSpec>,
125      pub backtrace_parent_becomes: Option<SpanSpec>,
126      pub backtrace_parent_unknown: Option<SpanSpec>,
127      /// Collects unrecognised top-level theme keys so the caller can warn about them.
128      #[serde(flatten)]
129      pub extra: HashMap<String, toml::Value>,
130    }
131  };
132}
133
134crate::for_each_tui_theme_style!(define_theme_spec);
135
136impl ThemeSpec {
137  /// Emit `tracing::warn!` for every unknown field in this spec and in all
138  /// nested `StyleSpec` / `SpanSpec` values.
139  ///
140  /// `source` is a human-readable description of where the spec came from
141  /// (e.g. a file path or `"inline theme config"`).
142  pub fn warn_unknown_fields(&self, source: &str, section: Option<&str>) {
143    for key in self.extra.keys() {
144      if let Some(section) = section {
145        tracing::warn!("Unknown key '{key}' in [{section}] in {source} will be ignored");
146      } else {
147        tracing::warn!("Unknown key '{key}' in {source} will be ignored");
148      }
149    }
150
151    macro_rules! warn_style {
152      ($($field:ident),* $(,)?) => {
153        $(
154          if let Some(ref spec) = self.$field {
155            let key = kebab_case(stringify!($field));
156            spec.warn_unknown_fields(source, &key, section);
157          }
158        )*
159      };
160    }
161    crate::for_each_tui_theme_style!(warn_style);
162
163    macro_rules! warn_span {
164      ($($field:ident),* $(,)?) => {
165        $(
166          if let Some(ref spec) = self.$field {
167            let key = kebab_case(stringify!($field));
168            spec.style.warn_unknown_fields(source, &key, section);
169          }
170        )*
171      };
172    }
173    warn_span!(
174      backtrace_parent_spawns,
175      backtrace_parent_becomes,
176      backtrace_parent_unknown,
177    );
178  }
179}
180
181/// Partial style override: only fields that are `Some` are applied on top of
182/// the existing style.
183#[derive(Debug, Default, Clone, Deserialize, Serialize)]
184#[serde(rename_all = "kebab-case")]
185pub struct StyleSpec {
186  pub fg: Option<ThemeColor>,
187  pub bg: Option<ThemeColor>,
188  pub underline_color: Option<ThemeColor>,
189  #[serde(default)]
190  pub modifiers: Vec<ThemeModifier>,
191  #[serde(default)]
192  pub remove_modifiers: Vec<ThemeModifier>,
193  /// Collects unrecognised keys for user-visible warnings.
194  #[serde(flatten)]
195  pub extra: HashMap<String, toml::Value>,
196}
197
198impl StyleSpec {
199  pub fn warn_unknown_fields(&self, source: &str, parent_key: &str, section: Option<&str>) {
200    for unknown_key in self.extra.keys() {
201      if let Some(section) = section {
202        tracing::warn!(
203          "Unknown key '{unknown_key}' for '{parent_key}' in [{section}] in {source} will be ignored"
204        );
205      } else {
206        tracing::warn!(
207          "Unknown key '{unknown_key}' for '{parent_key}' in {source} will be ignored"
208        );
209      }
210    }
211  }
212}
213
214fn kebab_case(field: &str) -> String {
215  field.replace('_', "-")
216}
217
218/// Partial span override: `content` replaces the span text; all style fields
219/// are forwarded to `StyleSpec`.
220#[derive(Debug, Default, Clone, Deserialize, Serialize)]
221#[serde(rename_all = "kebab-case")]
222pub struct SpanSpec {
223  pub content: Option<String>,
224  #[serde(flatten)]
225  pub style: StyleSpec,
226}
227
228/// A color value as it appears in a theme TOML file.
229#[derive(Debug, Clone, Deserialize, Serialize)]
230#[serde(untagged)]
231pub enum ThemeColor {
232  Named(String),
233  Indexed(u8),
234  Rgb { r: u8, g: u8, b: u8 },
235}
236
237/// A modifier flag that can be listed under `modifiers` or `remove-modifiers`.
238#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
239#[serde(rename_all = "kebab-case")]
240pub enum ThemeModifier {
241  Bold,
242  Dim,
243  Italic,
244  Underlined,
245  SlowBlink,
246  RapidBlink,
247  Reversed,
248  Hidden,
249  CrossedOut,
250}
251
252/// Top-level wrapper for a theme TOML file.
253///
254/// All TUI theme keys must live inside the `[tui]` section.  This makes the
255/// format forward-compatible so that future `[log]` (or other mode) sections
256/// can coexist in the same file without conflicts.
257///
258/// ```toml
259/// [tui]
260/// active-border = { fg = "cyan", modifiers = ["bold"] }
261/// ```
262#[derive(Debug, Default, Deserialize, Serialize)]
263pub struct ThemeFile {
264  /// TUI-mode theme overrides.
265  pub tui: Option<ThemeSpec>,
266  /// Collects unrecognised top-level sections so the caller can warn.
267  #[serde(flatten)]
268  pub extra: HashMap<String, toml::Value>,
269}
270
271impl ThemeFile {
272  /// Emit `tracing::warn!` for every unknown top-level section in the file.
273  pub fn warn_unknown_sections(&self, source: &str) {
274    for key in self.extra.keys() {
275      tracing::warn!(
276        "Unknown key '{key}' at top level in theme file {source} will be ignored; theme keys must be under [tui]"
277      );
278    }
279  }
280}