Skip to main content

libsurfer/
config.rs

1#[cfg(not(target_arch = "wasm32"))]
2use camino::{Utf8Path, Utf8PathBuf};
3use config::builder::DefaultState;
4use config::{Config, ConfigBuilder};
5#[cfg(not(target_arch = "wasm32"))]
6use config::{Environment, File};
7use derive_more::{Display, FromStr};
8#[cfg(not(target_arch = "wasm32"))]
9use directories::ProjectDirs;
10use ecolor::Color32;
11use enum_iterator::Sequence;
12use epaint::{PathStroke, Stroke};
13use eyre::{Report, Result, WrapErr as _, anyhow};
14use serde::de;
15use serde::{Deserialize, Deserializer, Serialize};
16use std::collections::HashMap;
17#[cfg(not(target_arch = "wasm32"))]
18use std::path::{Path, PathBuf};
19use std::sync::LazyLock;
20use surver::SurverConfig;
21use tracing::info;
22
23use crate::hierarchy::{HierarchyStyle, ParameterDisplayLocation};
24use crate::keyboard_shortcuts::{SurferShortcuts, deserialize_shortcuts};
25use crate::mousegestures::GestureZones;
26use crate::time::TimeFormat;
27use crate::trace_style::TraceStyle;
28use crate::wave_container::VariableMeta;
29use crate::{clock_highlighting::ClockHighlightType, variable_name_type::VariableNameType};
30use surfer_translation_types::{VariableEncoding, VariableType};
31
32macro_rules! theme {
33    ($name:expr) => {
34        (
35            $name,
36            include_str!(concat!("../../themes/", $name, ".toml")),
37        )
38    };
39}
40
41macro_rules! named_theme {
42    ($name:expr, $file:expr) => {
43        (
44            $name,
45            include_str!(concat!("../../themes/", $file, ".toml")),
46        )
47    };
48}
49
50/// Built-in theme names and their corresponding embedded content
51static BUILTIN_THEMES: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
52    HashMap::from([
53        theme!("dark+"),
54        theme!("dark-high-contrast"),
55        named_theme!("IBM", "ibm"),
56        theme!("light+"),
57        theme!("light-high-contrast"),
58        named_theme!("Okabe/Ito", "okabe-ito"),
59        named_theme!("Petroff Dark", "petroff-dark"),
60        named_theme!("Petroff Light", "petroff-light"),
61        named_theme!("Rosé Pine", "rose-pine"),
62        named_theme!("Rosé Pine Moon", "rose-pine-moon"),
63        named_theme!("Rosé Pine Dawn", "rose-pine-dawn"),
64        named_theme!("Solarized", "solarized"),
65    ])
66});
67
68#[cfg(not(target_arch = "wasm32"))]
69pub static PROJECT_DIR: LazyLock<Option<ProjectDirs>> =
70    LazyLock::new(|| ProjectDirs::from("org", "surfer-project", "surfer"));
71#[cfg(not(target_arch = "wasm32"))]
72const OLD_CONFIG_FILE: &str = "surfer.toml";
73#[cfg(not(target_arch = "wasm32"))]
74const CONFIG_FILE: &str = "config.toml";
75#[cfg(not(target_arch = "wasm32"))]
76const THEMES_DIR: &str = "themes";
77#[cfg(not(target_arch = "wasm32"))]
78pub const LOCAL_DIR: &str = ".surfer";
79
80/// Select the function of the arrow keys
81#[derive(Clone, Copy, Debug, Deserialize, Display, FromStr, PartialEq, Eq, Sequence, Serialize)]
82pub enum ArrowKeyBindings {
83    /// The left/right arrow keys step to the next edge
84    Edge,
85
86    /// The left/right arrow keys scroll the viewport left/right
87    Scroll,
88}
89
90#[derive(Clone, Copy, Debug, Deserialize, Display, FromStr, PartialEq, Eq, Sequence, Serialize)]
91pub enum TransitionValue {
92    /// Transition value is the previous value
93    Previous,
94    /// Transition value is the next value
95    Next,
96    /// Transition value is both previous and next value
97    Both,
98}
99
100/// Select the function when dragging with primary mouse button
101#[derive(Debug, Deserialize, Display, PartialEq, Eq, Sequence, Serialize, Clone, Copy)]
102pub enum PrimaryMouseDrag {
103    /// The left/right arrow keys step to the next edge
104    #[display("Measure time")]
105    Measure,
106
107    /// The left/right arrow keys scroll the viewport left/right
108    #[display("Move cursor")]
109    Cursor,
110}
111
112#[derive(Debug, Deserialize, Display, PartialEq, Eq, Sequence, Serialize, Clone, Copy)]
113pub enum AutoLoad {
114    Always,
115    Never,
116    Ask,
117}
118
119impl AutoLoad {
120    #[must_use]
121    pub fn from_bool(auto_load: bool) -> Self {
122        if auto_load {
123            AutoLoad::Always
124        } else {
125            AutoLoad::Never
126        }
127    }
128}
129
130#[derive(Clone, Copy, Debug, Deserialize, Display, FromStr, PartialEq, Eq, Sequence, Serialize)]
131pub enum FocusHighlight {
132    Off,
133    Background,
134    LineWidth,
135    BrightnessShift,
136    LineWidthAndBrightnessShift,
137}
138
139#[derive(Debug, Deserialize)]
140pub struct SurferConfig {
141    pub layout: SurferLayout,
142    #[serde(deserialize_with = "deserialize_theme")]
143    pub theme: SurferTheme,
144    /// Mouse gesture configurations.
145    ///
146    /// Color and linewidth are configured in the theme using [`SurferTheme::gesture`].
147    pub gesture: SurferGesture,
148    pub behavior: SurferBehavior,
149    /// Time stamp format
150    pub default_time_format: TimeFormat,
151    pub default_variable_name_type: VariableNameType,
152    default_clock_highlight_type: ClockHighlightType,
153    /// Distance in pixels for cursor snap
154    #[serde(deserialize_with = "deserialize_non_negative_f32")]
155    pub snap_distance: f32,
156    /// Maximum size of the undo stack
157    pub undo_stack_size: usize,
158    /// Reload changed waves
159    autoreload_files: AutoLoad,
160    /// Load state file
161    autoload_sibling_state_files: AutoLoad,
162    /// Plugin configuration
163    pub plugin: PluginConfig,
164    /// WCP Configuration
165    pub wcp: WcpConfig,
166    /// HTTP Server Configuration
167    pub server: SurverConfig,
168    /// Animation time for UI elements in seconds
169    #[serde(deserialize_with = "deserialize_non_negative_f32")]
170    pub animation_time: f32,
171    /// UI animation enabled
172    pub animation_enabled: bool,
173    /// Maximum URL length for remote connections.
174    /// Should only be changed in case you are behind a proxy that limits the URL length
175    pub max_url_length: u16,
176    /// Keyboard shortcuts
177    #[serde(deserialize_with = "deserialize_shortcuts")]
178    pub shortcuts: SurferShortcuts,
179    /// Show the text label of dividers inline with the waveforms
180    pub show_divider_text: bool,
181}
182
183impl SurferConfig {
184    #[must_use]
185    pub fn default_clock_highlight_type(&self) -> ClockHighlightType {
186        self.default_clock_highlight_type
187    }
188
189    #[must_use]
190    pub fn autoload_sibling_state_files(&self) -> AutoLoad {
191        self.autoload_sibling_state_files
192    }
193
194    #[must_use]
195    pub fn autoreload_files(&self) -> AutoLoad {
196        self.autoreload_files
197    }
198
199    #[must_use]
200    pub fn animation_enabled(&self) -> bool {
201        self.animation_enabled
202    }
203
204    #[must_use]
205    pub fn show_divider_text(&self) -> bool {
206        self.show_divider_text
207    }
208}
209
210#[derive(Debug, Deserialize)]
211pub struct SurferLayout {
212    /// Flag to show/hide the hierarchy view
213    show_hierarchy: bool,
214    /// Flag to show/hide the menu
215    show_menu: bool,
216    /// Flag to show/hide toolbar
217    show_toolbar: bool,
218    /// Flag to show/hide tick lines
219    show_ticks: bool,
220    /// Flag to show/hide tooltip for variables
221    show_tooltip: bool,
222    /// Flag to show/hide tooltip for scopes
223    show_scope_tooltip: bool,
224    /// Flag to show/hide the overview
225    show_overview: bool,
226    /// Flag to show/hide the statusbar
227    show_statusbar: bool,
228    /// Flag to show/hide the indices of variables in the variable list
229    show_variable_indices: bool,
230    /// Flag to show/hide the variable direction icon
231    show_variable_direction: bool,
232    /// Flag to show/hide a default timeline
233    show_default_timeline: bool,
234    /// Flag to show/hide empty scopes
235    show_empty_scopes: bool,
236    /// Flag to show/hide scope and variable type icons in the hierarchy
237    show_hierarchy_icons: bool,
238    /// Where to show parameters in the hierarchy
239    parameter_display_location: ParameterDisplayLocation,
240    /// Initial window height
241    pub window_height: usize,
242    /// Initial window width
243    pub window_width: usize,
244    /// Initial window x-position
245    pub window_x_position: usize,
246    /// Initial window y-position
247    pub window_y_position: usize,
248    /// Align variable names right
249    align_names_right: bool,
250    /// Set style of hierarchy
251    hierarchy_style: HierarchyStyle,
252    /// Text size in points for values in waves
253    #[serde(deserialize_with = "deserialize_non_negative_f32")]
254    pub waveforms_text_size: f32,
255    /// Line height in points for waves
256    #[serde(deserialize_with = "deserialize_non_negative_f32")]
257    pub waveforms_line_height: f32,
258    /// Pixel gap between consecutive waveform traces
259    #[serde(deserialize_with = "deserialize_non_negative_f32")]
260    pub waveforms_gap: f32,
261    /// Line height multiples for higher variables
262    #[serde(deserialize_with = "deserialize_non_negative_f32_vec")]
263    pub waveforms_line_height_multiples: Vec<f32>,
264    /// Default analog waveform multiplier
265    #[serde(deserialize_with = "deserialize_non_negative_f32")]
266    pub analog_waveform_multiplier: f32,
267    /// Line height in points for transaction streams
268    #[serde(deserialize_with = "deserialize_non_negative_f32")]
269    pub transactions_line_height: f32,
270    /// UI zoom factors
271    #[serde(deserialize_with = "deserialize_non_negative_f32_vec")]
272    pub zoom_factors: Vec<f32>,
273    /// Default UI zoom factor
274    #[serde(deserialize_with = "deserialize_non_negative_f32")]
275    default_zoom_factor: f32,
276    /// How to highlight the focused waveform
277    focus_highlight: FocusHighlight,
278    /// Move the focus to the newly inserted marker?
279    move_focus_on_inserted_marker: bool,
280    /// Fill high values in boolean waveforms
281    fill_high_values: bool,
282    /// Draw unknown vector waveform values as a centered horizontal line
283    draw_vector_unknowns_as_line: bool,
284    /// Trace style for vector waveforms
285    trace_style: TraceStyle,
286    /// Value to display when cursor is on a transition
287    transition_value: TransitionValue,
288    /// Toolbar-specific layout settings
289    #[serde(default)]
290    toolbar: ToolbarLayout,
291    /// Enable time offset adjustment for waveforms which don't start at 0.
292    /// When enabled, the waveform canvas will be drawn from the first available time instead of from 0.
293    #[serde(default)]
294    pub enable_time_offset: bool,
295}
296
297#[derive(Debug, Deserialize, Default)]
298struct ToolbarLayout {
299    /// Default row number for each toolbar group
300    #[serde(default)]
301    row: HashMap<String, u8>,
302    /// Default visibility for each toolbar group
303    #[serde(default)]
304    visibility: HashMap<String, bool>,
305}
306
307impl SurferLayout {
308    #[must_use]
309    pub fn show_hierarchy(&self) -> bool {
310        self.show_hierarchy
311    }
312    #[must_use]
313    pub fn show_menu(&self) -> bool {
314        self.show_menu
315    }
316    #[must_use]
317    pub fn show_ticks(&self) -> bool {
318        self.show_ticks
319    }
320    #[must_use]
321    pub fn show_tooltip(&self) -> bool {
322        self.show_tooltip
323    }
324    #[must_use]
325    pub fn show_scope_tooltip(&self) -> bool {
326        self.show_scope_tooltip
327    }
328    #[must_use]
329    pub fn show_default_timeline(&self) -> bool {
330        self.show_default_timeline
331    }
332    #[must_use]
333    pub fn show_toolbar(&self) -> bool {
334        self.show_toolbar
335    }
336    #[must_use]
337    pub fn show_overview(&self) -> bool {
338        self.show_overview
339    }
340    #[must_use]
341    pub fn show_statusbar(&self) -> bool {
342        self.show_statusbar
343    }
344    #[must_use]
345    pub fn align_names_right(&self) -> bool {
346        self.align_names_right
347    }
348    #[must_use]
349    pub fn show_variable_indices(&self) -> bool {
350        self.show_variable_indices
351    }
352    #[must_use]
353    pub fn show_variable_direction(&self) -> bool {
354        self.show_variable_direction
355    }
356    #[must_use]
357    pub fn default_zoom_factor(&self) -> f32 {
358        self.default_zoom_factor
359    }
360    #[must_use]
361    pub fn show_empty_scopes(&self) -> bool {
362        self.show_empty_scopes
363    }
364    #[must_use]
365    pub fn show_hierarchy_icons(&self) -> bool {
366        self.show_hierarchy_icons
367    }
368    #[must_use]
369    pub fn parameter_display_location(&self) -> ParameterDisplayLocation {
370        self.parameter_display_location
371    }
372    #[must_use]
373    pub fn focus_highlight(&self) -> FocusHighlight {
374        self.focus_highlight
375    }
376    #[must_use]
377    pub fn move_focus_on_inserted_marker(&self) -> bool {
378        self.move_focus_on_inserted_marker
379    }
380    #[must_use]
381    pub fn fill_high_values(&self) -> bool {
382        self.fill_high_values
383    }
384    #[must_use]
385    pub fn draw_vector_unknowns_as_line(&self) -> bool {
386        self.draw_vector_unknowns_as_line
387    }
388    #[must_use]
389    pub fn hierarchy_style(&self) -> HierarchyStyle {
390        self.hierarchy_style
391    }
392    #[must_use]
393    pub fn trace_style(&self) -> TraceStyle {
394        self.trace_style
395    }
396    #[must_use]
397    pub fn transition_value(&self) -> TransitionValue {
398        self.transition_value
399    }
400    #[must_use]
401    pub fn toolbar_group_row(&self, id: &str) -> Option<u8> {
402        self.toolbar.row.get(id).copied()
403    }
404    #[must_use]
405    pub fn toolbar_group_visibility(&self, id: &str) -> Option<bool> {
406        self.toolbar.visibility.get(id).copied()
407    }
408    #[must_use]
409    pub fn enable_time_offset(&self) -> bool {
410        self.enable_time_offset
411    }
412}
413
414#[derive(Debug, Deserialize)]
415pub struct SurferBehavior {
416    /// Keep or remove variables if unavailable during reload
417    pub keep_during_reload: bool,
418    /// Number of entries to keep in file history.
419    pub file_history_size: usize,
420    /// Select the functionality bound to the arrow keys
421    arrow_key_bindings: ArrowKeyBindings,
422    /// Whether dragging with primary mouse button will measure time or move cursor
423    /// (press shift for the other)
424    primary_button_drag_behavior: PrimaryMouseDrag,
425}
426
427impl SurferBehavior {
428    #[must_use]
429    pub fn file_history_size(&self) -> usize {
430        self.file_history_size
431    }
432
433    #[must_use]
434    pub fn primary_button_drag_behavior(&self) -> PrimaryMouseDrag {
435        self.primary_button_drag_behavior
436    }
437
438    #[must_use]
439    pub fn arrow_key_bindings(&self) -> ArrowKeyBindings {
440        self.arrow_key_bindings
441    }
442}
443
444#[derive(Debug, Deserialize)]
445/// Mouse gesture configurations.
446///
447/// Color and linewidth are configured in the theme using [`SurferTheme::gesture`].
448pub struct SurferGesture {
449    /// Size of the overlay help
450    #[serde(deserialize_with = "deserialize_non_negative_f32")]
451    pub size: f32,
452    /// (Squared) minimum distance to move to remove the overlay help and perform gesture
453    #[serde(deserialize_with = "deserialize_non_negative_f32")]
454    pub deadzone: f32,
455    /// Circle radius for background as a factor of size/2
456    #[serde(deserialize_with = "deserialize_non_negative_f32")]
457    pub background_radius: f32,
458    /// Gamma factor for background circle, between 0 (opaque) and 1 (transparent)
459    #[serde(deserialize_with = "deserialize_unit_interval_f32")]
460    pub background_gamma: f32,
461    /// Mapping between the eight directions and actions
462    pub mapping: GestureZones,
463}
464
465#[derive(Clone, Debug, Deserialize)]
466pub struct SurferLineStyle {
467    #[serde(deserialize_with = "deserialize_hex_color")]
468    pub color: Color32,
469    #[serde(deserialize_with = "deserialize_non_negative_f32")]
470    pub width: f32,
471}
472
473impl From<SurferLineStyle> for Stroke {
474    fn from(style: SurferLineStyle) -> Self {
475        Stroke {
476            color: style.color,
477            width: style.width,
478        }
479    }
480}
481
482impl From<&SurferLineStyle> for Stroke {
483    fn from(style: &SurferLineStyle) -> Self {
484        Stroke {
485            color: style.color,
486            width: style.width,
487        }
488    }
489}
490
491impl From<&SurferLineStyle> for PathStroke {
492    fn from(style: &SurferLineStyle) -> Self {
493        PathStroke::new(style.width, style.color)
494    }
495}
496
497#[derive(Debug, Deserialize)]
498/// Tick mark configuration
499pub struct SurferTicks {
500    /// 0 to 1, where 1 means as many ticks that can fit without overlap
501    #[serde(deserialize_with = "deserialize_unit_interval_f32")]
502    pub density: f32,
503    /// Line style to use for ticks
504    pub style: SurferLineStyle,
505}
506
507#[derive(Debug, Deserialize)]
508pub struct SurferRelationArrow {
509    /// Arrow line style
510    pub style: SurferLineStyle,
511
512    /// Arrowhead angle in degrees
513    #[serde(deserialize_with = "deserialize_non_negative_f32")]
514    pub head_angle: f32,
515
516    /// Arrowhead length
517    #[serde(deserialize_with = "deserialize_non_negative_f32")]
518    pub head_length: f32,
519}
520
521#[derive(Debug, Deserialize)]
522pub struct SurferTheme {
523    /// Color used for text across the UI
524    #[serde(deserialize_with = "deserialize_hex_color")]
525    pub foreground: Color32,
526    #[serde(deserialize_with = "deserialize_hex_color")]
527    /// Color of borders between UI elements
528    pub border_color: Color32,
529    /// Color used for text across the markers
530    #[serde(deserialize_with = "deserialize_hex_color")]
531    pub alt_text_color: Color32,
532    /// Colors used for the background and text of the wave view
533    pub canvas_colors: ThemeColorTriple,
534    /// Colors used for most UI elements not on the variable canvas
535    pub primary_ui_color: ThemeColorPair,
536    /// Colors used for the variable and value list, as well as secondary elements
537    /// like text fields
538    pub secondary_ui_color: ThemeColorPair,
539    /// Color used for selected ui elements such as the currently selected hierarchy
540    pub selected_elements_colors: ThemeColorPair,
541
542    pub accent_info: ThemeColorPair,
543    pub accent_warn: ThemeColorPair,
544    pub accent_error: ThemeColorPair,
545
546    ///  Line style for cursor
547    pub cursor: SurferLineStyle,
548
549    /// Line style for mouse gesture lines
550    pub gesture: SurferLineStyle,
551
552    /// Line style for measurement lines
553    pub measure: SurferLineStyle,
554
555    /// Line style for rectangle annotations
556    pub annotation_rectangle: SurferLineStyle,
557
558    /// Line style for arrow annotations
559    pub annotation_arrow: SurferLineStyle,
560
561    ///  Line style for clock highlight lines
562    pub clock_highlight_line: SurferLineStyle,
563    #[serde(deserialize_with = "deserialize_hex_color_vec")]
564    /// Per-clock colors used for clock highlight lines in multi-clock views
565    pub clock_highlight_line_colors: Vec<Color32>,
566    #[serde(deserialize_with = "deserialize_hex_color")]
567    pub clock_highlight_cycle: Color32,
568    #[serde(deserialize_with = "deserialize_hex_color_vec")]
569    /// Per-clock colors used for clock highlight fills in multi-clock Cycle mode
570    pub clock_highlight_cycle_colors: Vec<Color32>,
571    /// Draw arrows on rising clock edges
572    pub clock_rising_marker: bool,
573
574    #[serde(deserialize_with = "deserialize_hex_color")]
575    /// Default variable color
576    pub variable_default: Color32,
577    #[serde(deserialize_with = "deserialize_hex_color")]
578    /// Color used for high-impedance variables
579    pub variable_highimp: Color32,
580    #[serde(deserialize_with = "deserialize_hex_color")]
581    /// Color used for undefined variables
582    pub variable_undef: Color32,
583    #[serde(deserialize_with = "deserialize_hex_color")]
584    /// Color used for don't-care variables
585    pub variable_dontcare: Color32,
586    #[serde(deserialize_with = "deserialize_hex_color")]
587    /// Color used for weak variables
588    pub variable_weak: Color32,
589    #[serde(deserialize_with = "deserialize_hex_color")]
590    /// Color used for constant variables (parameters)
591    pub variable_parameter: Color32,
592    #[serde(deserialize_with = "deserialize_hex_color")]
593    /// Default transaction color
594    pub transaction_default: Color32,
595    // Relation arrows of transactions
596    pub relation_arrow: SurferRelationArrow,
597    #[serde(deserialize_with = "deserialize_hex_color")]
598    /// Color used for constant variables (parameters)
599    pub variable_event: Color32,
600
601    /// Opacity with which variable backgrounds are drawn. 0 is fully transparent and 1 is fully
602    /// opaque.
603    #[serde(deserialize_with = "deserialize_unit_interval_f32")]
604    pub waveform_opacity: f32,
605    /// Opacity of variable backgrounds for wide signals (signals with more than one bit)
606    #[serde(deserialize_with = "deserialize_unit_interval_f32")]
607    pub wide_opacity: f32,
608
609    #[serde(deserialize_with = "deserialize_color_map")]
610    pub colors: HashMap<String, Color32>,
611    #[serde(deserialize_with = "deserialize_hex_color")]
612    pub highlight_background: Color32,
613
614    /// Variable line width
615    #[serde(deserialize_with = "deserialize_non_negative_f32")]
616    pub linewidth: f32,
617
618    /// Variable line width for accented variables
619    #[serde(deserialize_with = "deserialize_non_negative_f32")]
620    pub thick_linewidth: f32,
621    /// Line width multiplier for focused waveform traces
622    #[serde(deserialize_with = "deserialize_non_negative_f32")]
623    pub focus_highlight_line_width_multiplier: f32,
624    /// Brightness shift (0.0 to 1.0) for focused waveform traces. Lightens the color on dark backgrounds, darkens on light backgrounds.
625    #[serde(deserialize_with = "deserialize_unit_interval_f32")]
626    pub focus_highlight_brightness_shift: f32,
627
628    /// Vector transition max width
629    #[serde(deserialize_with = "deserialize_non_negative_f32")]
630    pub vector_transition_width: f32,
631
632    /// Number of lines using standard background before changing to
633    /// alternate background and so on, set to zero to disable
634    pub alt_frequency: usize,
635
636    /// Viewport separator line
637    pub viewport_separator: SurferLineStyle,
638
639    // Drag hint and threshold parameters
640    #[serde(deserialize_with = "deserialize_hex_color")]
641    pub drag_hint_color: Color32,
642    #[serde(deserialize_with = "deserialize_non_negative_f32")]
643    pub drag_hint_width: f32,
644    #[serde(deserialize_with = "deserialize_non_negative_f32")]
645    pub drag_threshold: f32,
646
647    /// Tick information
648    pub ticks: SurferTicks,
649
650    /// List of theme names
651    pub theme_names: Vec<String>,
652
653    /// The name of the currently selected theme, or empty for the default theme
654    #[serde(default)]
655    pub theme_name: String,
656
657    /// Icons for scope types in the hierarchy view
658    #[serde(default)]
659    pub scope_icons: ScopeIcons,
660
661    /// Icons for variable types in the hierarchy view
662    #[serde(default)]
663    pub variable_icons: VariableIcons,
664}
665
666/// Colors for different scope type icons in the hierarchy view.
667#[derive(Clone, Debug, Deserialize)]
668#[serde(default)]
669pub struct ScopeIconColors {
670    #[serde(deserialize_with = "deserialize_hex_color")]
671    pub module: Color32,
672    #[serde(deserialize_with = "deserialize_hex_color")]
673    pub task: Color32,
674    #[serde(deserialize_with = "deserialize_hex_color")]
675    pub function: Color32,
676    #[serde(deserialize_with = "deserialize_hex_color")]
677    pub begin: Color32,
678    #[serde(deserialize_with = "deserialize_hex_color")]
679    pub fork: Color32,
680    #[serde(deserialize_with = "deserialize_hex_color")]
681    pub generate: Color32,
682    #[serde(rename = "struct", deserialize_with = "deserialize_hex_color")]
683    pub struct_: Color32,
684    #[serde(deserialize_with = "deserialize_hex_color")]
685    pub union: Color32,
686    #[serde(deserialize_with = "deserialize_hex_color")]
687    pub class: Color32,
688    #[serde(deserialize_with = "deserialize_hex_color")]
689    pub interface: Color32,
690    #[serde(deserialize_with = "deserialize_hex_color")]
691    pub package: Color32,
692    #[serde(deserialize_with = "deserialize_hex_color")]
693    pub program: Color32,
694    #[serde(deserialize_with = "deserialize_hex_color")]
695    pub vhdl_architecture: Color32,
696    #[serde(deserialize_with = "deserialize_hex_color")]
697    pub vhdl_procedure: Color32,
698    #[serde(deserialize_with = "deserialize_hex_color")]
699    pub vhdl_function: Color32,
700    #[serde(deserialize_with = "deserialize_hex_color")]
701    pub vhdl_record: Color32,
702    #[serde(deserialize_with = "deserialize_hex_color")]
703    pub vhdl_process: Color32,
704    #[serde(deserialize_with = "deserialize_hex_color")]
705    pub vhdl_block: Color32,
706    #[serde(deserialize_with = "deserialize_hex_color")]
707    pub vhdl_for_generate: Color32,
708    #[serde(deserialize_with = "deserialize_hex_color")]
709    pub vhdl_if_generate: Color32,
710    #[serde(deserialize_with = "deserialize_hex_color")]
711    pub vhdl_generate: Color32,
712    #[serde(deserialize_with = "deserialize_hex_color")]
713    pub vhdl_package: Color32,
714    #[serde(deserialize_with = "deserialize_hex_color")]
715    pub ghw_generic: Color32,
716    #[serde(deserialize_with = "deserialize_hex_color")]
717    pub vhdl_array: Color32,
718    #[serde(deserialize_with = "deserialize_hex_color")]
719    pub unknown: Color32,
720    #[serde(deserialize_with = "deserialize_hex_color")]
721    pub clocking: Color32,
722    #[serde(deserialize_with = "deserialize_hex_color")]
723    pub sv_array: Color32,
724}
725
726impl Default for ScopeIconColors {
727    fn default() -> Self {
728        Self {
729            module: Color32::from_rgb(0x4F, 0xC3, 0xF7), // Light Blue
730            task: Color32::from_rgb(0xFF, 0xB7, 0x4D),   // Orange
731            function: Color32::from_rgb(0xBA, 0x68, 0xC8), // Purple
732            begin: Color32::from_rgb(0x81, 0xC7, 0x84),  // Green
733            fork: Color32::from_rgb(0xFF, 0x80, 0x80),   // Red
734            generate: Color32::from_rgb(0x64, 0xB5, 0xF6), // Blue
735            struct_: Color32::from_rgb(0x4D, 0xD0, 0xE1), // Cyan
736            union: Color32::from_rgb(0x4D, 0xD0, 0xE1),  // Cyan
737            class: Color32::from_rgb(0xF0, 0x62, 0x92),  // Pink
738            interface: Color32::from_rgb(0xAE, 0xD5, 0x81), // Light Green
739            package: Color32::from_rgb(0xFF, 0xD5, 0x4F), // Yellow
740            program: Color32::from_rgb(0xA1, 0x88, 0x7F), // Brown
741            vhdl_architecture: Color32::from_rgb(0x4F, 0xC3, 0xF7), // Light Blue (like module)
742            vhdl_procedure: Color32::from_rgb(0xFF, 0xB7, 0x4D), // Orange (like task)
743            vhdl_function: Color32::from_rgb(0xBA, 0x68, 0xC8), // Purple (like function)
744            vhdl_record: Color32::from_rgb(0x4D, 0xD0, 0xE1), // Cyan (like struct)
745            vhdl_process: Color32::from_rgb(0x81, 0xC7, 0x84), // Green (like begin)
746            vhdl_block: Color32::from_rgb(0x90, 0xA4, 0xAE), // Blue Grey
747            vhdl_for_generate: Color32::from_rgb(0x64, 0xB5, 0xF6), // Blue (like generate)
748            vhdl_if_generate: Color32::from_rgb(0x64, 0xB5, 0xF6), // Blue (like generate)
749            vhdl_generate: Color32::from_rgb(0x64, 0xB5, 0xF6), // Blue (like generate)
750            vhdl_package: Color32::from_rgb(0xFF, 0xD5, 0x4F), // Yellow (like package)
751            ghw_generic: Color32::from_rgb(0xB0, 0xBE, 0xC5), // Blue Grey Light
752            vhdl_array: Color32::from_rgb(0xCE, 0x93, 0xD8), // Light Purple
753            clocking: Color32::from_rgb(0xF0, 0x62, 0x92), // Pink (like class)
754            sv_array: Color32::from_rgb(0xCE, 0x93, 0xD8), // Light Purple (like vhdl_array)
755            unknown: Color32::from_rgb(0x9E, 0x9E, 0x9E), // Grey
756        }
757    }
758}
759
760/// Icons for different scope types in the hierarchy view.
761/// Each field maps to a `wellen::ScopeType` and contains a Remix icon string.
762#[derive(Clone, Debug, Deserialize)]
763#[serde(default)]
764pub struct ScopeIcons {
765    // Verilog/SystemVerilog scope types
766    pub module: String,
767    pub task: String,
768    pub function: String,
769    pub begin: String,
770    pub fork: String,
771    pub generate: String,
772    #[serde(rename = "struct")]
773    pub struct_: String,
774    pub union: String,
775    pub class: String,
776    pub interface: String,
777    pub package: String,
778    pub program: String,
779    // VHDL scope types
780    pub vhdl_architecture: String,
781    pub vhdl_procedure: String,
782    pub vhdl_function: String,
783    pub vhdl_record: String,
784    pub vhdl_process: String,
785    pub vhdl_block: String,
786    pub vhdl_for_generate: String,
787    pub vhdl_if_generate: String,
788    pub vhdl_generate: String,
789    pub vhdl_package: String,
790    pub ghw_generic: String,
791    pub vhdl_array: String,
792    pub unknown: String,
793    pub clocking: String,
794    pub sv_array: String,
795    /// Colors for scope icons
796    #[serde(default)]
797    pub colors: ScopeIconColors,
798}
799
800impl Default for ScopeIcons {
801    fn default() -> Self {
802        use egui_remixicon::icons;
803        Self {
804            // Verilog/SystemVerilog scope types
805            module: icons::CPU_LINE.to_string(),
806            task: icons::TASK_LINE.to_string(),
807            function: icons::BRACES_LINE.to_string(),
808            begin: icons::CODE_BOX_LINE.to_string(),
809            fork: icons::GIT_BRANCH_LINE.to_string(),
810            generate: icons::REPEAT_LINE.to_string(),
811            struct_: icons::TABLE_LINE.to_string(),
812            union: icons::MERGE_CELLS_HORIZONTAL.to_string(),
813            class: icons::TABLE_LINE.to_string(),
814            interface: icons::PLUG_LINE.to_string(),
815            package: icons::BOX_3_LINE.to_string(),
816            program: icons::FILE_CODE_LINE.to_string(),
817            // VHDL scope types
818            vhdl_architecture: icons::CPU_LINE.to_string(),
819            vhdl_procedure: icons::TERMINAL_LINE.to_string(),
820            vhdl_function: icons::BRACES_LINE.to_string(),
821            vhdl_record: icons::TABLE_LINE.to_string(),
822            vhdl_process: icons::FLASHLIGHT_LINE.to_string(),
823            vhdl_block: icons::CODE_BLOCK.to_string(),
824            vhdl_for_generate: icons::REPEAT_LINE.to_string(),
825            vhdl_if_generate: icons::QUESTION_LINE.to_string(),
826            vhdl_generate: icons::REPEAT_LINE.to_string(),
827            vhdl_package: icons::BOX_3_LINE.to_string(),
828            ghw_generic: icons::SETTINGS_3_LINE.to_string(),
829            vhdl_array: icons::BRACKETS_LINE.to_string(),
830            sv_array: icons::BRACKETS_LINE.to_string(),
831            clocking: icons::TIME_LINE.to_string(),
832            unknown: icons::QUESTION_LINE.to_string(),
833            colors: ScopeIconColors::default(),
834        }
835    }
836}
837
838impl ScopeIcons {
839    /// Returns the icon and color for a given scope type.
840    /// If `scope_type` is `None`, returns the default module icon and color.
841    #[must_use]
842    pub fn get_icon(&self, scope_type: Option<wellen::ScopeType>) -> (&str, Color32) {
843        use wellen::ScopeType;
844        match scope_type {
845            None => (&self.module, self.colors.module),
846            Some(st) => match st {
847                ScopeType::Module => (&self.module, self.colors.module),
848                ScopeType::Task => (&self.task, self.colors.task),
849                ScopeType::Function => (&self.function, self.colors.function),
850                ScopeType::Begin => (&self.begin, self.colors.begin),
851                ScopeType::Fork => (&self.fork, self.colors.fork),
852                ScopeType::Generate => (&self.generate, self.colors.generate),
853                ScopeType::Struct => (&self.struct_, self.colors.struct_),
854                ScopeType::Union => (&self.union, self.colors.union),
855                ScopeType::Class => (&self.class, self.colors.class),
856                ScopeType::Interface => (&self.interface, self.colors.interface),
857                ScopeType::Package => (&self.package, self.colors.package),
858                ScopeType::Program => (&self.program, self.colors.program),
859                ScopeType::VhdlArchitecture => {
860                    (&self.vhdl_architecture, self.colors.vhdl_architecture)
861                }
862                ScopeType::VhdlProcedure => (&self.vhdl_procedure, self.colors.vhdl_procedure),
863                ScopeType::VhdlFunction => (&self.vhdl_function, self.colors.vhdl_function),
864                ScopeType::VhdlRecord => (&self.vhdl_record, self.colors.vhdl_record),
865                ScopeType::VhdlProcess => (&self.vhdl_process, self.colors.vhdl_process),
866                ScopeType::VhdlBlock => (&self.vhdl_block, self.colors.vhdl_block),
867                ScopeType::VhdlForGenerate => {
868                    (&self.vhdl_for_generate, self.colors.vhdl_for_generate)
869                }
870                ScopeType::VhdlIfGenerate => (&self.vhdl_if_generate, self.colors.vhdl_if_generate),
871                ScopeType::VhdlGenerate => (&self.vhdl_generate, self.colors.vhdl_generate),
872                ScopeType::VhdlPackage => (&self.vhdl_package, self.colors.vhdl_package),
873                ScopeType::GhwGeneric => (&self.ghw_generic, self.colors.ghw_generic),
874                ScopeType::VhdlArray => (&self.vhdl_array, self.colors.vhdl_array),
875                ScopeType::Unknown => (&self.unknown, self.colors.unknown),
876                ScopeType::SvArray => (&self.sv_array, self.colors.sv_array),
877                ScopeType::Clocking => (&self.clocking, self.colors.clocking),
878                _ => (&self.unknown, self.colors.unknown),
879            },
880        }
881    }
882}
883
884/// Colors for different variable type icons in the hierarchy view.
885/// Each field contains a Color32 value for the corresponding variable type.
886#[derive(Clone, Debug, Deserialize)]
887#[serde(default)]
888pub struct VariableIconColors {
889    /// Color for 1-bit wire signals
890    #[serde(deserialize_with = "deserialize_hex_color")]
891    pub wire: Color32,
892    /// Color for multi-bit bus signals
893    #[serde(deserialize_with = "deserialize_hex_color")]
894    pub bus: Color32,
895    /// Color for string variables
896    #[serde(deserialize_with = "deserialize_hex_color")]
897    pub string: Color32,
898    /// Color for event variables
899    #[serde(deserialize_with = "deserialize_hex_color")]
900    pub event: Color32,
901    /// Color for other types (integers, floats, enums)
902    #[serde(deserialize_with = "deserialize_hex_color")]
903    pub other: Color32,
904}
905
906impl Default for VariableIconColors {
907    fn default() -> Self {
908        Self {
909            wire: Color32::from_rgb(0x81, 0xC7, 0x84),   // Green
910            bus: Color32::from_rgb(0x64, 0xB5, 0xF6),    // Blue
911            string: Color32::from_rgb(0xFF, 0xB7, 0x4D), // Orange
912            event: Color32::from_rgb(0xF0, 0x62, 0x92),  // Pink
913            other: Color32::from_rgb(0xBA, 0x68, 0xC8),  // Purple
914        }
915    }
916}
917
918/// Icons for different variable types in the hierarchy view.
919/// Each field contains a Remix icon string.
920#[derive(Clone, Debug, Deserialize)]
921#[serde(default)]
922pub struct VariableIcons {
923    /// 1-bit wire signals
924    pub wire: String,
925    /// Multi-bit bus signals
926    pub bus: String,
927    /// String variables
928    pub string: String,
929    /// Event variables
930    pub event: String,
931    /// Other types (integers, floats, enums)
932    pub other: String,
933    /// Colors for variable icons
934    #[serde(default)]
935    pub colors: VariableIconColors,
936}
937
938impl Default for VariableIcons {
939    fn default() -> Self {
940        use egui_remixicon::icons;
941        Self {
942            wire: icons::GIT_COMMIT_LINE.to_string(),
943            bus: icons::BRACKETS_LINE.to_string(),
944            string: icons::TEXT.to_string(),
945            event: icons::ARROW_UP_LONG_LINE.to_string(),
946            other: icons::NUMBERS_LINE.to_string(),
947            colors: VariableIconColors::default(),
948        }
949    }
950}
951
952impl VariableIcons {
953    /// Returns the icon and color for a given variable meta.
954    /// If `meta` is `None`, returns the default "other" icon and color.
955    #[must_use]
956    pub fn get_icon(&self, meta: Option<&VariableMeta>) -> (&str, Color32) {
957        let Some(meta) = meta else {
958            return (&self.other, self.colors.other);
959        };
960
961        if matches!(
962            meta.variable_type,
963            Some(VariableType::VCDEvent | VariableType::EventParameter)
964        ) {
965            return (&self.event, self.colors.event);
966        }
967
968        match meta.encoding {
969            VariableEncoding::String => (&self.string, self.colors.string),
970            VariableEncoding::Real => (&self.other, self.colors.other),
971            VariableEncoding::BitVector => match meta.num_bits {
972                Some(1) => (&self.wire, self.colors.wire),
973                Some(n) if n > 1 => (&self.bus, self.colors.bus),
974                _ => (&self.other, self.colors.other),
975            },
976        }
977    }
978}
979
980fn gamma_correction(value: u8) -> f32 {
981    const INV_3294: f32 = 1.0 / 3294.0;
982    const INV_269: f32 = 1.0 / 269.0;
983
984    let v = f32::from(value);
985    if value < 10 {
986        v * INV_3294
987    } else {
988        (v * INV_269 + 0.0513).powf(2.4)
989    }
990}
991
992pub(crate) fn get_luminance(color: Color32) -> f32 {
993    0.2126 * gamma_correction(color.r())
994        + 0.7152 * gamma_correction(color.g())
995        + 0.0722 * gamma_correction(color.b())
996}
997
998impl SurferTheme {
999    #[must_use]
1000    pub fn get_color(&self, color: &str) -> Option<Color32> {
1001        self.colors.get(color).copied()
1002    }
1003
1004    #[must_use]
1005    pub fn get_best_text_color(&self, backgroundcolor: Color32) -> Color32 {
1006        // Based on https://ux.stackexchange.com/questions/82056/how-to-measure-the-contrast-between-any-given-color-and-white
1007
1008        // Compute luminance
1009        let l_foreground = get_luminance(self.foreground);
1010        let l_alt_text_color = get_luminance(self.alt_text_color);
1011        let l_background = get_luminance(backgroundcolor);
1012
1013        // Compute contrast ratio
1014        let mut cr_foreground = (l_foreground + 0.05) / (l_background + 0.05);
1015        cr_foreground = cr_foreground.max(1. / cr_foreground);
1016        let mut cr_alt_text_color = (l_alt_text_color + 0.05) / (l_background + 0.05);
1017        cr_alt_text_color = cr_alt_text_color.max(1. / cr_alt_text_color);
1018
1019        // Return color with highest contrast
1020        if cr_foreground > cr_alt_text_color {
1021            self.foreground
1022        } else {
1023            self.alt_text_color
1024        }
1025    }
1026
1027    fn generate_defaults(
1028        theme_name: Option<&String>,
1029    ) -> (ConfigBuilder<DefaultState>, Vec<String>) {
1030        let default_theme = String::from(include_str!("../../default_theme.toml"));
1031
1032        let mut theme = Config::builder().add_source(config::File::from_str(
1033            &default_theme,
1034            config::FileFormat::Toml,
1035        ));
1036
1037        let theme_names = all_theme_names();
1038
1039        let override_theme = theme_name
1040            .as_ref()
1041            .and_then(|name| BUILTIN_THEMES.get(name.as_str()).copied())
1042            .unwrap_or("");
1043
1044        theme = theme.add_source(config::File::from_str(
1045            override_theme,
1046            config::FileFormat::Toml,
1047        ));
1048        (theme, theme_names)
1049    }
1050
1051    #[cfg(target_arch = "wasm32")]
1052    pub fn new(theme_name: Option<String>) -> Result<Self> {
1053        let theme_name = theme_name.filter(|s| !s.is_empty());
1054        let (theme, _) = Self::generate_defaults(theme_name.as_ref());
1055
1056        let theme = theme.set_override("theme_names", all_theme_names())?;
1057
1058        let mut result: SurferTheme = theme
1059            .build()?
1060            .try_deserialize()
1061            .map_err(|e| anyhow!("Failed to parse config {e}"))?;
1062        result.theme_name = theme_name.unwrap_or_default();
1063        Ok(result)
1064    }
1065
1066    #[cfg(not(target_arch = "wasm32"))]
1067    pub fn new(theme_name: Option<String>) -> Result<Self> {
1068        use std::fs::ReadDir;
1069
1070        let theme_name = theme_name.filter(|s| !s.is_empty());
1071        let (mut theme, mut theme_names) = Self::generate_defaults(theme_name.as_ref());
1072
1073        let mut add_themes_from_dir = |dir: ReadDir| {
1074            for theme in dir.flatten() {
1075                if let Ok(theme_path) = theme.file_name().into_string()
1076                    && let Some(fname_str) = theme_path.strip_suffix(".toml")
1077                {
1078                    let fname = fname_str.to_string();
1079                    if !fname.is_empty() && !theme_names.contains(&fname) {
1080                        theme_names.push(fname);
1081                    }
1082                }
1083            }
1084        };
1085
1086        // read themes from config directory
1087        if let Some(proj_dirs) = &*PROJECT_DIR {
1088            let config_themes_dir = proj_dirs.config_dir().join(THEMES_DIR);
1089            if let Ok(config_themes_dir) = std::fs::read_dir(config_themes_dir) {
1090                add_themes_from_dir(config_themes_dir);
1091            }
1092        }
1093
1094        // Read themes from local directories.
1095        let local_config_dirs = find_local_configs();
1096
1097        // Add any existing themes from most top-level to most local. This allows overwriting of
1098        // higher-level theme settings with a local `.surfer` directory.
1099        local_config_dirs
1100            .iter()
1101            .filter_map(|p| std::fs::read_dir(p.join(THEMES_DIR)).ok())
1102            .for_each(add_themes_from_dir);
1103
1104        if let Some(name) = theme_name.as_ref()
1105            && !name.is_empty()
1106        {
1107            let theme_path = Utf8Path::new(THEMES_DIR).join(name.to_owned() + ".toml");
1108
1109            // First filter out all the existing local themes and add them in the aforementioned
1110            // order.
1111            let local_themes: Vec<Utf8PathBuf> = local_config_dirs
1112                .iter()
1113                .map(|p| p.join(&theme_path))
1114                .filter(|p| p.exists())
1115                .collect();
1116            if local_themes.is_empty() {
1117                // If no local themes exist, search in the config directory.
1118                if let Some(proj_dirs) = &*PROJECT_DIR {
1119                    let config_theme_path = proj_dirs.config_dir().join(theme_path);
1120                    if config_theme_path.exists() {
1121                        theme = theme.add_source(File::from(config_theme_path).required(false));
1122                    }
1123                }
1124            } else {
1125                theme = local_themes.into_iter().fold(theme, |t, p| {
1126                    t.add_source(File::from(p.into_std_path_buf()).required(false))
1127                });
1128            }
1129        }
1130
1131        let theme = theme.set_override("theme_names", theme_names)?;
1132
1133        let mut result: SurferTheme = theme
1134            .build()?
1135            .try_deserialize()
1136            .map_err(|e| anyhow!("Failed to parse theme {e}"))?;
1137        result.theme_name = theme_name.unwrap_or_default();
1138        Ok(result)
1139    }
1140}
1141
1142#[derive(Debug, Deserialize)]
1143pub struct ThemeColorPair {
1144    #[serde(deserialize_with = "deserialize_hex_color")]
1145    pub foreground: Color32,
1146    #[serde(deserialize_with = "deserialize_hex_color")]
1147    pub background: Color32,
1148}
1149
1150#[derive(Debug, Deserialize)]
1151pub struct ThemeColorTriple {
1152    #[serde(deserialize_with = "deserialize_hex_color")]
1153    pub foreground: Color32,
1154    #[serde(deserialize_with = "deserialize_hex_color")]
1155    pub background: Color32,
1156    #[serde(deserialize_with = "deserialize_hex_color")]
1157    pub alt_background: Color32,
1158}
1159
1160#[derive(Debug, Deserialize)]
1161pub struct PluginConfig {
1162    /// Maximum memory in MiB available to each WASM translator plugin
1163    pub max_memory_mib: u64,
1164}
1165
1166#[derive(Debug, Deserialize)]
1167pub struct WcpConfig {
1168    /// Controls if a server is started after Surfer is launched
1169    pub autostart: bool,
1170    /// Address to bind to (address:port)
1171    pub address: String,
1172}
1173
1174impl SurferConfig {
1175    #[cfg(target_arch = "wasm32")]
1176    pub fn new(_force_default_config: bool) -> Result<Self> {
1177        Self::new_from_toml(&include_str!("../../default_config.toml"))
1178    }
1179
1180    #[cfg(not(target_arch = "wasm32"))]
1181    pub fn new(force_default_config: bool) -> Result<Self> {
1182        use tracing::warn;
1183
1184        let default_config = String::from(include_str!("../../default_config.toml"));
1185
1186        let mut config = Config::builder().add_source(config::File::from_str(
1187            &default_config,
1188            config::FileFormat::Toml,
1189        ));
1190
1191        let config = if force_default_config {
1192            config
1193        } else {
1194            if let Some(proj_dirs) = &*PROJECT_DIR {
1195                let config_file = proj_dirs.config_dir().join(CONFIG_FILE);
1196                config = config.add_source(File::from(config_file).required(false));
1197            }
1198
1199            let old_config_path = Utf8Path::new(OLD_CONFIG_FILE);
1200            if old_config_path.exists() {
1201                warn!(
1202                    "Configuration in 'surfer.toml' is deprecated. Please move your configuration to '.surfer/config.toml'."
1203                );
1204            }
1205
1206            // `surfer.toml` will not be searched for upward, as it is deprecated.
1207            config = config.add_source(File::from(old_config_path.as_std_path()).required(false));
1208
1209            // Add configs from most top-level to most local. This allows overwriting of
1210            // higher-level settings with a local `.surfer` directory.
1211            find_local_configs()
1212                .into_iter()
1213                .fold(config, |c, p| {
1214                    c.add_source(
1215                        File::from(p.join(CONFIG_FILE).into_std_path_buf()).required(false),
1216                    )
1217                })
1218                .add_source(Environment::with_prefix("surfer")) // Add environment finally
1219        };
1220
1221        config
1222            .build()?
1223            .try_deserialize()
1224            .map_err(|e| anyhow!("Failed to parse config {e}"))
1225    }
1226
1227    pub fn new_from_toml(config: &str) -> Result<Self> {
1228        Ok(toml::from_str(config)?)
1229    }
1230}
1231
1232impl Default for SurferConfig {
1233    fn default() -> Self {
1234        Self::new(false).expect("Failed to load default config")
1235    }
1236}
1237
1238fn hex_string_to_color32(str: &str) -> Result<Color32> {
1239    let str = str.strip_prefix('#').unwrap_or(str).to_string();
1240    let str = if str.len() == 3 {
1241        str.chars().flat_map(|c| [c, c]).collect()
1242    } else {
1243        str
1244    };
1245    if str.len() == 6 {
1246        let r = u8::from_str_radix(&str[0..2], 16)
1247            .with_context(|| format!("'{str}' is not a valid RGB hex color"))?;
1248        let g = u8::from_str_radix(&str[2..4], 16)
1249            .with_context(|| format!("'{str}' is not a valid RGB hex color"))?;
1250        let b = u8::from_str_radix(&str[4..6], 16)
1251            .with_context(|| format!("'{str}' is not a valid RGB hex color"))?;
1252        Ok(Color32::from_rgb(r, g, b))
1253    } else {
1254        Result::Err(Report::msg(format!("'{str}' is not a valid RGB hex color")))
1255    }
1256}
1257
1258fn all_theme_names() -> Vec<String> {
1259    let mut names: Vec<String> = BUILTIN_THEMES.keys().map(ToString::to_string).collect();
1260    names.sort();
1261    names
1262}
1263
1264fn deserialize_hex_color<'de, D>(deserializer: D) -> Result<Color32, D::Error>
1265where
1266    D: Deserializer<'de>,
1267{
1268    let buf = String::deserialize(deserializer)?;
1269    hex_string_to_color32(&buf).map_err(de::Error::custom)
1270}
1271
1272fn deserialize_color_map<'de, D>(deserializer: D) -> Result<HashMap<String, Color32>, D::Error>
1273where
1274    D: Deserializer<'de>,
1275{
1276    #[derive(Deserialize)]
1277    struct Wrapper(#[serde(deserialize_with = "deserialize_hex_color")] Color32);
1278
1279    let v = HashMap::<String, Wrapper>::deserialize(deserializer)?;
1280    Ok(v.into_iter().map(|(k, Wrapper(v))| (k, v)).collect())
1281}
1282
1283fn deserialize_hex_color_vec<'de, D>(deserializer: D) -> Result<Vec<Color32>, D::Error>
1284where
1285    D: Deserializer<'de>,
1286{
1287    #[derive(Deserialize)]
1288    struct Wrapper(#[serde(deserialize_with = "deserialize_hex_color")] Color32);
1289
1290    let v = Vec::<Wrapper>::deserialize(deserializer)?;
1291    Ok(v.into_iter().map(|Wrapper(v)| v).collect())
1292}
1293
1294fn deserialize_theme<'de, D>(deserializer: D) -> Result<SurferTheme, D::Error>
1295where
1296    D: Deserializer<'de>,
1297{
1298    let buf = String::deserialize(deserializer)?;
1299    SurferTheme::new(Some(buf)).map_err(de::Error::custom)
1300}
1301
1302fn deserialize_non_negative_f32<'de, D>(deserializer: D) -> Result<f32, D::Error>
1303where
1304    D: Deserializer<'de>,
1305{
1306    let value = f32::deserialize(deserializer)?;
1307    Ok(value.max(0.0))
1308}
1309
1310fn deserialize_non_negative_f32_vec<'de, D>(deserializer: D) -> Result<Vec<f32>, D::Error>
1311where
1312    D: Deserializer<'de>,
1313{
1314    let values = Vec::<f32>::deserialize(deserializer)?;
1315    Ok(values.into_iter().map(|v| v.max(0.0)).collect())
1316}
1317
1318fn deserialize_unit_interval_f32<'de, D>(deserializer: D) -> Result<f32, D::Error>
1319where
1320    D: Deserializer<'de>,
1321{
1322    let value = f32::deserialize(deserializer)?;
1323    Ok(value.clamp(0.0, 1.0))
1324}
1325
1326/// Searches for `.surfer` directories upward from the current location until it reaches root.
1327///
1328/// Returns an empty vector in case the search fails in any way. If any `.surfer` directories
1329/// are found, they will be returned in a `Vec<Utf8PathBuf>` in a pre-order of most top-level to most
1330/// local. All plain files are ignored.
1331#[cfg(not(target_arch = "wasm32"))]
1332#[must_use]
1333pub fn find_local_configs() -> Vec<Utf8PathBuf> {
1334    use crate::util::search_upward;
1335    match std::env::current_dir() {
1336        Ok(dir) => {
1337            let root = dir
1338                .ancestors()
1339                .last()
1340                .map_or_else(|| PathBuf::from("/"), Path::to_path_buf);
1341            search_upward(dir, root, LOCAL_DIR)
1342                .into_iter()
1343                .filter(|p| p.is_dir()) // Only keep directories and ignore plain files.
1344                .rev() // Reverse for pre-order traversal of directories.
1345                .collect()
1346        }
1347        Err(_) => vec![],
1348    }
1349}
1350
1351#[cfg(not(target_arch = "wasm32"))]
1352pub fn write_default_config() -> eyre::Result<()> {
1353    use std::fs;
1354
1355    let default_config = include_str!("../../default_config.toml");
1356
1357    if let Some(proj_dirs) = &*PROJECT_DIR {
1358        let config_dir = proj_dirs.config_dir();
1359        let config_path = Utf8PathBuf::from_path_buf(config_dir.join(CONFIG_FILE))
1360            .map_err(|p| eyre::eyre!("Config path '{}' contains invalid UTF-8", p.display()))?;
1361
1362        if config_path.exists() {
1363            return Err(eyre::eyre!(
1364                "Config file already exists at {config_path}. Delete it first if you want to recreate it."
1365            ));
1366        }
1367
1368        fs::create_dir_all(config_dir)?;
1369
1370        fs::write(&config_path, default_config)?;
1371
1372        info!("Default config written to {config_path}");
1373    }
1374
1375    Ok(())
1376}
1377
1378#[cfg(test)]
1379mod tests {
1380    use super::*;
1381
1382    #[test]
1383    fn test_hex_string_3_chars() {
1384        // Test that 3-character hex strings are doubled correctly
1385        let result = hex_string_to_color32("abc").unwrap();
1386        let expected = Color32::from_rgb(0xaa, 0xbb, 0xcc);
1387        assert_eq!(result, expected);
1388    }
1389
1390    #[test]
1391    fn test_hex_string_6_chars() {
1392        // Test standard 6-character hex string
1393        let result = hex_string_to_color32("a7e47e").unwrap();
1394        let expected = Color32::from_rgb(0xa7, 0xe4, 0x7e);
1395        assert_eq!(result, expected);
1396    }
1397
1398    #[test]
1399    fn test_hex_string_black() {
1400        // Test black color (all zeros)
1401        let result = hex_string_to_color32("000000").unwrap();
1402        let expected = Color32::from_rgb(0x00, 0x00, 0x00);
1403        assert_eq!(result, expected);
1404    }
1405
1406    #[test]
1407    fn test_hex_string_white() {
1408        // Test white color (all ones)
1409        let result = hex_string_to_color32("ffffff").unwrap();
1410        let expected = Color32::from_rgb(0xff, 0xff, 0xff);
1411        assert_eq!(result, expected);
1412    }
1413
1414    #[test]
1415    fn test_hex_string_uppercase() {
1416        // Test uppercase hex characters
1417        let result = hex_string_to_color32("ABCDEF").unwrap();
1418        let expected = Color32::from_rgb(0xab, 0xcd, 0xef);
1419        assert_eq!(result, expected);
1420    }
1421
1422    #[test]
1423    fn test_hex_string_mixed_case() {
1424        // Test mixed case hex characters
1425        let result = hex_string_to_color32("Ab5DeF").unwrap();
1426        let expected = Color32::from_rgb(0xab, 0x5d, 0xef);
1427        assert_eq!(result, expected);
1428    }
1429
1430    #[test]
1431    fn test_hex_string_invalid_length() {
1432        // Test that invalid length returns error
1433        let result = hex_string_to_color32("ab");
1434        assert!(result.is_err());
1435
1436        let result = hex_string_to_color32("abcde");
1437        assert!(result.is_err());
1438
1439        let result = hex_string_to_color32("abcdefgh");
1440        assert!(result.is_err());
1441    }
1442
1443    #[test]
1444    fn test_hex_string_invalid_characters() {
1445        // Test that invalid hex characters return error
1446        let result = hex_string_to_color32("GGGGGG");
1447        assert!(result.is_err());
1448
1449        let result = hex_string_to_color32("12345g");
1450        assert!(result.is_err());
1451
1452        let result = hex_string_to_color32("zzzzzz");
1453        assert!(result.is_err());
1454    }
1455
1456    #[test]
1457    fn test_hex_string_empty() {
1458        // Test empty string
1459        let result = hex_string_to_color32("");
1460        assert!(result.is_err());
1461    }
1462
1463    #[test]
1464    fn test_hex_string_3_chars_doubling() {
1465        // Test specific 3-character doubling behavior
1466        let result = hex_string_to_color32("050").unwrap();
1467        let expected = Color32::from_rgb(0x00, 0x55, 0x00);
1468        assert_eq!(result, expected);
1469    }
1470
1471    #[test]
1472    fn test_hex_string_with_hash_3_chars() {
1473        let result = hex_string_to_color32("#abc").unwrap();
1474        let expected = Color32::from_rgb(0xaa, 0xbb, 0xcc);
1475        assert_eq!(result, expected);
1476    }
1477
1478    #[test]
1479    fn test_hex_string_with_hash_6_chars() {
1480        let result = hex_string_to_color32("#ABCDEF").unwrap();
1481        let expected = Color32::from_rgb(0xab, 0xcd, 0xef);
1482        assert_eq!(result, expected);
1483    }
1484}