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., "robotic", "stealth", "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    /// DEPRECATED: Use `framework_auto_enable` for per-framework settings.
81    /// Kept for backward compatibility during migration.
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub auto_enable_frameworks: Option<bool>,
84
85    /// Per-framework auto-enable settings.
86    /// Maps framework name (e.g., "claude", "gemini") to auto-enable flag.
87    /// If a framework is not present, defaults to true.
88    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
89    pub framework_auto_enable: HashMap<String, bool>,
90
91    /// Method for spawning new terminals.
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub terminal_spawn_method: Option<String>,
94
95    /// Whether to shorten model names (e.g., "gpt-5.2-codex" → "gpt-5.2").
96    /// Defaults to true.
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub shorten_model_names: Option<bool>,
99
100    /// Whether to check for updates on startup.
101    /// Defaults to true. When enabled, mi6 will check for new versions in the background
102    /// on startup and notify if an update is available.
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub check_for_updates: Option<bool>,
105
106    /// Whether to use theme-defined pane background colors.
107    /// Defaults to false for backward compatibility (uses terminal default).
108    /// When enabled, uses the theme's `[panes]` and `[core]` background colors.
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub pane_backgrounds: Option<bool>,
111}
112
113/// Column profile stored in TOML format.
114#[derive(Debug, Clone, Default, Serialize, Deserialize)]
115pub struct TuiColumnProfile {
116    /// Column visibility for this profile.
117    #[serde(default)]
118    pub visibility: Vec<bool>,
119
120    /// Column order for this profile (field names as strings).
121    #[serde(default)]
122    pub order: Vec<String>,
123}
124
125/// Sort specification for a single column.
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
127pub struct TuiSortSpec {
128    /// Field name to sort by (e.g., "cpu", "memory", "age").
129    pub field: String,
130
131    /// Sort order: "ascending" or "descending".
132    pub order: String,
133}
134
135impl TuiConfig {
136    /// Check if the config is empty (all fields are default/None).
137    pub fn is_empty(&self) -> bool {
138        self.version.is_none()
139            && self.column_visibility.is_empty()
140            && self.column_order.is_empty()
141            && self.refresh_interval_ms.is_none()
142            && self.transcript_poll_interval_ms.is_none()
143            && self.theme.is_none()
144            && self.show_borders.is_none()
145            && self.profiles.is_empty()
146            && self.group_panel_enabled.is_none()
147            && self.animation_enabled.is_none()
148            && self.animation_duration_ms.is_none()
149            && self.sort_spec.is_none()
150            && self.auto_enable_frameworks.is_none()
151            && self.framework_auto_enable.is_empty()
152            && self.terminal_spawn_method.is_none()
153            && self.shorten_model_names.is_none()
154            && self.check_for_updates.is_none()
155            && self.pane_backgrounds.is_none()
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_tui_config_default_is_empty() {
165        let config = TuiConfig::default();
166        assert!(config.is_empty());
167    }
168
169    #[test]
170    fn test_tui_config_serde_roundtrip() {
171        let mut framework_auto_enable = HashMap::new();
172        framework_auto_enable.insert("claude".to_string(), false);
173        framework_auto_enable.insert("gemini".to_string(), true);
174
175        let config = TuiConfig {
176            version: Some(CURRENT_TUI_CONFIG_VERSION),
177            column_visibility: vec![true, false, true],
178            column_order: vec!["pid".to_string(), "status".to_string(), "age".to_string()],
179            refresh_interval_ms: Some(1000),
180            transcript_poll_interval_ms: Some(5000),
181            theme: Some("dracula".to_string()),
182            show_borders: Some(true),
183            profiles: HashMap::new(),
184            group_panel_enabled: Some(true),
185            animation_enabled: Some(true),
186            animation_duration_ms: Some(220),
187            sort_spec: Some(vec![TuiSortSpec {
188                field: "cpu".to_string(),
189                order: "descending".to_string(),
190            }]),
191            auto_enable_frameworks: None, // Deprecated
192            framework_auto_enable,
193            terminal_spawn_method: Some("new_mux_pane".to_string()),
194            shorten_model_names: Some(true),
195            check_for_updates: Some(true),
196            pane_backgrounds: Some(false),
197        };
198
199        let toml_str = toml::to_string_pretty(&config).unwrap();
200        let parsed: TuiConfig = toml::from_str(&toml_str).unwrap();
201
202        assert_eq!(parsed.version, config.version);
203        assert_eq!(parsed.column_visibility, config.column_visibility);
204        assert_eq!(parsed.column_order, config.column_order);
205        assert_eq!(parsed.theme, config.theme);
206        assert_eq!(parsed.refresh_interval_ms, config.refresh_interval_ms);
207        assert_eq!(parsed.framework_auto_enable, config.framework_auto_enable);
208    }
209
210    #[test]
211    fn test_tui_config_empty_fields_not_serialized() {
212        let config = TuiConfig {
213            theme: Some("dracula".to_string()),
214            ..Default::default()
215        };
216
217        let toml_str = toml::to_string_pretty(&config).unwrap();
218        // Empty fields should not appear in output
219        assert!(!toml_str.contains("column_visibility"));
220        assert!(!toml_str.contains("profiles"));
221        // But theme should appear
222        assert!(toml_str.contains("theme"));
223    }
224}