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
38static 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#[derive(Clone, Copy, Debug, Deserialize, Display, FromStr, PartialEq, Eq, Sequence, Serialize)]
76pub enum ArrowKeyBindings {
77 Edge,
79
80 Scroll,
82}
83
84#[derive(Clone, Copy, Debug, Deserialize, Display, FromStr, PartialEq, Eq, Sequence, Serialize)]
85pub enum TransitionValue {
86 Previous,
88 Next,
90 Both,
92}
93
94#[derive(Debug, Deserialize, Display, PartialEq, Eq, Sequence, Serialize, Clone, Copy)]
96pub enum PrimaryMouseDrag {
97 #[display("Measure time")]
99 Measure,
100
101 #[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 pub gesture: SurferGesture,
131 pub behavior: SurferBehavior,
132 pub default_time_format: TimeFormat,
134 pub default_variable_name_type: VariableNameType,
135 default_clock_highlight_type: ClockHighlightType,
136 #[serde(deserialize_with = "deserialize_non_negative_f32")]
138 pub snap_distance: f32,
139 pub undo_stack_size: usize,
141 autoreload_files: AutoLoad,
143 autoload_sibling_state_files: AutoLoad,
145 pub wcp: WcpConfig,
147 pub server: SurverConfig,
149 #[serde(deserialize_with = "deserialize_non_negative_f32")]
151 pub animation_time: f32,
152 pub animation_enabled: bool,
154 pub max_url_length: u16,
157 #[serde(deserialize_with = "deserialize_shortcuts")]
159 pub shortcuts: SurferShortcuts,
160 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 show_hierarchy: bool,
195 show_menu: bool,
197 show_toolbar: bool,
199 show_ticks: bool,
201 show_tooltip: bool,
203 show_scope_tooltip: bool,
205 show_overview: bool,
207 show_statusbar: bool,
209 show_variable_indices: bool,
211 show_variable_direction: bool,
213 show_default_timeline: bool,
215 show_empty_scopes: bool,
217 show_hierarchy_icons: bool,
219 parameter_display_location: ParameterDisplayLocation,
221 pub window_height: usize,
223 pub window_width: usize,
225 pub window_x_position: usize,
227 pub window_y_position: usize,
229 align_names_right: bool,
231 hierarchy_style: HierarchyStyle,
233 #[serde(deserialize_with = "deserialize_non_negative_f32")]
235 pub waveforms_text_size: f32,
236 #[serde(deserialize_with = "deserialize_non_negative_f32")]
238 pub waveforms_line_height: f32,
239 #[serde(deserialize_with = "deserialize_non_negative_f32")]
241 pub waveforms_gap: f32,
242 #[serde(deserialize_with = "deserialize_non_negative_f32_vec")]
244 pub waveforms_line_height_multiples: Vec<f32>,
245 #[serde(deserialize_with = "deserialize_non_negative_f32")]
247 pub analog_waveform_multiplier: f32,
248 #[serde(deserialize_with = "deserialize_non_negative_f32")]
250 pub transactions_line_height: f32,
251 #[serde(deserialize_with = "deserialize_non_negative_f32_vec")]
253 pub zoom_factors: Vec<f32>,
254 #[serde(deserialize_with = "deserialize_non_negative_f32")]
256 default_zoom_factor: f32,
257 highlight_focused: bool,
259 move_focus_on_inserted_marker: bool,
261 fill_high_values: bool,
263 use_dinotrace_style: bool,
265 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 pub keep_during_reload: bool,
364 pub file_history_size: usize,
366 arrow_key_bindings: ArrowKeyBindings,
368 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)]
391pub struct SurferGesture {
393 #[serde(deserialize_with = "deserialize_non_negative_f32")]
395 pub size: f32,
396 #[serde(deserialize_with = "deserialize_non_negative_f32")]
398 pub deadzone: f32,
399 #[serde(deserialize_with = "deserialize_non_negative_f32")]
401 pub background_radius: f32,
402 #[serde(deserialize_with = "deserialize_unit_interval_f32")]
404 pub background_gamma: f32,
405 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)]
442pub struct SurferTicks {
444 #[serde(deserialize_with = "deserialize_unit_interval_f32")]
446 pub density: f32,
447 pub style: SurferLineStyle,
449}
450
451#[derive(Debug, Deserialize)]
452pub struct SurferRelationArrow {
453 pub style: SurferLineStyle,
455
456 #[serde(deserialize_with = "deserialize_non_negative_f32")]
458 pub head_angle: f32,
459
460 #[serde(deserialize_with = "deserialize_non_negative_f32")]
462 pub head_length: f32,
463}
464
465#[derive(Debug, Deserialize)]
466pub struct SurferTheme {
467 #[serde(deserialize_with = "deserialize_hex_color")]
469 pub foreground: Color32,
470 #[serde(deserialize_with = "deserialize_hex_color")]
471 pub border_color: Color32,
473 #[serde(deserialize_with = "deserialize_hex_color")]
475 pub alt_text_color: Color32,
476 pub canvas_colors: ThemeColorTriple,
478 pub primary_ui_color: ThemeColorPair,
480 pub secondary_ui_color: ThemeColorPair,
483 pub selected_elements_colors: ThemeColorPair,
485
486 pub accent_info: ThemeColorPair,
487 pub accent_warn: ThemeColorPair,
488 pub accent_error: ThemeColorPair,
489
490 pub cursor: SurferLineStyle,
492
493 pub gesture: SurferLineStyle,
495
496 pub measure: SurferLineStyle,
498
499 pub annotation_rectangle: SurferLineStyle,
501
502 pub annotation_arrow: SurferLineStyle,
504
505 pub clock_highlight_line: SurferLineStyle,
507 #[serde(deserialize_with = "deserialize_hex_color_vec")]
508 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 pub clock_highlight_cycle_colors: Vec<Color32>,
515 pub clock_rising_marker: bool,
517
518 #[serde(deserialize_with = "deserialize_hex_color")]
519 pub variable_default: Color32,
521 #[serde(deserialize_with = "deserialize_hex_color")]
522 pub variable_highimp: Color32,
524 #[serde(deserialize_with = "deserialize_hex_color")]
525 pub variable_undef: Color32,
527 #[serde(deserialize_with = "deserialize_hex_color")]
528 pub variable_dontcare: Color32,
530 #[serde(deserialize_with = "deserialize_hex_color")]
531 pub variable_weak: Color32,
533 #[serde(deserialize_with = "deserialize_hex_color")]
534 pub variable_parameter: Color32,
536 #[serde(deserialize_with = "deserialize_hex_color")]
537 pub transaction_default: Color32,
539 pub relation_arrow: SurferRelationArrow,
541 #[serde(deserialize_with = "deserialize_hex_color")]
542 pub variable_event: Color32,
544
545 #[serde(deserialize_with = "deserialize_unit_interval_f32")]
548 pub waveform_opacity: f32,
549 #[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 #[serde(deserialize_with = "deserialize_non_negative_f32")]
560 pub linewidth: f32,
561
562 #[serde(deserialize_with = "deserialize_non_negative_f32")]
564 pub thick_linewidth: f32,
565
566 #[serde(deserialize_with = "deserialize_non_negative_f32")]
568 pub vector_transition_width: f32,
569
570 pub alt_frequency: usize,
573
574 pub viewport_separator: SurferLineStyle,
576
577 #[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 pub ticks: SurferTicks,
587
588 pub theme_names: Vec<String>,
590
591 #[serde(default)]
593 pub scope_icons: ScopeIcons,
594
595 #[serde(default)]
597 pub variable_icons: VariableIcons,
598}
599
600#[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), task: Color32::from_rgb(0xFF, 0xB7, 0x4D), function: Color32::from_rgb(0xBA, 0x68, 0xC8), begin: Color32::from_rgb(0x81, 0xC7, 0x84), fork: Color32::from_rgb(0xFF, 0x80, 0x80), generate: Color32::from_rgb(0x64, 0xB5, 0xF6), struct_: Color32::from_rgb(0x4D, 0xD0, 0xE1), union: Color32::from_rgb(0x4D, 0xD0, 0xE1), class: Color32::from_rgb(0xF0, 0x62, 0x92), interface: Color32::from_rgb(0xAE, 0xD5, 0x81), package: Color32::from_rgb(0xFF, 0xD5, 0x4F), program: Color32::from_rgb(0xA1, 0x88, 0x7F), vhdl_architecture: Color32::from_rgb(0x4F, 0xC3, 0xF7), vhdl_procedure: Color32::from_rgb(0xFF, 0xB7, 0x4D), vhdl_function: Color32::from_rgb(0xBA, 0x68, 0xC8), vhdl_record: Color32::from_rgb(0x4D, 0xD0, 0xE1), vhdl_process: Color32::from_rgb(0x81, 0xC7, 0x84), vhdl_block: Color32::from_rgb(0x90, 0xA4, 0xAE), vhdl_for_generate: Color32::from_rgb(0x64, 0xB5, 0xF6), vhdl_if_generate: Color32::from_rgb(0x64, 0xB5, 0xF6), vhdl_generate: Color32::from_rgb(0x64, 0xB5, 0xF6), vhdl_package: Color32::from_rgb(0xFF, 0xD5, 0x4F), ghw_generic: Color32::from_rgb(0xB0, 0xBE, 0xC5), vhdl_array: Color32::from_rgb(0xCE, 0x93, 0xD8), clocking: Color32::from_rgb(0xF0, 0x62, 0x92), sv_array: Color32::from_rgb(0xCE, 0x93, 0xD8), unknown: Color32::from_rgb(0x9E, 0x9E, 0x9E), }
691 }
692}
693
694#[derive(Clone, Debug, Deserialize)]
697#[serde(default)]
698pub struct ScopeIcons {
699 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 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 #[serde(default)]
731 pub colors: ScopeIconColors,
732}
733
734impl Default for ScopeIcons {
735 fn default() -> Self {
736 use egui_remixicon::icons;
737 Self {
738 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_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 #[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#[derive(Clone, Debug, Deserialize)]
821#[serde(default)]
822pub struct VariableIconColors {
823 #[serde(deserialize_with = "deserialize_hex_color")]
825 pub wire: Color32,
826 #[serde(deserialize_with = "deserialize_hex_color")]
828 pub bus: Color32,
829 #[serde(deserialize_with = "deserialize_hex_color")]
831 pub string: Color32,
832 #[serde(deserialize_with = "deserialize_hex_color")]
834 pub event: Color32,
835 #[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), bus: Color32::from_rgb(0x64, 0xB5, 0xF6), string: Color32::from_rgb(0xFF, 0xB7, 0x4D), event: Color32::from_rgb(0xF0, 0x62, 0x92), other: Color32::from_rgb(0xBA, 0x68, 0xC8), }
849 }
850}
851
852#[derive(Clone, Debug, Deserialize)]
855#[serde(default)]
856pub struct VariableIcons {
857 pub wire: String,
859 pub bus: String,
861 pub string: String,
863 pub event: String,
865 pub other: String,
867 #[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 #[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 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 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 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 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 let local_config_dirs = find_local_configs();
1021
1022 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 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 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 pub autostart: bool,
1087 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 config = config.add_source(File::from(old_config_path).required(false));
1125
1126 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")) };
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#[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()) .rev() .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 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 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 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 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 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 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 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 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 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 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}