mi6_core/config/
tui.rs

1//! TUI configuration for the unified config.toml file.
2//!
3//! This module defines the `[tui]` section of the unified config file.
4//! TUI-specific types (SessionField, SortColumn, etc.) remain in mi6-tui,
5//! but this module uses string-based representations for portability.
6
7use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11/// Current TUI config version.
12///
13/// ## Versioning Strategy
14/// - Bump when making **breaking changes**: removing/renaming fields, changing semantics
15/// - Do NOT bump for **additive changes**: new optional fields with defaults
16///
17/// ## Version History
18/// - Version 1: Original JSON format with numeric column indices
19/// - Version 2: JSON format with SessionField enum values
20/// - Version 3: Unified TOML format (current)
21pub const CURRENT_TUI_CONFIG_VERSION: u32 = 3;
22
23/// TUI configuration section for the unified config.
24///
25/// This struct is designed to be serialized as the `[tui]` section in config.toml.
26/// It stores values in a format that can be serialized/deserialized without
27/// depending on TUI-specific types like SessionField.
28#[derive(Debug, Clone, Default, Serialize, Deserialize)]
29pub struct TuiConfig {
30    /// Config version for migrations.
31    #[serde(default)]
32    pub version: Option<u32>,
33
34    /// Column visibility settings (one bool per column).
35    #[serde(default, skip_serializing_if = "Vec::is_empty")]
36    pub column_visibility: Vec<bool>,
37
38    /// Column display order (field names as strings).
39    #[serde(default, skip_serializing_if = "Vec::is_empty")]
40    pub column_order: Vec<String>,
41
42    /// Refresh interval in milliseconds.
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub refresh_interval_ms: Option<u64>,
45
46    /// Transcript poll interval in milliseconds.
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub transcript_poll_interval_ms: Option<u64>,
49
50    /// Theme name (e.g., "default", "minimal", "dracula").
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub theme: Option<String>,
53
54    /// Override for border visibility.
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub show_borders: Option<bool>,
57
58    /// Named column profiles.
59    /// Each profile contains visibility and order settings.
60    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
61    pub profiles: HashMap<String, TuiColumnProfile>,
62
63    /// Whether the group panel is enabled/visible.
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub group_panel_enabled: Option<bool>,
66
67    /// Whether animations are enabled (e.g., panel slide transitions).
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub animation_enabled: Option<bool>,
70
71    /// Animation duration in milliseconds.
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub animation_duration_ms: Option<u64>,
74
75    /// Sort specification - list of sort columns with direction.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub sort_spec: Option<Vec<TuiSortSpec>>,
78
79    /// Whether to auto-enable newly detected frameworks.
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub auto_enable_frameworks: Option<bool>,
82
83    /// Method for spawning new terminals.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub terminal_spawn_method: Option<String>,
86}
87
88/// Column profile stored in TOML format.
89#[derive(Debug, Clone, Default, Serialize, Deserialize)]
90pub struct TuiColumnProfile {
91    /// Column visibility for this profile.
92    #[serde(default)]
93    pub visibility: Vec<bool>,
94
95    /// Column order for this profile (field names as strings).
96    #[serde(default)]
97    pub order: Vec<String>,
98}
99
100/// Sort specification for a single column.
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
102pub struct TuiSortSpec {
103    /// Field name to sort by (e.g., "cpu", "memory", "age").
104    pub field: String,
105
106    /// Sort order: "ascending" or "descending".
107    pub order: String,
108}
109
110impl TuiConfig {
111    /// Check if the config is empty (all fields are default/None).
112    pub fn is_empty(&self) -> bool {
113        self.version.is_none()
114            && self.column_visibility.is_empty()
115            && self.column_order.is_empty()
116            && self.refresh_interval_ms.is_none()
117            && self.transcript_poll_interval_ms.is_none()
118            && self.theme.is_none()
119            && self.show_borders.is_none()
120            && self.profiles.is_empty()
121            && self.group_panel_enabled.is_none()
122            && self.animation_enabled.is_none()
123            && self.animation_duration_ms.is_none()
124            && self.sort_spec.is_none()
125            && self.auto_enable_frameworks.is_none()
126            && self.terminal_spawn_method.is_none()
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn test_tui_config_default_is_empty() {
136        let config = TuiConfig::default();
137        assert!(config.is_empty());
138    }
139
140    #[test]
141    fn test_tui_config_serde_roundtrip() {
142        let config = TuiConfig {
143            version: Some(CURRENT_TUI_CONFIG_VERSION),
144            column_visibility: vec![true, false, true],
145            column_order: vec!["pid".to_string(), "status".to_string(), "age".to_string()],
146            refresh_interval_ms: Some(1000),
147            transcript_poll_interval_ms: Some(5000),
148            theme: Some("dracula".to_string()),
149            show_borders: Some(true),
150            profiles: HashMap::new(),
151            group_panel_enabled: Some(true),
152            animation_enabled: Some(true),
153            animation_duration_ms: Some(220),
154            sort_spec: Some(vec![TuiSortSpec {
155                field: "cpu".to_string(),
156                order: "descending".to_string(),
157            }]),
158            auto_enable_frameworks: Some(false),
159            terminal_spawn_method: Some("new_mux_pane".to_string()),
160        };
161
162        let toml_str = toml::to_string_pretty(&config).unwrap();
163        let parsed: TuiConfig = toml::from_str(&toml_str).unwrap();
164
165        assert_eq!(parsed.version, config.version);
166        assert_eq!(parsed.column_visibility, config.column_visibility);
167        assert_eq!(parsed.column_order, config.column_order);
168        assert_eq!(parsed.theme, config.theme);
169        assert_eq!(parsed.refresh_interval_ms, config.refresh_interval_ms);
170    }
171
172    #[test]
173    fn test_tui_config_empty_fields_not_serialized() {
174        let config = TuiConfig {
175            theme: Some("default".to_string()),
176            ..Default::default()
177        };
178
179        let toml_str = toml::to_string_pretty(&config).unwrap();
180        // Empty fields should not appear in output
181        assert!(!toml_str.contains("column_visibility"));
182        assert!(!toml_str.contains("profiles"));
183        // But theme should appear
184        assert!(toml_str.contains("theme"));
185    }
186}