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
29impl Default for TuiConfig {
30 fn default() -> Self {
31 Self {
32 admin_url: "http://localhost:9080".into(),
33 refresh_interval: 2,
34 theme: "dark".into(),
35 last_tab: None,
36 log_file: None,
37 }
38 }
39}
40
41impl TuiConfig {
42 pub fn path() -> Option<PathBuf> {
44 home_dir().map(|h| h.join(".config").join("mockforge").join("tui.toml"))
45 }
46
47 pub fn load() -> Self {
50 Self::path()
51 .and_then(|p| std::fs::read_to_string(&p).ok())
52 .and_then(|s| toml::from_str(&s).ok())
53 .unwrap_or_default()
54 }
55
56 pub fn save(&self) -> anyhow::Result<()> {
58 let path =
59 Self::path().ok_or_else(|| anyhow::anyhow!("cannot determine home directory"))?;
60 if let Some(parent) = path.parent() {
61 std::fs::create_dir_all(parent)?;
62 }
63 let contents = toml::to_string_pretty(self)?;
64 std::fs::write(&path, contents)?;
65 Ok(())
66 }
67
68 pub fn is_light_theme(&self) -> bool {
70 self.theme.eq_ignore_ascii_case("light")
71 }
72}
73
74fn home_dir() -> Option<PathBuf> {
76 std::env::var_os("HOME").map(PathBuf::from)
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn default_config_values() {
85 let cfg = TuiConfig::default();
86 assert_eq!(cfg.admin_url, "http://localhost:9080");
87 assert_eq!(cfg.refresh_interval, 2);
88 assert_eq!(cfg.theme, "dark");
89 assert!(cfg.last_tab.is_none());
90 assert!(cfg.log_file.is_none());
91 }
92
93 #[test]
94 fn deserialize_minimal_toml() {
95 let toml_str = r#"admin_url = "http://remote:9080""#;
96 let cfg: TuiConfig = toml::from_str(toml_str).unwrap();
97 assert_eq!(cfg.admin_url, "http://remote:9080");
98 assert_eq!(cfg.refresh_interval, 2);
100 assert_eq!(cfg.theme, "dark");
101 }
102
103 #[test]
104 fn deserialize_full_toml() {
105 let toml_str = r#"
106admin_url = "http://prod:9090"
107refresh_interval = 5
108theme = "light"
109last_tab = 3
110log_file = "/tmp/tui.log"
111"#;
112 let cfg: TuiConfig = toml::from_str(toml_str).unwrap();
113 assert_eq!(cfg.admin_url, "http://prod:9090");
114 assert_eq!(cfg.refresh_interval, 5);
115 assert_eq!(cfg.theme, "light");
116 assert_eq!(cfg.last_tab, Some(3));
117 assert_eq!(cfg.log_file.as_deref(), Some("/tmp/tui.log"));
118 }
119
120 #[test]
121 fn roundtrip_serialize_deserialize() {
122 let cfg = TuiConfig {
123 admin_url: "http://test:8080".into(),
124 refresh_interval: 10,
125 theme: "light".into(),
126 last_tab: Some(5),
127 log_file: Some("/var/log/tui.log".into()),
128 };
129 let serialized = toml::to_string_pretty(&cfg).unwrap();
130 let deserialized: TuiConfig = toml::from_str(&serialized).unwrap();
131 assert_eq!(cfg, deserialized);
132 }
133
134 #[test]
135 fn is_light_theme_case_insensitive() {
136 let mut cfg = TuiConfig::default();
137 assert!(!cfg.is_light_theme());
138
139 cfg.theme = "light".into();
140 assert!(cfg.is_light_theme());
141
142 cfg.theme = "Light".into();
143 assert!(cfg.is_light_theme());
144
145 cfg.theme = "LIGHT".into();
146 assert!(cfg.is_light_theme());
147
148 cfg.theme = "dark".into();
149 assert!(!cfg.is_light_theme());
150 }
151
152 #[test]
153 fn unknown_fields_ignored() {
154 let toml_str = r#"
155admin_url = "http://localhost:9080"
156unknown_field = "should be ignored"
157"#;
158 let cfg: TuiConfig = toml::from_str(toml_str).unwrap();
159 assert_eq!(cfg.admin_url, "http://localhost:9080");
160 }
161
162 #[test]
163 fn config_path_is_under_home() {
164 if let Some(path) = TuiConfig::path() {
166 assert!(path.ends_with(".config/mockforge/tui.toml"));
167 }
168 }
169
170 #[test]
171 fn load_returns_default_when_no_file() {
172 let cfg = TuiConfig::load();
174 assert_eq!(cfg, TuiConfig::default());
175 }
176}