use crate::colors::{
SectionColors, CONTEXT_COLORS, COST_COLORS, CWD_COLORS, GIT_COLORS, MODEL_COLORS,
QUOTA_5H_COLORS, QUOTA_7D_COLORS,
};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Debug, Deserialize, Serialize)]
pub struct ThemeConfig {
pub separator: (u8, u8, u8), pub cwd: SectionColors,
pub git: SectionColors,
pub model: SectionColors,
pub context: SectionColors,
pub quota_5h: SectionColors,
pub quota_7d: SectionColors,
pub cost: SectionColors,
}
impl Default for ThemeConfig {
fn default() -> Self {
ThemeConfig {
separator: (65, 65, 62), cwd: CWD_COLORS,
git: GIT_COLORS,
model: MODEL_COLORS,
context: CONTEXT_COLORS,
quota_5h: QUOTA_5H_COLORS,
quota_7d: QUOTA_7D_COLORS,
cost: COST_COLORS,
}
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct CwdConfig {
pub enabled: bool,
pub full_path: bool,
pub show_username: bool,
}
impl Default for CwdConfig {
fn default() -> Self {
CwdConfig {
enabled: true,
full_path: true,
show_username: false,
}
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct GitConfig {
pub enabled: bool,
pub show_repo_name: bool,
pub show_diff_stats: bool,
}
impl Default for GitConfig {
fn default() -> Self {
GitConfig {
enabled: true,
show_repo_name: false,
show_diff_stats: true,
}
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct ModelConfig {
pub enabled: bool,
pub show_output_style: bool,
pub show_thinking_mode: bool,
}
impl Default for ModelConfig {
fn default() -> Self {
ModelConfig {
enabled: true,
show_output_style: false,
show_thinking_mode: true,
}
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct ContextConfig {
pub enabled: bool,
pub show_decimals: bool,
pub show_token_counts: bool,
pub display_mode: String,
}
impl Default for ContextConfig {
fn default() -> Self {
ContextConfig {
enabled: true,
show_decimals: false,
show_token_counts: true,
display_mode: "used".to_string(),
}
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct QuotaConfig {
pub enabled: bool,
pub show_time_remaining: bool,
pub cache_ttl: u64,
}
impl Default for QuotaConfig {
fn default() -> Self {
QuotaConfig {
enabled: true,
show_time_remaining: true,
cache_ttl: 60,
}
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct CostConfig {
pub enabled: bool,
pub show_durations: bool,
}
impl Default for CostConfig {
fn default() -> Self {
CostConfig {
enabled: false,
show_durations: true,
}
}
}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct SectionsConfig {
pub cwd: CwdConfig,
pub git: GitConfig,
pub model: ModelConfig,
pub context: ContextConfig,
pub quota: QuotaConfig,
pub cost: CostConfig,
}
pub const POWERLINE_ARROW: &str = "\u{E0B0}";
#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct DisplayConfig {
pub multiline: bool,
pub default_terminal_width: usize,
pub use_powerline: bool,
#[serde(skip)] pub arrow: String,
pub segment_separator: String,
pub details_separator: String,
pub section_padding: usize,
pub show_background: bool,
}
impl Default for DisplayConfig {
fn default() -> Self {
DisplayConfig {
multiline: true,
default_terminal_width: 120,
use_powerline: false,
arrow: POWERLINE_ARROW.to_string(),
segment_separator: "".to_string(),
details_separator: ", ".to_string(),
section_padding: 1,
show_background: true,
}
}
}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct Config {
pub sections: SectionsConfig,
pub display: DisplayConfig,
#[serde(skip)] pub theme: ThemeConfig,
}
fn get_config_dir() -> Option<std::path::PathBuf> {
Some(
crate::utils::get_home_dir()?
.join(".claude")
.join("statusline"),
)
}
fn load_theme(dir: &Path) -> ThemeConfig {
let path = dir.join("colors.json");
if !path.exists() {
let theme = ThemeConfig::default();
if let Ok(json) = serde_json::to_string_pretty(&theme) {
let _ = fs::write(&path, json);
}
return theme;
}
match std::fs::read_to_string(&path) {
Ok(content) => serde_json::from_str(&content).unwrap_or_else(|e| {
eprintln!("statusline warning: invalid colors.json: {}", e);
ThemeConfig::default()
}),
Err(_) => ThemeConfig::default(),
}
}
pub fn load_config() -> Config {
let dir = match get_config_dir() {
Some(d) => d,
None => return Config::default(),
};
if !dir.exists() {
let _ = fs::create_dir_all(&dir);
}
let config_path = dir.join("settings.json");
if !config_path.exists() {
let config = Config::default();
if let Ok(json) = serde_json::to_string_pretty(&config) {
let _ = fs::write(&config_path, json);
}
let mut final_config = config;
final_config.theme = load_theme(&dir);
return final_config;
}
let mut config = match std::fs::read_to_string(&config_path) {
Ok(content) => serde_json::from_str::<Config>(&content).unwrap_or_else(|e| {
eprintln!("statusline warning: invalid settings.json: {}", e);
Config::default()
}),
Err(_) => Config::default(),
};
config.theme = load_theme(&dir);
config
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_defaults() {
let config = Config::default();
assert_eq!(config.display.segment_separator, "");
assert_eq!(config.display.details_separator, ", ");
assert_eq!(config.display.section_padding, 1);
assert!(config.sections.cwd.enabled);
assert!(!config.sections.cwd.show_username);
assert!(!config.sections.git.show_repo_name);
assert_eq!(config.sections.quota.cache_ttl, 60);
assert!(!config.sections.context.show_decimals);
assert!(config.sections.context.show_token_counts);
assert_eq!(config.theme.cwd.background, Some((217, 119, 87)));
}
#[test]
fn test_theme_deserialization() {
let json = r#"{
"separator": [255, 0, 0],
"cwd": { "background": null, "foreground": [20, 20, 20], "details": [30, 30, 30] },
"git": { "background": [40, 40, 40], "foreground": [50, 50, 50], "details": [60, 60, 60] },
"model": { "background": [70, 70, 70], "foreground": [80, 80, 80], "details": [90, 90, 90] },
"context": { "background": [100, 100, 100], "foreground": [110, 110, 110], "details": [120, 120, 120] },
"quota_5h": { "background": [130, 130, 130], "foreground": [140, 140, 140], "details": [150, 150, 150] },
"quota_7d": { "background": [160, 160, 160], "foreground": [170, 170, 170], "details": [180, 180, 180] },
"cost": { "background": [190, 190, 190], "foreground": [200, 200, 200], "details": [210, 210, 210] }
}"#;
let theme: ThemeConfig = serde_json::from_str(json).unwrap();
assert_eq!(theme.separator, (255, 0, 0));
assert_eq!(theme.cwd.background, None);
assert_eq!(theme.git.background, Some((40, 40, 40)));
}
#[test]
fn test_root_settings_sample_deserializes() {
let sample = include_str!("../settings.json");
let config: Config = serde_json::from_str(sample).expect("root settings.json should parse");
assert_eq!(config.sections.quota.cache_ttl, 60);
assert_eq!(config.sections.context.display_mode, "used");
assert!(!config.sections.cwd.show_username);
}
#[test]
fn test_representative_config_snippet_deserializes() {
let sample = r#"
{
"sections": {
"cwd": { "enabled": true, "full_path": false, "show_username": true },
"git": { "enabled": true, "show_repo_name": true, "show_diff_stats": false },
"model": { "enabled": true, "show_output_style": true, "show_thinking_mode": true },
"context": { "enabled": true, "show_decimals": true, "show_token_counts": true, "display_mode": "remaining" },
"quota": { "enabled": true, "show_time_remaining": false, "cache_ttl": 300 },
"cost": { "enabled": true, "show_durations": false }
},
"display": {
"multiline": false,
"default_terminal_width": 140,
"use_powerline": false,
"segment_separator": " | ",
"details_separator": " | ",
"section_padding": 2,
"show_background": false
}
}
"#;
let config: Config =
serde_json::from_str(sample).expect("representative config should parse");
assert_eq!(config.sections.context.display_mode, "remaining");
assert_eq!(config.sections.quota.cache_ttl, 300);
assert!(!config.display.show_background);
}
}