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 eyre::Report;
11use eyre::{Context, Result};
12use serde::de;
13use serde::{Deserialize, Deserializer, Serialize};
14use std::collections::HashMap;
15#[cfg(not(target_arch = "wasm32"))]
16use std::path::{Path, PathBuf};
17
18use crate::hierarchy::HierarchyStyle;
19use crate::mousegestures::GestureZones;
20use crate::time::TimeFormat;
21use crate::{clock_highlighting::ClockHighlightType, variable_name_type::VariableNameType};
22
23/// Select the function of the arrow keys
24#[derive(Clone, Copy, Debug, Deserialize, Display, FromStr, PartialEq, Eq, Sequence, Serialize)]
25pub enum ArrowKeyBindings {
26    /// The left/right arrow keys step to the next edge
27    Edge,
28
29    /// The left/right arrow keys scroll the viewport left/right
30    Scroll,
31}
32
33/// Select the function when dragging with primary mouse button
34#[derive(Debug, Deserialize, Display, PartialEq, Eq, Sequence, Serialize, Clone, Copy)]
35pub enum PrimaryMouseDrag {
36    /// The left/right arrow keys step to the next edge
37    #[display("Measure time")]
38    Measure,
39
40    /// The left/right arrow keys scroll the viewport left/right
41    #[display("Move cursor")]
42    Cursor,
43}
44
45#[derive(Debug, Deserialize, Display, PartialEq, Eq, Sequence, Serialize, Clone, Copy)]
46pub enum AutoLoad {
47    Always,
48    Never,
49    Ask,
50}
51
52impl AutoLoad {
53    pub fn from_bool(auto_load: bool) -> Self {
54        if auto_load {
55            AutoLoad::Always
56        } else {
57            AutoLoad::Never
58        }
59    }
60}
61
62#[derive(Debug, Deserialize)]
63pub struct SurferConfig {
64    pub layout: SurferLayout,
65    #[serde(deserialize_with = "deserialize_theme")]
66    pub theme: SurferTheme,
67    /// Mouse gesture configurations. Color and linewidth are configured in the theme using [SurferTheme::gesture].
68    pub gesture: SurferGesture,
69    pub behavior: SurferBehavior,
70    /// Time stamp format
71    pub default_time_format: TimeFormat,
72    pub default_variable_name_type: VariableNameType,
73    default_clock_highlight_type: ClockHighlightType,
74    /// Distance in pixels for cursor snap
75    pub snap_distance: f32,
76    /// Maximum size of the undo stack
77    pub undo_stack_size: usize,
78    /// Reload changed waves
79    autoreload_files: AutoLoad,
80    /// Load state file
81    autoload_sibling_state_files: AutoLoad,
82    /// WCP Configuration
83    pub wcp: WcpConfig,
84}
85
86impl SurferConfig {
87    pub fn default_clock_highlight_type(&self) -> ClockHighlightType {
88        self.default_clock_highlight_type
89    }
90
91    pub fn autoload_sibling_state_files(&self) -> AutoLoad {
92        self.autoload_sibling_state_files
93    }
94
95    pub fn autoreload_files(&self) -> AutoLoad {
96        self.autoreload_files
97    }
98}
99
100#[derive(Debug, Deserialize)]
101pub struct SurferLayout {
102    /// Flag to show/hide the hierarchy view
103    show_hierarchy: bool,
104    /// Flag to show/hide the menu
105    show_menu: bool,
106    /// Flag to show/hide toolbar
107    show_toolbar: bool,
108    /// Flag to show/hide tick lines
109    show_ticks: bool,
110    /// Flag to show/hide tooltip for variables
111    show_tooltip: bool,
112    /// Flag to show/hide tooltip for scopes
113    show_scope_tooltip: bool,
114    /// Flag to show/hide the overview
115    show_overview: bool,
116    /// Flag to show/hide the statusbar
117    show_statusbar: bool,
118    /// Flag to show/hide the indices of variables in the variable list
119    show_variable_indices: bool,
120    /// Flag to show/hide the variable direction icon
121    show_variable_direction: bool,
122    /// Flag to show/hide a default timeline
123    show_default_timeline: bool,
124    /// Flag to show/hide empty scopes
125    show_empty_scopes: bool,
126    /// Flag to show parameters in scope view
127    show_parameters_in_scopes: bool,
128    /// Initial window height
129    pub window_height: usize,
130    /// Initial window width
131    pub window_width: usize,
132    /// Align variable names right
133    align_names_right: bool,
134    /// Set style of hierarchy
135    hierarchy_style: HierarchyStyle,
136    /// Text size in points for values in waves
137    pub waveforms_text_size: f32,
138    /// Line height in points for waves
139    pub waveforms_line_height: f32,
140    /// Line height multiples for higher variables
141    pub waveforms_line_height_multiples: Vec<f32>,
142    /// Line height in points for transaction streams
143    pub transactions_line_height: f32,
144    /// UI zoom factors
145    pub zoom_factors: Vec<f32>,
146    /// Default UI zoom factor
147    pub default_zoom_factor: f32,
148    #[serde(default)]
149    /// Highlight the waveform of the focused item?
150    highlight_focused: bool,
151    /// Move the focus to the newly inserted marker?
152    move_focus_on_inserted_marker: bool,
153    /// Fill high values in boolean waveforms
154    #[serde(default = "default_true")]
155    fill_high_values: bool,
156}
157
158fn default_true() -> bool {
159    true
160}
161
162impl SurferLayout {
163    pub fn show_hierarchy(&self) -> bool {
164        self.show_hierarchy
165    }
166    pub fn show_menu(&self) -> bool {
167        self.show_menu
168    }
169    pub fn show_ticks(&self) -> bool {
170        self.show_ticks
171    }
172    pub fn show_tooltip(&self) -> bool {
173        self.show_tooltip
174    }
175    pub fn show_scope_tooltip(&self) -> bool {
176        self.show_scope_tooltip
177    }
178    pub fn show_default_timeline(&self) -> bool {
179        self.show_default_timeline
180    }
181    pub fn show_toolbar(&self) -> bool {
182        self.show_toolbar
183    }
184    pub fn show_overview(&self) -> bool {
185        self.show_overview
186    }
187    pub fn show_statusbar(&self) -> bool {
188        self.show_statusbar
189    }
190    pub fn align_names_right(&self) -> bool {
191        self.align_names_right
192    }
193    pub fn show_variable_indices(&self) -> bool {
194        self.show_variable_indices
195    }
196    pub fn show_variable_direction(&self) -> bool {
197        self.show_variable_direction
198    }
199    pub fn default_zoom_factor(&self) -> f32 {
200        self.default_zoom_factor
201    }
202    pub fn show_empty_scopes(&self) -> bool {
203        self.show_empty_scopes
204    }
205    pub fn show_parameters_in_scopes(&self) -> bool {
206        self.show_parameters_in_scopes
207    }
208    pub fn highlight_focused(&self) -> bool {
209        self.highlight_focused
210    }
211    pub fn move_focus_on_inserted_marker(&self) -> bool {
212        self.move_focus_on_inserted_marker
213    }
214    pub fn fill_high_values(&self) -> bool {
215        self.fill_high_values
216    }
217    pub fn hierarchy_style(&self) -> HierarchyStyle {
218        self.hierarchy_style
219    }
220}
221
222#[derive(Debug, Deserialize)]
223pub struct SurferBehavior {
224    /// Keep or remove variables if unavailable during reload
225    pub keep_during_reload: bool,
226    /// Select the functionality bound to the arrow keys
227    pub arrow_key_bindings: ArrowKeyBindings,
228    /// Whether dragging with primary mouse button will measure time or move cursor
229    /// (press shift for the other)
230    primary_button_drag_behavior: PrimaryMouseDrag,
231}
232
233impl SurferBehavior {
234    pub fn primary_button_drag_behavior(&self) -> PrimaryMouseDrag {
235        self.primary_button_drag_behavior
236    }
237
238    pub fn arrow_key_bindings(&self) -> ArrowKeyBindings {
239        self.arrow_key_bindings
240    }
241}
242
243#[derive(Debug, Deserialize)]
244/// Mouse gesture configurations. Color and linewidth are configured in the theme using [SurferTheme::gesture].
245pub struct SurferGesture {
246    /// Size of the overlay help
247    pub size: f32,
248    /// (Squared) minimum distance to move to remove the overlay help and perform gesture
249    pub deadzone: f32,
250    /// Circle radius for background as a factor of size/2
251    pub background_radius: f32,
252    /// Gamma factor for background circle, between 0 (opaque) and 1 (transparent)
253    pub background_gamma: f32,
254    /// Mapping between the eight directions and actions
255    pub mapping: GestureZones,
256}
257
258#[derive(Debug, Deserialize)]
259pub struct SurferLineStyle {
260    #[serde(deserialize_with = "deserialize_hex_color")]
261    pub color: Color32,
262    pub width: f32,
263}
264
265#[derive(Debug, Deserialize)]
266/// Tick mark configuration
267pub struct SurferTicks {
268    /// 0 to 1, where 1 means as many ticks that can fit without overlap
269    pub density: f32,
270    /// Line style to use for ticks
271    pub style: SurferLineStyle,
272}
273
274#[derive(Debug, Deserialize)]
275pub struct SurferRelationArrow {
276    /// Arrow line style
277    pub style: SurferLineStyle,
278
279    /// Arrowhead angle in degrees
280    pub head_angle: f32,
281
282    /// Arrowhead length
283    pub head_length: f32,
284}
285
286#[derive(Debug, Deserialize)]
287pub struct SurferTheme {
288    /// Color used for text across the UI
289    #[serde(deserialize_with = "deserialize_hex_color")]
290    pub foreground: Color32,
291    #[serde(deserialize_with = "deserialize_hex_color")]
292    /// Color of borders between UI elements
293    pub border_color: Color32,
294    /// Color used for text across the markers
295    #[serde(deserialize_with = "deserialize_hex_color")]
296    pub alt_text_color: Color32,
297    /// Colors used for the background and text of the wave view
298    pub canvas_colors: ThemeColorTriple,
299    /// Colors used for most UI elements not on the variable canvas
300    pub primary_ui_color: ThemeColorPair,
301    /// Colors used for the variable and value list, as well as secondary elements
302    /// like text fields
303    pub secondary_ui_color: ThemeColorPair,
304    /// Color used for selected ui elements such as the currently selected hierarchy
305    pub selected_elements_colors: ThemeColorPair,
306
307    pub accent_info: ThemeColorPair,
308    pub accent_warn: ThemeColorPair,
309    pub accent_error: ThemeColorPair,
310
311    ///  Line style for cursor
312    pub cursor: SurferLineStyle,
313
314    /// Line style for mouse gesture lines
315    pub gesture: SurferLineStyle,
316
317    /// Line style for measurement lines
318    pub measure: SurferLineStyle,
319
320    ///  Line style for clock highlight lines
321    pub clock_highlight_line: SurferLineStyle,
322    #[serde(deserialize_with = "deserialize_hex_color")]
323    pub clock_highlight_cycle: Color32,
324    /// Draw arrows on rising clock edges
325    pub clock_rising_marker: bool,
326
327    #[serde(deserialize_with = "deserialize_hex_color")]
328    /// Default variable color
329    pub variable_default: Color32,
330    #[serde(deserialize_with = "deserialize_hex_color")]
331    /// Color used for high-impedance variables
332    pub variable_highimp: Color32,
333    #[serde(deserialize_with = "deserialize_hex_color")]
334    /// Color used for undefined variables
335    pub variable_undef: Color32,
336    #[serde(deserialize_with = "deserialize_hex_color")]
337    /// Color used for don't-care variables
338    pub variable_dontcare: Color32,
339    #[serde(deserialize_with = "deserialize_hex_color")]
340    /// Color used for weak variables
341    pub variable_weak: Color32,
342    #[serde(deserialize_with = "deserialize_hex_color")]
343    /// Color used for constant variables (parameters)
344    pub variable_parameter: Color32,
345    #[serde(deserialize_with = "deserialize_hex_color")]
346    /// Default transaction color
347    pub transaction_default: Color32,
348    // Relation arrows of transactions
349    pub relation_arrow: SurferRelationArrow,
350
351    /// Opacity with which variable backgrounds are drawn. 0 is fully transparent and 1 is fully
352    /// opaque.
353    pub waveform_opacity: f32,
354    /// Opacity of variable backgrounds for wide signals (signals with more than one bit)
355    #[serde(default)]
356    pub wide_opacity: f32,
357
358    #[serde(default = "default_colors", deserialize_with = "deserialize_color_map")]
359    pub colors: HashMap<String, Color32>,
360    #[serde(deserialize_with = "deserialize_hex_color")]
361    pub highlight_background: Color32,
362
363    /// Variable line width
364    pub linewidth: f32,
365
366    /// Vector transition max width
367    pub vector_transition_width: f32,
368
369    /// Number of lines using standard background before changing to
370    /// alternate background and so on, set to zero to disable
371    pub alt_frequency: usize,
372
373    /// Viewport separator line
374    pub viewport_separator: SurferLineStyle,
375
376    // Drag hint and threshold parameters
377    #[serde(deserialize_with = "deserialize_hex_color")]
378    pub drag_hint_color: Color32,
379    pub drag_hint_width: f32,
380    pub drag_threshold: f32,
381
382    /// Tick information
383    pub ticks: SurferTicks,
384
385    /// List of theme names
386    #[serde(default = "Vec::new")]
387    pub theme_names: Vec<String>,
388}
389
390fn get_luminance(color: &Color32) -> f32 {
391    let rg = if color.r() < 10 {
392        color.r() as f32 / 3294.0
393    } else {
394        (color.r() as f32 / 269.0 + 0.0513).powf(2.4)
395    };
396    let gg = if color.g() < 10 {
397        color.g() as f32 / 3294.0
398    } else {
399        (color.g() as f32 / 269.0 + 0.0513).powf(2.4)
400    };
401    let bg = if color.b() < 10 {
402        color.b() as f32 / 3294.0
403    } else {
404        (color.b() as f32 / 269.0 + 0.0513).powf(2.4)
405    };
406    0.2126 * rg + 0.7152 * gg + 0.0722 * bg
407}
408
409impl SurferTheme {
410    pub fn get_color(&self, color: &str) -> Option<&Color32> {
411        self.colors.get(color)
412    }
413
414    pub fn get_best_text_color(&self, backgroundcolor: &Color32) -> &Color32 {
415        // Based on https://ux.stackexchange.com/questions/82056/how-to-measure-the-contrast-between-any-given-color-and-white
416
417        // Compute luminance
418        let l_foreground = get_luminance(&self.foreground);
419        let l_alt_text_color = get_luminance(&self.alt_text_color);
420        let l_background = get_luminance(backgroundcolor);
421
422        // Compute contrast ratio
423        let mut cr_foreground = (l_foreground + 0.05) / (l_background + 0.05);
424        cr_foreground = cr_foreground.max(1. / cr_foreground);
425        let mut cr_alt_text_color = (l_alt_text_color + 0.05) / (l_background + 0.05);
426        cr_alt_text_color = cr_alt_text_color.max(1. / cr_alt_text_color);
427
428        // Return color with highest contrast
429        if cr_foreground > cr_alt_text_color {
430            &self.foreground
431        } else {
432            &self.alt_text_color
433        }
434    }
435
436    fn generate_defaults(
437        theme_name: &Option<String>,
438    ) -> (ConfigBuilder<DefaultState>, Vec<String>) {
439        let default_theme = String::from(include_str!("../../default_theme.toml"));
440
441        let mut theme = Config::builder().add_source(config::File::from_str(
442            &default_theme,
443            config::FileFormat::Toml,
444        ));
445
446        let theme_names = all_theme_names();
447
448        let override_theme = match theme_name.clone().unwrap_or_default().as_str() {
449            "dark+" => include_str!("../../themes/dark+.toml"),
450            "dark-high-contrast" => include_str!("../../themes/dark-high-contrast.toml"),
451            "ibm" => include_str!("../../themes/ibm.toml"),
452            "light+" => include_str!("../../themes/light+.toml"),
453            "light-high-contrast" => include_str!("../../themes/light-high-contrast.toml"),
454            "okabe/ito" => include_str!("../../themes/okabe-ito.toml"),
455            "solarized" => include_str!("../../themes/solarized.toml"),
456            _ => "",
457        }
458        .to_string();
459
460        theme = theme.add_source(config::File::from_str(
461            &override_theme,
462            config::FileFormat::Toml,
463        ));
464        (theme, theme_names)
465    }
466
467    #[cfg(target_arch = "wasm32")]
468    pub fn new(theme_name: Option<String>) -> Result<Self> {
469        use eyre::anyhow;
470
471        let (theme, _) = Self::generate_defaults(&theme_name);
472
473        let theme = theme.set_override("theme_names", all_theme_names())?;
474
475        theme
476            .build()?
477            .try_deserialize()
478            .map_err(|e| anyhow!("Failed to parse config {e}"))
479    }
480
481    #[cfg(not(target_arch = "wasm32"))]
482    pub fn new(theme_name: Option<String>) -> eyre::Result<Self> {
483        use std::fs::ReadDir;
484
485        use eyre::anyhow;
486
487        let (mut theme, mut theme_names) = Self::generate_defaults(&theme_name);
488
489        let mut add_themes_from_dir = |dir: ReadDir| {
490            for theme in dir.flatten() {
491                if let Ok(theme_path) = theme.file_name().into_string() {
492                    if theme_path.ends_with(".toml") {
493                        let fname = theme_path.strip_suffix(".toml").unwrap().to_string();
494                        if !fname.is_empty() && !theme_names.contains(&fname) {
495                            theme_names.push(fname);
496                        }
497                    }
498                }
499            }
500        };
501
502        // read themes from config directory
503        if let Some(proj_dirs) = ProjectDirs::from("org", "surfer-project", "surfer") {
504            let config_themes_dir = proj_dirs.config_dir().join("themes");
505            if let Ok(config_themes_dir) = std::fs::read_dir(config_themes_dir) {
506                add_themes_from_dir(config_themes_dir);
507            }
508        }
509
510        // Read themes from local directories.
511        let local_config_dirs = find_local_configs();
512
513        // Add any existing themes from most top-level to most local. This allows overwriting of
514        // higher-level theme settings with a local `.surfer` directory.
515        local_config_dirs
516            .iter()
517            .filter_map(|p| std::fs::read_dir(p.join("themes")).ok())
518            .for_each(add_themes_from_dir);
519
520        if theme_name
521            .clone()
522            .is_some_and(|theme_name| !theme_name.is_empty())
523        {
524            let theme_path = Path::new("themes").join(theme_name.unwrap() + ".toml");
525
526            // First filter out all the existing local themes and add them in the aforementioned
527            // order.
528            let local_themes: Vec<PathBuf> = local_config_dirs
529                .iter()
530                .map(|p| p.join(&theme_path))
531                .filter(|p| p.exists())
532                .collect();
533            if !local_themes.is_empty() {
534                theme = local_themes
535                    .into_iter()
536                    .fold(theme, |t, p| t.add_source(File::from(p).required(false)));
537            } else {
538                // If no local themes exist, search in the config directory.
539                if let Some(proj_dirs) = ProjectDirs::from("org", "surfer-project", "surfer") {
540                    let config_theme_path = proj_dirs.config_dir().join(theme_path);
541                    if config_theme_path.exists() {
542                        theme = theme.add_source(File::from(config_theme_path).required(false));
543                    }
544                }
545            }
546        }
547
548        let theme = theme.set_override("theme_names", theme_names)?;
549
550        theme
551            .build()?
552            .try_deserialize()
553            .map_err(|e| anyhow!("Failed to parse theme {e}"))
554    }
555}
556
557#[derive(Debug, Deserialize)]
558pub struct ThemeColorPair {
559    #[serde(deserialize_with = "deserialize_hex_color")]
560    pub foreground: Color32,
561    #[serde(deserialize_with = "deserialize_hex_color")]
562    pub background: Color32,
563}
564
565#[derive(Debug, Deserialize)]
566pub struct ThemeColorTriple {
567    #[serde(deserialize_with = "deserialize_hex_color")]
568    pub foreground: Color32,
569    #[serde(deserialize_with = "deserialize_hex_color")]
570    pub background: Color32,
571    #[serde(deserialize_with = "deserialize_hex_color")]
572    pub alt_background: Color32,
573}
574
575#[derive(Debug, Deserialize)]
576pub struct WcpConfig {
577    /// Controls if a server is started after Surfer is launched
578    pub autostart: bool,
579    /// Address to bind to (address:port)
580    pub address: String,
581}
582
583fn default_colors() -> HashMap<String, Color32> {
584    vec![
585        ("Green", "a7e47e"),
586        ("Red", "c52e2e"),
587        ("Yellow", "f3d54a"),
588        ("Blue", "81a2be"),
589        ("Purple", "b294bb"),
590        ("Aqua", "8abeb7"),
591        ("Gray", "c5c8c6"),
592    ]
593    .iter()
594    .map(|(name, hexcode)| {
595        (
596            name.to_string(),
597            hex_string_to_color32(hexcode.to_string()).unwrap(),
598        )
599    })
600    .collect()
601}
602
603impl SurferConfig {
604    #[cfg(target_arch = "wasm32")]
605    pub fn new(_force_default_config: bool) -> Result<Self> {
606        Self::new_from_toml(&include_str!("../../default_config.toml"))
607    }
608
609    #[cfg(not(target_arch = "wasm32"))]
610    pub fn new(force_default_config: bool) -> eyre::Result<Self> {
611        use eyre::anyhow;
612        use log::warn;
613
614        let default_config = String::from(include_str!("../../default_config.toml"));
615
616        let mut config = Config::builder().add_source(config::File::from_str(
617            &default_config,
618            config::FileFormat::Toml,
619        ));
620
621        let config = if !force_default_config {
622            if let Some(proj_dirs) = ProjectDirs::from("org", "surfer-project", "surfer") {
623                let config_file = proj_dirs.config_dir().join("config.toml");
624                config = config.add_source(File::from(config_file).required(false));
625            }
626
627            if Path::new("surfer.toml").exists() {
628                warn!("Configuration in 'surfer.toml' is deprecated. Please move your configuration to '.surfer/config.toml'.");
629            }
630
631            // `surfer.toml` will not be searched for upward, as it is deprecated.
632            config = config.add_source(File::from(Path::new("surfer.toml")).required(false));
633
634            // Add configs from most top-level to most local. This allows overwriting of
635            // higher-level settings with a local `.surfer` directory.
636            find_local_configs()
637                .into_iter()
638                .fold(config, |c, p| {
639                    c.add_source(File::from(p.join("config.toml")).required(false))
640                })
641                .add_source(Environment::with_prefix("surfer")) // Add environment finally
642        } else {
643            config
644        };
645
646        config
647            .build()?
648            .try_deserialize()
649            .map_err(|e| anyhow!("Failed to parse config {e}"))
650    }
651
652    pub fn new_from_toml(config: &str) -> Result<Self> {
653        Ok(toml::from_str(config)?)
654    }
655}
656
657impl Default for SurferConfig {
658    fn default() -> Self {
659        Self::new(false).expect("Failed to load default config")
660    }
661}
662
663fn hex_string_to_color32(mut str: String) -> Result<Color32> {
664    let mut hex_str = String::new();
665    if str.len() == 3 {
666        for c in str.chars() {
667            hex_str.push(c);
668            hex_str.push(c);
669        }
670        str = hex_str;
671    }
672    if str.len() == 6 {
673        let r = u8::from_str_radix(&str[0..2], 16)
674            .with_context(|| format!("'{str}' is not a valid RGB hex color"))?;
675        let g = u8::from_str_radix(&str[2..4], 16)
676            .with_context(|| format!("'{str}' is not a valid RGB hex color"))?;
677        let b = u8::from_str_radix(&str[4..6], 16)
678            .with_context(|| format!("'{str}' is not a valid RGB hex color"))?;
679        Ok(Color32::from_rgb(r, g, b))
680    } else {
681        eyre::Result::Err(Report::msg(format!("'{str}' is not a valid RGB hex color")))
682    }
683}
684
685fn all_theme_names() -> Vec<String> {
686    vec![
687        "dark+".to_string(),
688        "dark-high-contrast".to_string(),
689        "ibm".to_string(),
690        "light+".to_string(),
691        "light-high-contrast".to_string(),
692        "okabe/ito".to_string(),
693        "solarized".to_string(),
694    ]
695}
696
697fn deserialize_hex_color<'de, D>(deserializer: D) -> Result<Color32, D::Error>
698where
699    D: Deserializer<'de>,
700{
701    let buf = String::deserialize(deserializer)?;
702    hex_string_to_color32(buf).map_err(de::Error::custom)
703}
704
705fn deserialize_color_map<'de, D>(deserializer: D) -> Result<HashMap<String, Color32>, D::Error>
706where
707    D: Deserializer<'de>,
708{
709    #[derive(Deserialize)]
710    struct Wrapper(#[serde(deserialize_with = "deserialize_hex_color")] Color32);
711
712    let v = HashMap::<String, Wrapper>::deserialize(deserializer)?;
713    Ok(v.into_iter().map(|(k, Wrapper(v))| (k, v)).collect())
714}
715
716fn deserialize_theme<'de, D>(deserializer: D) -> Result<SurferTheme, D::Error>
717where
718    D: Deserializer<'de>,
719{
720    let buf = String::deserialize(deserializer)?;
721    SurferTheme::new(Some(buf)).map_err(de::Error::custom)
722}
723
724/// Searches for `.surfer` directories upward from the current location until it reaches root.
725/// Returns an empty vector in case the search fails in any way. If any `.surfer` directories
726/// are found, they will be returned in a `Vec<PathBuf>` in a pre-order of most top-level to most
727/// local. All plain files are ignored.
728#[cfg(not(target_arch = "wasm32"))]
729fn find_local_configs() -> Vec<PathBuf> {
730    use crate::util::search_upward;
731    match std::env::current_dir() {
732        Ok(dir) => search_upward(dir, "/", ".surfer")
733            .into_iter()
734            .filter(|p| p.is_dir()) // Only keep directories and ignore plain files.
735            .rev() // Reverse for pre-order traversal of directories.
736            .collect(),
737        Err(_) => vec![],
738    }
739}