Skip to main content

libsurfer/
config.rs

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