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#[derive(Clone, Copy, Debug, Deserialize, Display, FromStr, PartialEq, Eq, Sequence, Serialize)]
25pub enum ArrowKeyBindings {
26 Edge,
28
29 Scroll,
31}
32
33#[derive(Debug, Deserialize, Display, PartialEq, Eq, Sequence, Serialize, Clone, Copy)]
35pub enum PrimaryMouseDrag {
36 #[display("Measure time")]
38 Measure,
39
40 #[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 pub gesture: SurferGesture,
69 pub behavior: SurferBehavior,
70 pub default_time_format: TimeFormat,
72 pub default_variable_name_type: VariableNameType,
73 default_clock_highlight_type: ClockHighlightType,
74 pub snap_distance: f32,
76 pub undo_stack_size: usize,
78 autoreload_files: AutoLoad,
80 autoload_sibling_state_files: AutoLoad,
82 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 show_hierarchy: bool,
104 show_menu: bool,
106 show_toolbar: bool,
108 show_ticks: bool,
110 show_tooltip: bool,
112 show_scope_tooltip: bool,
114 show_overview: bool,
116 show_statusbar: bool,
118 show_variable_indices: bool,
120 show_variable_direction: bool,
122 show_default_timeline: bool,
124 show_empty_scopes: bool,
126 show_parameters_in_scopes: bool,
128 pub window_height: usize,
130 pub window_width: usize,
132 align_names_right: bool,
134 hierarchy_style: HierarchyStyle,
136 pub waveforms_text_size: f32,
138 pub waveforms_line_height: f32,
140 pub waveforms_line_height_multiples: Vec<f32>,
142 pub transactions_line_height: f32,
144 pub zoom_factors: Vec<f32>,
146 pub default_zoom_factor: f32,
148 #[serde(default)]
149 highlight_focused: bool,
151 move_focus_on_inserted_marker: bool,
153 #[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 pub keep_during_reload: bool,
226 pub arrow_key_bindings: ArrowKeyBindings,
228 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)]
244pub struct SurferGesture {
246 pub size: f32,
248 pub deadzone: f32,
250 pub background_radius: f32,
252 pub background_gamma: f32,
254 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)]
266pub struct SurferTicks {
268 pub density: f32,
270 pub style: SurferLineStyle,
272}
273
274#[derive(Debug, Deserialize)]
275pub struct SurferRelationArrow {
276 pub style: SurferLineStyle,
278
279 pub head_angle: f32,
281
282 pub head_length: f32,
284}
285
286#[derive(Debug, Deserialize)]
287pub struct SurferTheme {
288 #[serde(deserialize_with = "deserialize_hex_color")]
290 pub foreground: Color32,
291 #[serde(deserialize_with = "deserialize_hex_color")]
292 pub border_color: Color32,
294 #[serde(deserialize_with = "deserialize_hex_color")]
296 pub alt_text_color: Color32,
297 pub canvas_colors: ThemeColorTriple,
299 pub primary_ui_color: ThemeColorPair,
301 pub secondary_ui_color: ThemeColorPair,
304 pub selected_elements_colors: ThemeColorPair,
306
307 pub accent_info: ThemeColorPair,
308 pub accent_warn: ThemeColorPair,
309 pub accent_error: ThemeColorPair,
310
311 pub cursor: SurferLineStyle,
313
314 pub gesture: SurferLineStyle,
316
317 pub measure: SurferLineStyle,
319
320 pub clock_highlight_line: SurferLineStyle,
322 #[serde(deserialize_with = "deserialize_hex_color")]
323 pub clock_highlight_cycle: Color32,
324 pub clock_rising_marker: bool,
326
327 #[serde(deserialize_with = "deserialize_hex_color")]
328 pub variable_default: Color32,
330 #[serde(deserialize_with = "deserialize_hex_color")]
331 pub variable_highimp: Color32,
333 #[serde(deserialize_with = "deserialize_hex_color")]
334 pub variable_undef: Color32,
336 #[serde(deserialize_with = "deserialize_hex_color")]
337 pub variable_dontcare: Color32,
339 #[serde(deserialize_with = "deserialize_hex_color")]
340 pub variable_weak: Color32,
342 #[serde(deserialize_with = "deserialize_hex_color")]
343 pub variable_parameter: Color32,
345 #[serde(deserialize_with = "deserialize_hex_color")]
346 pub transaction_default: Color32,
348 pub relation_arrow: SurferRelationArrow,
350
351 pub waveform_opacity: f32,
354 #[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 pub linewidth: f32,
365
366 pub vector_transition_width: f32,
368
369 pub alt_frequency: usize,
372
373 pub viewport_separator: SurferLineStyle,
375
376 #[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 pub ticks: SurferTicks,
384
385 #[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 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 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 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 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 let local_config_dirs = find_local_configs();
512
513 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 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 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 pub autostart: bool,
579 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 config = config.add_source(File::from(Path::new("surfer.toml")).required(false));
633
634 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")) } 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#[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()) .rev() .collect(),
737 Err(_) => vec![],
738 }
739}