1use std::path::PathBuf;
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11#[serde(default)]
12pub struct TuiConfig {
13 pub admin_url: String,
15
16 pub refresh_interval: u64,
18
19 pub theme: String,
21
22 pub last_tab: Option<usize>,
24
25 pub log_file: Option<String>,
27
28 #[serde(default)]
34 pub extra_servers: Vec<String>,
35}
36
37impl Default for TuiConfig {
38 fn default() -> Self {
39 Self {
40 admin_url: "http://localhost:9080".into(),
41 refresh_interval: 2,
42 theme: "dark".into(),
43 last_tab: None,
44 log_file: None,
45 extra_servers: Vec::new(),
46 }
47 }
48}
49
50impl TuiConfig {
51 pub fn all_admin_urls(&self) -> Vec<String> {
56 let mut urls = Vec::with_capacity(1 + self.extra_servers.len());
57 urls.push(self.admin_url.clone());
58 for u in &self.extra_servers {
59 if !u.is_empty() && !urls.iter().any(|existing| existing == u) {
60 urls.push(u.clone());
61 }
62 }
63 urls
64 }
65}
66
67impl TuiConfig {
68 pub fn path() -> Option<PathBuf> {
70 home_dir().map(|h| h.join(".config").join("mockforge").join("tui.toml"))
71 }
72
73 pub fn load() -> Self {
76 Self::path()
77 .and_then(|p| std::fs::read_to_string(&p).ok())
78 .and_then(|s| toml::from_str(&s).ok())
79 .unwrap_or_default()
80 }
81
82 pub fn save(&self) -> anyhow::Result<()> {
84 let path =
85 Self::path().ok_or_else(|| anyhow::anyhow!("cannot determine home directory"))?;
86 if let Some(parent) = path.parent() {
87 std::fs::create_dir_all(parent)?;
88 }
89 let contents = toml::to_string_pretty(self)?;
90 std::fs::write(&path, contents)?;
91 Ok(())
92 }
93
94 pub fn is_light_theme(&self) -> bool {
96 self.theme.eq_ignore_ascii_case("light")
97 }
98}
99
100fn home_dir() -> Option<PathBuf> {
102 std::env::var_os("HOME").map(PathBuf::from)
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn default_config_values() {
111 let cfg = TuiConfig::default();
112 assert_eq!(cfg.admin_url, "http://localhost:9080");
113 assert_eq!(cfg.refresh_interval, 2);
114 assert_eq!(cfg.theme, "dark");
115 assert!(cfg.last_tab.is_none());
116 assert!(cfg.log_file.is_none());
117 }
118
119 #[test]
120 fn deserialize_minimal_toml() {
121 let toml_str = r#"admin_url = "http://remote:9080""#;
122 let cfg: TuiConfig = toml::from_str(toml_str).unwrap();
123 assert_eq!(cfg.admin_url, "http://remote:9080");
124 assert_eq!(cfg.refresh_interval, 2);
126 assert_eq!(cfg.theme, "dark");
127 }
128
129 #[test]
130 fn deserialize_full_toml() {
131 let toml_str = r#"
132admin_url = "http://prod:9090"
133refresh_interval = 5
134theme = "light"
135last_tab = 3
136log_file = "/tmp/tui.log"
137"#;
138 let cfg: TuiConfig = toml::from_str(toml_str).unwrap();
139 assert_eq!(cfg.admin_url, "http://prod:9090");
140 assert_eq!(cfg.refresh_interval, 5);
141 assert_eq!(cfg.theme, "light");
142 assert_eq!(cfg.last_tab, Some(3));
143 assert_eq!(cfg.log_file.as_deref(), Some("/tmp/tui.log"));
144 }
145
146 #[test]
147 fn roundtrip_serialize_deserialize() {
148 let cfg = TuiConfig {
149 admin_url: "http://test:8080".into(),
150 refresh_interval: 10,
151 theme: "light".into(),
152 last_tab: Some(5),
153 log_file: Some("/var/log/tui.log".into()),
154 extra_servers: vec!["http://test2:8080".into()],
155 };
156 let serialized = toml::to_string_pretty(&cfg).unwrap();
157 let deserialized: TuiConfig = toml::from_str(&serialized).unwrap();
158 assert_eq!(cfg, deserialized);
159 }
160
161 #[test]
162 fn is_light_theme_case_insensitive() {
163 let mut cfg = TuiConfig::default();
164 assert!(!cfg.is_light_theme());
165
166 cfg.theme = "light".into();
167 assert!(cfg.is_light_theme());
168
169 cfg.theme = "Light".into();
170 assert!(cfg.is_light_theme());
171
172 cfg.theme = "LIGHT".into();
173 assert!(cfg.is_light_theme());
174
175 cfg.theme = "dark".into();
176 assert!(!cfg.is_light_theme());
177 }
178
179 #[test]
180 fn unknown_fields_ignored() {
181 let toml_str = r#"
182admin_url = "http://localhost:9080"
183unknown_field = "should be ignored"
184"#;
185 let cfg: TuiConfig = toml::from_str(toml_str).unwrap();
186 assert_eq!(cfg.admin_url, "http://localhost:9080");
187 }
188
189 #[test]
190 fn config_path_is_under_home() {
191 if let Some(path) = TuiConfig::path() {
193 assert!(path.ends_with(".config/mockforge/tui.toml"));
194 }
195 }
196
197 #[test]
198 fn load_returns_default_when_no_file() {
199 let cfg = TuiConfig::load();
201 assert_eq!(cfg, TuiConfig::default());
202 }
203}