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