use crate::error::CoreError;
pub use crate::types::config::Config;
use std::fs;
use std::path::PathBuf;
impl Config {
pub fn load() -> Result<Self, CoreError> {
let xdg_candidate =
dirs::home_dir().map(|h| h.join(".config").join("claude-code-statusline.toml"));
if let Some(ref xdg) = xdg_candidate {
if xdg.exists() {
let contents = fs::read_to_string(xdg).map_err(|e| CoreError::ConfigRead {
path: xdg.display().to_string(),
source: e,
})?;
let cfg: Config =
toml::from_str(&contents).map_err(|e| CoreError::ConfigParse {
path: xdg.display().to_string(),
source: e,
})?;
return Ok(cfg);
}
}
let primary = get_config_path();
if primary.exists() {
let contents = fs::read_to_string(&primary).map_err(|e| CoreError::ConfigRead {
path: primary.display().to_string(),
source: e,
})?;
let cfg: Config = toml::from_str(&contents).map_err(|e| CoreError::ConfigParse {
path: primary.display().to_string(),
source: e,
})?;
return Ok(cfg);
}
Ok(Config::default())
}
}
fn get_config_path() -> PathBuf {
if let Some(base) = dirs::config_dir() {
return base.join("claude-code-statusline.toml");
}
if let Some(home) = dirs::home_dir() {
let xdg_path = home.join(".config").join("claude-code-statusline.toml");
if xdg_path.exists() {
return xdg_path;
}
}
if let Some(base) = dirs::config_dir() {
return base.join("claude-code-statusline.toml");
}
PathBuf::from("~/.config/claude-code-statusline.toml")
}
pub fn config_path() -> PathBuf {
get_config_path()
}
pub struct ConfigProvider<'a> {
config: &'a Config,
}
impl<'a> ConfigProvider<'a> {
pub fn new(config: &'a Config) -> Self {
Self { config }
}
pub fn module_table(&self, module: &str) -> Option<&toml::value::Table> {
self.config.extra_module_table(module)
}
pub fn list_extra_modules(&self) -> Vec<String> {
self.config.extra_modules.keys().cloned().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::config::Config as Cfg;
use std::sync::{Mutex, OnceLock};
fn env_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.format, "$directory $claude_model");
assert_eq!(config.command_timeout, 500);
assert!(!config.debug);
assert_eq!(config.directory.format, "[$path]($style)");
assert_eq!(config.directory.style, "bold cyan");
assert_eq!(config.directory.truncation_length, 3);
assert!(config.directory.truncate_to_repo);
assert!(!config.directory.disabled);
assert_eq!(config.claude_model.format, "[$symbol$model]($style)");
assert_eq!(config.claude_model.style, "bold yellow");
assert_eq!(config.claude_model.symbol, "");
assert!(!config.claude_model.disabled);
}
#[test]
fn test_load_missing_config_returns_default() {
let _guard = env_lock().lock().unwrap();
let tmp = tempfile::tempdir().unwrap();
let orig_home = std::env::var_os("HOME");
unsafe {
std::env::set_var("HOME", tmp.path());
}
let config = Config::load().unwrap();
match orig_home {
Some(h) => unsafe { std::env::set_var("HOME", h) },
None => unsafe { std::env::remove_var("HOME") },
}
assert_eq!(config.format, "$directory $claude_model");
assert_eq!(config.command_timeout, 500);
}
#[test]
fn test_parse_valid_toml_config() {
let toml_str = r#"
format = "$directory $claude_model"
command_timeout = 300
debug = true
[directory]
format = "in [$path]($style)"
style = "bold blue"
truncation_length = 5
[claude_model]
symbol = "<"
style = "bold yellow"
"#;
let config: Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.format, "$directory $claude_model");
assert_eq!(config.command_timeout, 300);
assert!(config.debug);
assert_eq!(config.directory.format, "in [$path]($style)");
assert_eq!(config.directory.style, "bold blue");
assert_eq!(config.directory.truncation_length, 5);
assert_eq!(config.claude_model.symbol, "<");
assert_eq!(config.claude_model.style, "bold yellow");
}
#[test]
fn test_partial_config_uses_defaults() {
let toml_str = r#"
debug = true
[directory]
style = "italic green"
"#;
let config: Config = toml::from_str(toml_str).unwrap();
assert!(config.debug);
assert_eq!(config.directory.style, "italic green");
assert_eq!(config.format, "$directory $claude_model");
assert_eq!(config.command_timeout, 500);
assert_eq!(config.directory.format, "[$path]($style)");
assert_eq!(config.claude_model.symbol, "");
}
#[test]
fn test_invalid_toml_returns_default() {
let invalid_toml = "this is not valid TOML [ syntax";
let result = toml::from_str::<Config>(invalid_toml);
assert!(result.is_err());
}
#[test]
fn test_config_path_with_config_dir() {
let path = get_config_path();
if let Some(cfg_dir) = dirs::config_dir() {
let expected = cfg_dir.join("claude-code-statusline.toml");
assert_eq!(path, expected);
} else {
assert_eq!(path, PathBuf::from("~/.config/claude-code-statusline.toml"));
}
}
#[test]
fn extra_modules_are_preserved_and_accessible() {
let toml_str = r#"
[directory]
style = "bold blue"
[my_custom]
key = "value"
answer = 42
"#;
let cfg: Cfg = toml::from_str(toml_str).unwrap();
let provider = super::ConfigProvider::new(&cfg);
let t = provider.module_table("my_custom").expect("table exists");
assert_eq!(t.get("key").unwrap().as_str().unwrap(), "value");
assert_eq!(t.get("answer").unwrap().as_integer().unwrap(), 42);
assert!(
provider
.list_extra_modules()
.contains(&"my_custom".to_string())
);
}
#[test]
fn test_claude_model_default_symbol_is_empty() {
let cfg = Config::default();
assert_eq!(cfg.claude_model.symbol, "");
}
}