1use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11pub const CURRENT_TUI_CONFIG_VERSION: u32 = 3;
22
23#[derive(Debug, Clone, Default, Serialize, Deserialize)]
29pub struct TuiConfig {
30 #[serde(default)]
32 pub version: Option<u32>,
33
34 #[serde(default, skip_serializing_if = "Vec::is_empty")]
36 pub column_visibility: Vec<bool>,
37
38 #[serde(default, skip_serializing_if = "Vec::is_empty")]
40 pub column_order: Vec<String>,
41
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub refresh_interval_ms: Option<u64>,
45
46 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub transcript_poll_interval_ms: Option<u64>,
49
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub theme: Option<String>,
53
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub show_borders: Option<bool>,
57
58 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
61 pub profiles: HashMap<String, TuiColumnProfile>,
62
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub group_panel_enabled: Option<bool>,
66
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub animation_enabled: Option<bool>,
70
71 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub animation_duration_ms: Option<u64>,
74
75 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub sort_spec: Option<Vec<TuiSortSpec>>,
78
79 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub auto_enable_frameworks: Option<bool>,
84
85 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
89 pub framework_auto_enable: HashMap<String, bool>,
90
91 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub terminal_spawn_method: Option<String>,
94
95 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub shorten_model_names: Option<bool>,
99
100 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub check_for_updates: Option<bool>,
105
106 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub pane_backgrounds: Option<bool>,
111}
112
113#[derive(Debug, Clone, Default, Serialize, Deserialize)]
115pub struct TuiColumnProfile {
116 #[serde(default)]
118 pub visibility: Vec<bool>,
119
120 #[serde(default)]
122 pub order: Vec<String>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
127pub struct TuiSortSpec {
128 pub field: String,
130
131 pub order: String,
133}
134
135impl TuiConfig {
136 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, 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 assert!(!toml_str.contains("column_visibility"));
220 assert!(!toml_str.contains("profiles"));
221 assert!(toml_str.contains("theme"));
223 }
224}