Skip to main content

jugar_probar/presentar/
schema.rs

1//! Presentar YAML schema types.
2//!
3//! Defines the schema for ptop configuration files following the presentar spec.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Presentar configuration schema (ptop.yaml).
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PresentarConfig {
11    /// Refresh interval in milliseconds (min: 16 for 60 FPS).
12    #[serde(default = "default_refresh_ms")]
13    pub refresh_ms: u32,
14
15    /// Layout configuration.
16    #[serde(default)]
17    pub layout: LayoutConfig,
18
19    /// Panel configurations.
20    #[serde(default)]
21    pub panels: PanelConfigs,
22
23    /// Keybindings.
24    #[serde(default)]
25    pub keybindings: KeybindingConfig,
26
27    /// Theme configuration.
28    #[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/// Layout configuration.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct LayoutConfig {
51    /// Snap panels to grid.
52    #[serde(default = "default_true")]
53    pub snap_to_grid: bool,
54
55    /// Grid size for snapping.
56    #[serde(default = "default_grid_size")]
57    pub grid_size: u8,
58
59    /// Minimum panel width in columns.
60    #[serde(default = "default_min_panel_width")]
61    pub min_panel_width: u16,
62
63    /// Minimum panel height in rows.
64    #[serde(default = "default_min_panel_height")]
65    pub min_panel_height: u16,
66
67    /// Top section height ratio (0.0-1.0).
68    #[serde(default = "default_top_height")]
69    pub top_height: f32,
70
71    /// Bottom section height ratio (0.0-1.0).
72    #[serde(default = "default_bottom_height")]
73    pub bottom_height: f32,
74
75    /// Border style.
76    #[serde(default)]
77    pub border_style: BorderStyle,
78
79    /// Content padding.
80    #[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/// Border style enumeration.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
123#[serde(rename_all = "lowercase")]
124pub enum BorderStyle {
125    /// Rounded corners (btop-style).
126    #[default]
127    Rounded,
128    /// Sharp corners.
129    Sharp,
130    /// Double-line borders.
131    Double,
132    /// No borders.
133    None,
134}
135
136/// Panel configurations.
137#[derive(Debug, Clone, Serialize, Deserialize, Default)]
138pub struct PanelConfigs {
139    /// CPU panel configuration.
140    #[serde(default)]
141    pub cpu: PanelConfig,
142    /// Memory panel configuration.
143    #[serde(default)]
144    pub memory: PanelConfig,
145    /// Disk panel configuration.
146    #[serde(default)]
147    pub disk: PanelConfig,
148    /// Network panel configuration.
149    #[serde(default)]
150    pub network: PanelConfig,
151    /// Process panel configuration.
152    #[serde(default)]
153    pub process: ProcessPanelConfig,
154    /// GPU panel configuration.
155    #[serde(default)]
156    pub gpu: PanelConfig,
157    /// Battery panel configuration.
158    #[serde(default)]
159    pub battery: PanelConfig,
160    /// Sensors panel configuration.
161    #[serde(default)]
162    pub sensors: PanelConfig,
163    /// PSI panel configuration.
164    #[serde(default)]
165    pub psi: PanelConfig,
166    /// Connections panel configuration.
167    #[serde(default)]
168    pub connections: PanelConfig,
169    /// Files panel configuration.
170    #[serde(default)]
171    pub files: PanelConfig,
172}
173
174impl PanelConfigs {
175    /// Iterate over all panels with their enabled status.
176    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    /// Set enabled status for a panel.
193    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/// Generic panel configuration.
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct PanelConfig {
214    /// Whether the panel is enabled.
215    #[serde(default = "default_true")]
216    pub enabled: bool,
217
218    /// Histogram style.
219    #[serde(default)]
220    pub histogram: HistogramStyle,
221
222    /// Show temperature.
223    #[serde(default)]
224    pub show_temperature: bool,
225
226    /// Show frequency.
227    #[serde(default)]
228    pub show_frequency: bool,
229
230    /// Sparkline history in seconds.
231    #[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/// Process panel configuration.
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct ProcessPanelConfig {
254    /// Whether the panel is enabled.
255    #[serde(default = "default_true")]
256    pub enabled: bool,
257
258    /// Maximum processes to display.
259    #[serde(default = "default_max_processes")]
260    pub max_processes: u32,
261
262    /// Columns to display.
263    #[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/// Histogram style.
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
293#[serde(rename_all = "lowercase")]
294pub enum HistogramStyle {
295    /// Braille characters (high resolution).
296    #[default]
297    Braille,
298    /// Block characters.
299    Block,
300    /// ASCII characters.
301    Ascii,
302}
303
304/// Keybinding configuration.
305#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct KeybindingConfig {
307    /// Key to quit the application.
308    #[serde(default = "default_quit")]
309    pub quit: char,
310    /// Key to show help.
311    #[serde(default = "default_help")]
312    pub help: char,
313    /// Key to toggle FPS display.
314    #[serde(default = "default_toggle_fps")]
315    pub toggle_fps: char,
316    /// Key to filter processes.
317    #[serde(default = "default_filter")]
318    pub filter: char,
319    /// Key to sort by CPU usage.
320    #[serde(default = "default_sort_cpu")]
321    pub sort_cpu: char,
322    /// Key to sort by memory usage.
323    #[serde(default = "default_sort_mem")]
324    pub sort_mem: char,
325    /// Key to sort by PID.
326    #[serde(default = "default_sort_pid")]
327    pub sort_pid: char,
328    /// Key to kill selected process.
329    #[serde(default = "default_kill")]
330    pub kill_process: char,
331    /// Key to explode (expand) a panel.
332    #[serde(default = "default_explode")]
333    pub explode: String,
334    /// Key to collapse a panel.
335    #[serde(default = "default_collapse")]
336    pub collapse: String,
337    /// Key to navigate between panels.
338    #[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/// Theme configuration.
395#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct ThemeConfig {
397    /// Panel border colors (hex).
398    #[serde(default)]
399    pub panel_colors: HashMap<String, String>,
400
401    /// High contrast mode.
402    #[serde(default)]
403    pub high_contrast: bool,
404
405    /// Colorblind-safe palette.
406    #[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    /// Get panel colors as iterator.
435    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/// Panel type enumeration.
455#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
456#[serde(rename_all = "lowercase")]
457pub enum PanelType {
458    /// CPU usage panel.
459    Cpu,
460    /// Memory usage panel.
461    Memory,
462    /// Disk usage panel.
463    Disk,
464    /// Network usage panel.
465    Network,
466    /// Process list panel.
467    Process,
468    /// GPU usage panel.
469    Gpu,
470    /// Battery status panel.
471    Battery,
472    /// Sensors panel.
473    Sensors,
474    /// Compact sensors panel.
475    SensorsCompact,
476    /// PSI (Pressure Stall Information) panel.
477    Psi,
478    /// System info panel.
479    System,
480    /// Network connections panel.
481    Connections,
482    /// Treemap visualization panel.
483    Treemap,
484    /// Files panel.
485    Files,
486}
487
488impl PanelType {
489    /// Get panel index (0-based).
490    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    /// Get panel name.
510    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    /// Get panel key for config.
530    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    /// Get all panel types.
550    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    /// Parse configuration from YAML string.
572    pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml_ng::Error> {
573        serde_yaml_ng::from_str(yaml)
574    }
575
576    /// Serialize configuration to YAML string.
577    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}