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