claude_code_statusline_core/
config.rs1use crate::error::CoreError;
29pub use crate::types::config::Config;
30use std::fs;
31use std::path::PathBuf;
32
33impl Config {
34 pub fn load() -> Result<Self, CoreError> {
55 let xdg_candidate =
57 dirs::home_dir().map(|h| h.join(".config").join("claude-code-statusline.toml"));
58
59 if let Some(ref xdg) = xdg_candidate {
60 if xdg.exists() {
61 let contents = fs::read_to_string(xdg).map_err(|e| CoreError::ConfigRead {
62 path: xdg.display().to_string(),
63 source: e,
64 })?;
65 let cfg: Config =
66 toml::from_str(&contents).map_err(|e| CoreError::ConfigParse {
67 path: xdg.display().to_string(),
68 source: e,
69 })?;
70 return Ok(cfg);
71 }
72 }
73
74 let primary = get_config_path();
76 if primary.exists() {
77 let contents = fs::read_to_string(&primary).map_err(|e| CoreError::ConfigRead {
78 path: primary.display().to_string(),
79 source: e,
80 })?;
81 let cfg: Config = toml::from_str(&contents).map_err(|e| CoreError::ConfigParse {
82 path: primary.display().to_string(),
83 source: e,
84 })?;
85 return Ok(cfg);
86 }
87
88 Ok(Config::default())
90 }
91}
92
93fn get_config_path() -> PathBuf {
105 if let Some(base) = dirs::config_dir() {
107 return base.join("claude-code-statusline.toml");
108 }
109 if let Some(home) = dirs::home_dir() {
111 let xdg_path = home.join(".config").join("claude-code-statusline.toml");
112 if xdg_path.exists() {
113 return xdg_path;
114 }
115 }
116
117 if let Some(base) = dirs::config_dir() {
120 return base.join("claude-code-statusline.toml");
121 }
122
123 PathBuf::from("~/.config/claude-code-statusline.toml")
125}
126
127pub fn config_path() -> PathBuf {
132 get_config_path()
133}
134
135pub struct ConfigProvider<'a> {
138 config: &'a Config,
139}
140
141impl<'a> ConfigProvider<'a> {
142 pub fn new(config: &'a Config) -> Self {
143 Self { config }
144 }
145
146 pub fn module_table(&self, module: &str) -> Option<&toml::value::Table> {
148 self.config.extra_module_table(module)
151 }
152
153 pub fn list_extra_modules(&self) -> Vec<String> {
155 self.config.extra_modules.keys().cloned().collect()
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use crate::types::config::Config as Cfg;
163
164 use std::sync::{Mutex, OnceLock};
165
166 fn env_lock() -> &'static Mutex<()> {
167 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
168 LOCK.get_or_init(|| Mutex::new(()))
169 }
170
171 #[test]
172 fn test_default_config() {
173 let config = Config::default();
174
175 assert_eq!(config.format, "$directory $claude_model");
177 assert_eq!(config.command_timeout, 500);
178 assert!(!config.debug);
179
180 assert_eq!(config.directory.format, "[$path]($style)");
182 assert_eq!(config.directory.style, "bold cyan");
183 assert_eq!(config.directory.truncation_length, 3);
184 assert!(config.directory.truncate_to_repo);
185 assert!(!config.directory.disabled);
186
187 assert_eq!(config.claude_model.format, "[$symbol$model]($style)");
189 assert_eq!(config.claude_model.style, "bold yellow");
190 assert_eq!(config.claude_model.symbol, "");
191 assert!(!config.claude_model.disabled);
192 }
193
194 #[test]
195 fn test_load_missing_config_returns_default() {
196 let _guard = env_lock().lock().unwrap();
198 let tmp = tempfile::tempdir().unwrap();
199 let orig_home = std::env::var_os("HOME");
201 unsafe {
203 std::env::set_var("HOME", tmp.path());
204 }
205 let config = Config::load().unwrap();
206 match orig_home {
208 Some(h) => unsafe { std::env::set_var("HOME", h) },
209 None => unsafe { std::env::remove_var("HOME") },
210 }
211 assert_eq!(config.format, "$directory $claude_model");
212 assert_eq!(config.command_timeout, 500);
213 }
214
215 #[test]
216 fn test_parse_valid_toml_config() {
217 let toml_str = r#"
218 format = "$directory $claude_model"
219 command_timeout = 300
220 debug = true
221
222 [directory]
223 format = "in [$path]($style)"
224 style = "bold blue"
225 truncation_length = 5
226
227 [claude_model]
228 symbol = "<"
229 style = "bold yellow"
230 "#;
231
232 let config: Config = toml::from_str(toml_str).unwrap();
233
234 assert_eq!(config.format, "$directory $claude_model");
235 assert_eq!(config.command_timeout, 300);
236 assert!(config.debug);
237 assert_eq!(config.directory.format, "in [$path]($style)");
238 assert_eq!(config.directory.style, "bold blue");
239 assert_eq!(config.directory.truncation_length, 5);
240 assert_eq!(config.claude_model.symbol, "<");
241 assert_eq!(config.claude_model.style, "bold yellow");
242 }
243
244 #[test]
245 fn test_partial_config_uses_defaults() {
246 let toml_str = r#"
247 debug = true
248
249 [directory]
250 style = "italic green"
251 "#;
252
253 let config: Config = toml::from_str(toml_str).unwrap();
254
255 assert!(config.debug);
257 assert_eq!(config.directory.style, "italic green");
258
259 assert_eq!(config.format, "$directory $claude_model");
261 assert_eq!(config.command_timeout, 500);
262 assert_eq!(config.directory.format, "[$path]($style)");
263 assert_eq!(config.claude_model.symbol, "");
264 }
265
266 #[test]
267 fn test_invalid_toml_returns_default() {
268 let invalid_toml = "this is not valid TOML [ syntax";
269 let result = toml::from_str::<Config>(invalid_toml);
270 assert!(result.is_err());
271 }
272
273 #[test]
274 fn test_config_path_with_config_dir() {
275 let path = get_config_path();
277
278 if let Some(cfg_dir) = dirs::config_dir() {
279 let expected = cfg_dir.join("claude-code-statusline.toml");
280 assert_eq!(path, expected);
281 } else {
282 assert_eq!(path, PathBuf::from("~/.config/claude-code-statusline.toml"));
284 }
285 }
286
287 #[test]
288 fn extra_modules_are_preserved_and_accessible() {
289 let toml_str = r#"
290 [directory]
291 style = "bold blue"
292
293 [my_custom]
294 key = "value"
295 answer = 42
296 "#;
297 let cfg: Cfg = toml::from_str(toml_str).unwrap();
298 let provider = super::ConfigProvider::new(&cfg);
299 let t = provider.module_table("my_custom").expect("table exists");
300 assert_eq!(t.get("key").unwrap().as_str().unwrap(), "value");
301 assert_eq!(t.get("answer").unwrap().as_integer().unwrap(), 42);
302 assert!(
303 provider
304 .list_extra_modules()
305 .contains(&"my_custom".to_string())
306 );
307 }
308
309 #[test]
310 fn test_claude_model_default_symbol_is_empty() {
311 let cfg = Config::default();
313 assert_eq!(cfg.claude_model.symbol, "");
314 }
315}