1use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11pub const CURRENT_TUI_CONFIG_VERSION: u32 = 4;
23
24#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30pub struct TuiConfig {
31 #[serde(default)]
33 pub version: Option<u32>,
34
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
44 pub columns: Vec<String>,
45
46 #[serde(default, skip_serializing_if = "Vec::is_empty")]
52 pub column_visibility: Vec<bool>,
53
54 #[serde(default, skip_serializing_if = "Vec::is_empty")]
57 pub column_order: Vec<String>,
58
59 #[serde(default, skip_serializing_if = "Vec::is_empty")]
62 pub column_conditional: Vec<bool>,
63
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub refresh_interval_ms: Option<u64>,
67
68 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub transcript_poll_interval_ms: Option<u64>,
71
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub theme: Option<String>,
75
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub show_borders: Option<bool>,
79
80 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
83 pub profiles: HashMap<String, TuiColumnProfile>,
84
85 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub group_panel_enabled: Option<bool>,
88
89 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub animation_enabled: Option<bool>,
92
93 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub animation_duration_ms: Option<u64>,
96
97 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub sort_spec: Option<Vec<TuiSortSpec>>,
100
101 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub auto_enable_frameworks: Option<bool>,
106
107 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
111 pub framework_auto_enable: HashMap<String, bool>,
112
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub terminal_spawn_method: Option<String>,
116
117 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub shorten_model_names: Option<bool>,
121
122 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub check_for_updates: Option<bool>,
127
128 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub pane_backgrounds: Option<bool>,
133}
134
135#[derive(Debug, Clone, Default, Serialize, Deserialize)]
137pub struct TuiColumnProfile {
138 #[serde(default)]
140 pub visibility: Vec<bool>,
141
142 #[serde(default)]
144 pub order: Vec<String>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
149pub struct TuiSortSpec {
150 pub field: String,
152
153 pub order: String,
155}
156
157impl TuiConfig {
158 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(), "cpu".to_string(),
206 ],
207 column_visibility: vec![], column_order: vec![], column_conditional: vec![], 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, 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 let config = TuiConfig {
244 version: Some(CURRENT_TUI_CONFIG_VERSION),
245 columns: vec![
246 "pid".to_string(),
247 "~status".to_string(), "model".to_string(),
249 ],
250 ..Default::default()
251 };
252
253 let toml_str = toml::to_string_pretty(&config).unwrap();
254
255 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 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 assert!(!toml_str.contains("column_visibility"));
277 assert!(!toml_str.contains("profiles"));
278 assert!(toml_str.contains("theme"));
280 }
281}