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