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 with separate visibility/order/conditional arrays
21/// - Version 4: Unified columns list with ~prefix for conditional (current)
22pub const CURRENT_TUI_CONFIG_VERSION: u32 = 4;
23
24/// TUI configuration section for the unified config.
25///
26/// This struct is designed to be serialized as the `[tui]` section in config.toml.
27/// It stores values in a format that can be serialized/deserialized without
28/// depending on TUI-specific types like SessionField.
29#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30pub struct TuiConfig {
31    /// Config version for migrations.
32    #[serde(default)]
33    pub version: Option<u32>,
34
35    /// Unified column configuration (v4+).
36    ///
37    /// A list of column names in display order. Columns not in the list are hidden.
38    /// Use `~` prefix for conditional columns (visible when >1 distinct value).
39    ///
40    /// Example: `["pid", "status", "~model", "cpu"]`
41    /// - "pid", "status", "cpu" are always visible
42    /// - "~model" is conditional (auto-hides when all sessions have same model)
43    #[serde(default, skip_serializing_if = "Vec::is_empty")]
44    pub columns: Vec<String>,
45
46    // ========================================================================
47    // Legacy fields (v3 and earlier) - kept for migration
48    // ========================================================================
49    /// DEPRECATED (v3): Column visibility settings (one bool per column).
50    /// Use `columns` field instead.
51    #[serde(default, skip_serializing_if = "Vec::is_empty")]
52    pub column_visibility: Vec<bool>,
53
54    /// DEPRECATED (v3): Column display order (field names as strings).
55    /// Use `columns` field instead.
56    #[serde(default, skip_serializing_if = "Vec::is_empty")]
57    pub column_order: Vec<String>,
58
59    /// DEPRECATED (v3): Which columns are conditional.
60    /// Use `~` prefix in `columns` field instead.
61    #[serde(default, skip_serializing_if = "Vec::is_empty")]
62    pub column_conditional: Vec<bool>,
63
64    /// Refresh interval in milliseconds.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub refresh_interval_ms: Option<u64>,
67
68    /// Transcript poll interval in milliseconds.
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub transcript_poll_interval_ms: Option<u64>,
71
72    /// Theme name (e.g., "robotic", "stealth", "dracula").
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub theme: Option<String>,
75
76    /// Override for border visibility.
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub show_borders: Option<bool>,
79
80    /// Named column profiles.
81    /// Each profile contains visibility and order settings.
82    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
83    pub profiles: HashMap<String, TuiColumnProfile>,
84
85    /// Whether the group panel is enabled/visible.
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub group_panel_enabled: Option<bool>,
88
89    /// Whether animations are enabled (e.g., panel slide transitions).
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub animation_enabled: Option<bool>,
92
93    /// Animation duration in milliseconds.
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub animation_duration_ms: Option<u64>,
96
97    /// Sort specification - list of sort columns with direction.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub sort_spec: Option<Vec<TuiSortSpec>>,
100
101    /// Whether to auto-enable newly detected frameworks.
102    /// DEPRECATED: Use `framework_auto_enable` for per-framework settings.
103    /// Kept for backward compatibility during migration.
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub auto_enable_frameworks: Option<bool>,
106
107    /// Per-framework auto-enable settings.
108    /// Maps framework name (e.g., "claude", "gemini") to auto-enable flag.
109    /// If a framework is not present, defaults to true.
110    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
111    pub framework_auto_enable: HashMap<String, bool>,
112
113    /// Method for spawning new terminals.
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub terminal_spawn_method: Option<String>,
116
117    /// Whether to shorten model names (e.g., "gpt-5.2-codex" → "gpt-5.2").
118    /// Defaults to true.
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub shorten_model_names: Option<bool>,
121
122    /// Whether to check for updates on startup.
123    /// Defaults to true. When enabled, mi6 will check for new versions in the background
124    /// on startup and notify if an update is available.
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub check_for_updates: Option<bool>,
127
128    /// Whether to use theme-defined pane background colors.
129    /// Defaults to false for backward compatibility (uses terminal default).
130    /// When enabled, uses the theme's `[panes]` and `[core]` background colors.
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub pane_backgrounds: Option<bool>,
133}
134
135/// Column profile stored in TOML format.
136#[derive(Debug, Clone, Default, Serialize, Deserialize)]
137pub struct TuiColumnProfile {
138    /// Column visibility for this profile.
139    #[serde(default)]
140    pub visibility: Vec<bool>,
141
142    /// Column order for this profile (field names as strings).
143    #[serde(default)]
144    pub order: Vec<String>,
145}
146
147/// Sort specification for a single column.
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
149pub struct TuiSortSpec {
150    /// Field name to sort by (e.g., "cpu", "memory", "age").
151    pub field: String,
152
153    /// Sort order: "ascending" or "descending".
154    pub order: String,
155}
156
157impl TuiConfig {
158    /// Check if the config is empty (all fields are default/None).
159    pub fn is_empty(&self) -> bool {
160        self.version.is_none()
161            && self.columns.is_empty()
162            && self.column_visibility.is_empty()
163            && self.column_order.is_empty()
164            && self.column_conditional.is_empty()
165            && self.refresh_interval_ms.is_none()
166            && self.transcript_poll_interval_ms.is_none()
167            && self.theme.is_none()
168            && self.show_borders.is_none()
169            && self.profiles.is_empty()
170            && self.group_panel_enabled.is_none()
171            && self.animation_enabled.is_none()
172            && self.animation_duration_ms.is_none()
173            && self.sort_spec.is_none()
174            && self.auto_enable_frameworks.is_none()
175            && self.framework_auto_enable.is_empty()
176            && self.terminal_spawn_method.is_none()
177            && self.shorten_model_names.is_none()
178            && self.check_for_updates.is_none()
179            && self.pane_backgrounds.is_none()
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn test_tui_config_default_is_empty() {
189        let config = TuiConfig::default();
190        assert!(config.is_empty());
191    }
192
193    #[test]
194    fn test_tui_config_serde_roundtrip() {
195        let mut framework_auto_enable = HashMap::new();
196        framework_auto_enable.insert("claude".to_string(), false);
197        framework_auto_enable.insert("gemini".to_string(), true);
198
199        let config = TuiConfig {
200            version: Some(CURRENT_TUI_CONFIG_VERSION),
201            columns: vec![
202                "pid".to_string(),
203                "status".to_string(),
204                "~model".to_string(), // conditional
205                "cpu".to_string(),
206            ],
207            column_visibility: vec![],  // Deprecated
208            column_order: vec![],       // Deprecated
209            column_conditional: vec![], // Deprecated
210            refresh_interval_ms: Some(1000),
211            transcript_poll_interval_ms: Some(5000),
212            theme: Some("dracula".to_string()),
213            show_borders: Some(true),
214            profiles: HashMap::new(),
215            group_panel_enabled: Some(true),
216            animation_enabled: Some(true),
217            animation_duration_ms: Some(220),
218            sort_spec: Some(vec![TuiSortSpec {
219                field: "cpu".to_string(),
220                order: "descending".to_string(),
221            }]),
222            auto_enable_frameworks: None, // Deprecated
223            framework_auto_enable,
224            terminal_spawn_method: Some("new_mux_pane".to_string()),
225            shorten_model_names: Some(true),
226            check_for_updates: Some(true),
227            pane_backgrounds: Some(false),
228        };
229
230        let toml_str = toml::to_string_pretty(&config).unwrap();
231        let parsed: TuiConfig = toml::from_str(&toml_str).unwrap();
232
233        assert_eq!(parsed.version, config.version);
234        assert_eq!(parsed.columns, config.columns);
235        assert_eq!(parsed.theme, config.theme);
236        assert_eq!(parsed.refresh_interval_ms, config.refresh_interval_ms);
237        assert_eq!(parsed.framework_auto_enable, config.framework_auto_enable);
238    }
239
240    #[test]
241    fn test_tui_config_columns_format() {
242        // Test that the new columns format serializes correctly
243        let config = TuiConfig {
244            version: Some(CURRENT_TUI_CONFIG_VERSION),
245            columns: vec![
246                "pid".to_string(),
247                "~status".to_string(), // conditional
248                "model".to_string(),
249            ],
250            ..Default::default()
251        };
252
253        let toml_str = toml::to_string_pretty(&config).unwrap();
254
255        // Should contain the columns array
256        assert!(toml_str.contains("columns"));
257        assert!(toml_str.contains("pid"));
258        assert!(toml_str.contains("~status"));
259        assert!(toml_str.contains("model"));
260
261        // Should NOT contain deprecated fields
262        assert!(!toml_str.contains("column_visibility"));
263        assert!(!toml_str.contains("column_order"));
264        assert!(!toml_str.contains("column_conditional"));
265    }
266
267    #[test]
268    fn test_tui_config_empty_fields_not_serialized() {
269        let config = TuiConfig {
270            theme: Some("dracula".to_string()),
271            ..Default::default()
272        };
273
274        let toml_str = toml::to_string_pretty(&config).unwrap();
275        // Empty fields should not appear in output
276        assert!(!toml_str.contains("column_visibility"));
277        assert!(!toml_str.contains("profiles"));
278        // But theme should appear
279        assert!(toml_str.contains("theme"));
280    }
281}