1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PresentarConfig {
11 #[serde(default = "default_refresh_ms")]
13 pub refresh_ms: u32,
14
15 #[serde(default)]
17 pub layout: LayoutConfig,
18
19 #[serde(default)]
21 pub panels: PanelConfigs,
22
23 #[serde(default)]
25 pub keybindings: KeybindingConfig,
26
27 #[serde(default)]
29 pub theme: ThemeConfig,
30}
31
32impl Default for PresentarConfig {
33 fn default() -> Self {
34 Self {
35 refresh_ms: default_refresh_ms(),
36 layout: LayoutConfig::default(),
37 panels: PanelConfigs::default(),
38 keybindings: KeybindingConfig::default(),
39 theme: ThemeConfig::default(),
40 }
41 }
42}
43
44fn default_refresh_ms() -> u32 {
45 1000
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct LayoutConfig {
51 #[serde(default = "default_true")]
53 pub snap_to_grid: bool,
54
55 #[serde(default = "default_grid_size")]
57 pub grid_size: u8,
58
59 #[serde(default = "default_min_panel_width")]
61 pub min_panel_width: u16,
62
63 #[serde(default = "default_min_panel_height")]
65 pub min_panel_height: u16,
66
67 #[serde(default = "default_top_height")]
69 pub top_height: f32,
70
71 #[serde(default = "default_bottom_height")]
73 pub bottom_height: f32,
74
75 #[serde(default)]
77 pub border_style: BorderStyle,
78
79 #[serde(default = "default_padding")]
81 pub padding: u8,
82}
83
84impl Default for LayoutConfig {
85 fn default() -> Self {
86 Self {
87 snap_to_grid: true,
88 grid_size: 4,
89 min_panel_width: 30,
90 min_panel_height: 6,
91 top_height: 0.45,
92 bottom_height: 0.55,
93 border_style: BorderStyle::Rounded,
94 padding: 1,
95 }
96 }
97}
98
99fn default_true() -> bool {
100 true
101}
102fn default_grid_size() -> u8 {
103 4
104}
105fn default_min_panel_width() -> u16 {
106 30
107}
108fn default_min_panel_height() -> u16 {
109 6
110}
111fn default_top_height() -> f32 {
112 0.45
113}
114fn default_bottom_height() -> f32 {
115 0.55
116}
117fn default_padding() -> u8 {
118 1
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
123#[serde(rename_all = "lowercase")]
124pub enum BorderStyle {
125 #[default]
127 Rounded,
128 Sharp,
130 Double,
132 None,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, Default)]
138pub struct PanelConfigs {
139 #[serde(default)]
141 pub cpu: PanelConfig,
142 #[serde(default)]
144 pub memory: PanelConfig,
145 #[serde(default)]
147 pub disk: PanelConfig,
148 #[serde(default)]
150 pub network: PanelConfig,
151 #[serde(default)]
153 pub process: ProcessPanelConfig,
154 #[serde(default)]
156 pub gpu: PanelConfig,
157 #[serde(default)]
159 pub battery: PanelConfig,
160 #[serde(default)]
162 pub sensors: PanelConfig,
163 #[serde(default)]
165 pub psi: PanelConfig,
166 #[serde(default)]
168 pub connections: PanelConfig,
169 #[serde(default)]
171 pub files: PanelConfig,
172}
173
174impl PanelConfigs {
175 pub fn iter_enabled(&self) -> Vec<(PanelType, bool)> {
177 vec![
178 (PanelType::Cpu, self.cpu.enabled),
179 (PanelType::Memory, self.memory.enabled),
180 (PanelType::Disk, self.disk.enabled),
181 (PanelType::Network, self.network.enabled),
182 (PanelType::Process, self.process.enabled),
183 (PanelType::Gpu, self.gpu.enabled),
184 (PanelType::Battery, self.battery.enabled),
185 (PanelType::Sensors, self.sensors.enabled),
186 (PanelType::Psi, self.psi.enabled),
187 (PanelType::Connections, self.connections.enabled),
188 (PanelType::Files, self.files.enabled),
189 ]
190 }
191
192 pub fn set_enabled(&mut self, panel: PanelType, enabled: bool) {
194 match panel {
195 PanelType::Cpu => self.cpu.enabled = enabled,
196 PanelType::Memory => self.memory.enabled = enabled,
197 PanelType::Disk => self.disk.enabled = enabled,
198 PanelType::Network => self.network.enabled = enabled,
199 PanelType::Process => self.process.enabled = enabled,
200 PanelType::Gpu => self.gpu.enabled = enabled,
201 PanelType::Battery => self.battery.enabled = enabled,
202 PanelType::Sensors => self.sensors.enabled = enabled,
203 PanelType::Psi => self.psi.enabled = enabled,
204 PanelType::Connections => self.connections.enabled = enabled,
205 PanelType::Files => self.files.enabled = enabled,
206 _ => {}
207 }
208 }
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct PanelConfig {
214 #[serde(default = "default_true")]
216 pub enabled: bool,
217
218 #[serde(default)]
220 pub histogram: HistogramStyle,
221
222 #[serde(default)]
224 pub show_temperature: bool,
225
226 #[serde(default)]
228 pub show_frequency: bool,
229
230 #[serde(default = "default_sparkline_history")]
232 pub sparkline_history: u32,
233}
234
235impl Default for PanelConfig {
236 fn default() -> Self {
237 Self {
238 enabled: true,
239 histogram: HistogramStyle::Braille,
240 show_temperature: true,
241 show_frequency: true,
242 sparkline_history: 60,
243 }
244 }
245}
246
247fn default_sparkline_history() -> u32 {
248 60
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct ProcessPanelConfig {
254 #[serde(default = "default_true")]
256 pub enabled: bool,
257
258 #[serde(default = "default_max_processes")]
260 pub max_processes: u32,
261
262 #[serde(default = "default_columns")]
264 pub columns: Vec<String>,
265}
266
267impl Default for ProcessPanelConfig {
268 fn default() -> Self {
269 Self {
270 enabled: true,
271 max_processes: 20,
272 columns: default_columns(),
273 }
274 }
275}
276
277fn default_max_processes() -> u32 {
278 20
279}
280
281fn default_columns() -> Vec<String> {
282 vec![
283 "pid".into(),
284 "user".into(),
285 "cpu".into(),
286 "mem".into(),
287 "cmd".into(),
288 ]
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
293#[serde(rename_all = "lowercase")]
294pub enum HistogramStyle {
295 #[default]
297 Braille,
298 Block,
300 Ascii,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct KeybindingConfig {
307 #[serde(default = "default_quit")]
309 pub quit: char,
310 #[serde(default = "default_help")]
312 pub help: char,
313 #[serde(default = "default_toggle_fps")]
315 pub toggle_fps: char,
316 #[serde(default = "default_filter")]
318 pub filter: char,
319 #[serde(default = "default_sort_cpu")]
321 pub sort_cpu: char,
322 #[serde(default = "default_sort_mem")]
324 pub sort_mem: char,
325 #[serde(default = "default_sort_pid")]
327 pub sort_pid: char,
328 #[serde(default = "default_kill")]
330 pub kill_process: char,
331 #[serde(default = "default_explode")]
333 pub explode: String,
334 #[serde(default = "default_collapse")]
336 pub collapse: String,
337 #[serde(default = "default_navigate")]
339 pub navigate: String,
340}
341
342impl Default for KeybindingConfig {
343 fn default() -> Self {
344 Self {
345 quit: 'q',
346 help: '?',
347 toggle_fps: 'f',
348 filter: '/',
349 sort_cpu: 'c',
350 sort_mem: 'm',
351 sort_pid: 'p',
352 kill_process: 'k',
353 explode: "Enter".into(),
354 collapse: "Escape".into(),
355 navigate: "Tab".into(),
356 }
357 }
358}
359
360fn default_quit() -> char {
361 'q'
362}
363fn default_help() -> char {
364 '?'
365}
366fn default_toggle_fps() -> char {
367 'f'
368}
369fn default_filter() -> char {
370 '/'
371}
372fn default_sort_cpu() -> char {
373 'c'
374}
375fn default_sort_mem() -> char {
376 'm'
377}
378fn default_sort_pid() -> char {
379 'p'
380}
381fn default_kill() -> char {
382 'k'
383}
384fn default_explode() -> String {
385 "Enter".into()
386}
387fn default_collapse() -> String {
388 "Escape".into()
389}
390fn default_navigate() -> String {
391 "Tab".into()
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct ThemeConfig {
397 #[serde(default)]
399 pub panel_colors: HashMap<String, String>,
400
401 #[serde(default)]
403 pub high_contrast: bool,
404
405 #[serde(default)]
407 pub colorblind_safe: bool,
408}
409
410impl Default for ThemeConfig {
411 fn default() -> Self {
412 let mut panel_colors = HashMap::new();
413 panel_colors.insert("cpu".into(), "#64C8FF".into());
414 panel_colors.insert("memory".into(), "#B478FF".into());
415 panel_colors.insert("disk".into(), "#64B4FF".into());
416 panel_colors.insert("network".into(), "#FF9664".into());
417 panel_colors.insert("process".into(), "#DCC464".into());
418 panel_colors.insert("gpu".into(), "#64FF96".into());
419 panel_colors.insert("battery".into(), "#FFDC64".into());
420 panel_colors.insert("sensors".into(), "#FF6496".into());
421 panel_colors.insert("psi".into(), "#C85050".into());
422 panel_colors.insert("connections".into(), "#78B4DC".into());
423 panel_colors.insert("files".into(), "#B48C64".into());
424
425 Self {
426 panel_colors,
427 high_contrast: false,
428 colorblind_safe: false,
429 }
430 }
431}
432
433impl ThemeConfig {
434 pub fn iter_panel_colors(&self) -> impl Iterator<Item = (PanelType, &str)> {
436 [
437 (PanelType::Cpu, "cpu"),
438 (PanelType::Memory, "memory"),
439 (PanelType::Disk, "disk"),
440 (PanelType::Network, "network"),
441 (PanelType::Process, "process"),
442 (PanelType::Gpu, "gpu"),
443 (PanelType::Battery, "battery"),
444 (PanelType::Sensors, "sensors"),
445 (PanelType::Psi, "psi"),
446 (PanelType::Connections, "connections"),
447 (PanelType::Files, "files"),
448 ]
449 .into_iter()
450 .filter_map(|(panel, key)| self.panel_colors.get(key).map(|c| (panel, c.as_str())))
451 }
452}
453
454#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
456#[serde(rename_all = "lowercase")]
457pub enum PanelType {
458 Cpu,
460 Memory,
462 Disk,
464 Network,
466 Process,
468 Gpu,
470 Battery,
472 Sensors,
474 SensorsCompact,
476 Psi,
478 System,
480 Connections,
482 Treemap,
484 Files,
486}
487
488impl PanelType {
489 pub fn index(self) -> usize {
491 match self {
492 Self::Cpu => 0,
493 Self::Memory => 1,
494 Self::Disk => 2,
495 Self::Network => 3,
496 Self::Process => 4,
497 Self::Gpu => 5,
498 Self::Battery => 6,
499 Self::Sensors => 7,
500 Self::SensorsCompact => 8,
501 Self::Psi => 9,
502 Self::System => 10,
503 Self::Connections => 11,
504 Self::Treemap => 12,
505 Self::Files => 13,
506 }
507 }
508
509 pub fn name(self) -> &'static str {
511 match self {
512 Self::Cpu => "CPU",
513 Self::Memory => "Memory",
514 Self::Disk => "Disk",
515 Self::Network => "Network",
516 Self::Process => "Process",
517 Self::Gpu => "GPU",
518 Self::Battery => "Battery",
519 Self::Sensors => "Sensors",
520 Self::SensorsCompact => "SensorsCompact",
521 Self::Psi => "PSI",
522 Self::System => "System",
523 Self::Connections => "Connections",
524 Self::Treemap => "Treemap",
525 Self::Files => "Files",
526 }
527 }
528
529 pub fn key(self) -> &'static str {
531 match self {
532 Self::Cpu => "cpu",
533 Self::Memory => "memory",
534 Self::Disk => "disk",
535 Self::Network => "network",
536 Self::Process => "process",
537 Self::Gpu => "gpu",
538 Self::Battery => "battery",
539 Self::Sensors => "sensors",
540 Self::SensorsCompact => "sensors_compact",
541 Self::Psi => "psi",
542 Self::System => "system",
543 Self::Connections => "connections",
544 Self::Treemap => "treemap",
545 Self::Files => "files",
546 }
547 }
548
549 pub fn all() -> &'static [PanelType] {
551 &[
552 Self::Cpu,
553 Self::Memory,
554 Self::Disk,
555 Self::Network,
556 Self::Process,
557 Self::Gpu,
558 Self::Battery,
559 Self::Sensors,
560 Self::SensorsCompact,
561 Self::Psi,
562 Self::System,
563 Self::Connections,
564 Self::Treemap,
565 Self::Files,
566 ]
567 }
568}
569
570impl PresentarConfig {
571 pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml_ng::Error> {
573 serde_yaml_ng::from_str(yaml)
574 }
575
576 pub fn to_yaml(&self) -> Result<String, serde_yaml_ng::Error> {
578 serde_yaml_ng::to_string(self)
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585
586 #[test]
587 fn test_default_config() {
588 let config = PresentarConfig::default();
589 assert_eq!(config.refresh_ms, 1000);
590 assert!(config.layout.snap_to_grid);
591 assert_eq!(config.layout.grid_size, 4);
592 }
593
594 #[test]
595 fn test_parse_minimal_yaml() {
596 let yaml = "refresh_ms: 500";
597 let config = PresentarConfig::from_yaml(yaml).unwrap();
598 assert_eq!(config.refresh_ms, 500);
599 }
600
601 #[test]
602 fn test_parse_full_yaml() {
603 let yaml = r#"
604refresh_ms: 1000
605layout:
606 snap_to_grid: true
607 grid_size: 4
608panels:
609 cpu:
610 enabled: true
611 histogram: braille
612 memory:
613 enabled: true
614keybindings:
615 quit: q
616 help: "?"
617"#;
618 let config = PresentarConfig::from_yaml(yaml).unwrap();
619 assert_eq!(config.refresh_ms, 1000);
620 assert!(config.panels.cpu.enabled);
621 assert_eq!(config.keybindings.quit, 'q');
622 }
623
624 #[test]
625 fn test_panel_type_index() {
626 assert_eq!(PanelType::Cpu.index(), 0);
627 assert_eq!(PanelType::Memory.index(), 1);
628 assert_eq!(PanelType::Files.index(), 13);
629 }
630
631 #[test]
632 fn test_panel_type_name() {
633 assert_eq!(PanelType::Cpu.name(), "CPU");
634 assert_eq!(PanelType::Memory.name(), "Memory");
635 }
636
637 #[test]
638 fn test_panel_type_key() {
639 assert_eq!(PanelType::Cpu.key(), "cpu");
640 assert_eq!(PanelType::SensorsCompact.key(), "sensors_compact");
641 }
642
643 #[test]
644 fn test_iter_enabled() {
645 let config = PanelConfigs::default();
646 let enabled: Vec<_> = config.iter_enabled();
647 assert!(enabled.iter().all(|(_, e)| *e));
648 }
649
650 #[test]
651 fn test_set_enabled() {
652 let mut config = PanelConfigs::default();
653 config.set_enabled(PanelType::Cpu, false);
654 assert!(!config.cpu.enabled);
655 }
656
657 #[test]
658 fn test_theme_default_colors() {
659 let theme = ThemeConfig::default();
660 assert_eq!(theme.panel_colors.get("cpu"), Some(&"#64C8FF".to_string()));
661 assert_eq!(
662 theme.panel_colors.get("memory"),
663 Some(&"#B478FF".to_string())
664 );
665 }
666
667 #[test]
668 fn test_histogram_style_parse() {
669 let yaml = r#"
670panels:
671 cpu:
672 histogram: block
673"#;
674 let config: PresentarConfig = serde_yaml_ng::from_str(yaml).unwrap();
675 assert_eq!(config.panels.cpu.histogram, HistogramStyle::Block);
676 }
677
678 #[test]
679 fn test_border_style_parse() {
680 let yaml = r#"
681layout:
682 border_style: sharp
683"#;
684 let config: PresentarConfig = serde_yaml_ng::from_str(yaml).unwrap();
685 assert_eq!(config.layout.border_style, BorderStyle::Sharp);
686 }
687
688 #[test]
689 fn test_to_yaml_roundtrip() {
690 let config = PresentarConfig::default();
691 let yaml = config.to_yaml().unwrap();
692 let parsed = PresentarConfig::from_yaml(&yaml).unwrap();
693 assert_eq!(config.refresh_ms, parsed.refresh_ms);
694 }
695
696 #[test]
697 fn test_panel_type_all() {
698 let all = PanelType::all();
699 assert_eq!(all.len(), 14);
700 assert_eq!(all[0], PanelType::Cpu);
701 assert_eq!(all[13], PanelType::Files);
702 }
703
704 #[test]
705 fn test_process_panel_columns() {
706 let config = ProcessPanelConfig::default();
707 assert!(config.columns.contains(&"pid".to_string()));
708 assert!(config.columns.contains(&"cpu".to_string()));
709 assert!(config.columns.contains(&"mem".to_string()));
710 }
711}