claude_code_status_line/
config.rs

1use crate::colors::{
2    SectionColors, CONTEXT_COLORS, COST_COLORS, CWD_COLORS, GIT_COLORS, MODEL_COLORS,
3    QUOTA_5H_COLORS, QUOTA_7D_COLORS,
4};
5use serde::{Deserialize, Serialize};
6use std::fs;
7use std::path::Path;
8
9#[derive(Debug, Deserialize, Serialize)]
10pub struct ThemeConfig {
11    pub separator: (u8, u8, u8), // Color for the separator character
12    pub cwd: SectionColors,
13    pub git: SectionColors,
14    pub model: SectionColors,
15    pub context: SectionColors,
16    pub quota_5h: SectionColors,
17    pub quota_7d: SectionColors,
18    pub cost: SectionColors,
19}
20
21impl Default for ThemeConfig {
22    fn default() -> Self {
23        ThemeConfig {
24            separator: (65, 65, 62), // Default to muted gray
25            cwd: CWD_COLORS,
26            git: GIT_COLORS,
27            model: MODEL_COLORS,
28            context: CONTEXT_COLORS,
29            quota_5h: QUOTA_5H_COLORS,
30            quota_7d: QUOTA_7D_COLORS,
31            cost: COST_COLORS,
32        }
33    }
34}
35
36// Section-specific configurations
37
38#[derive(Debug, Deserialize, Serialize)]
39#[serde(default)]
40pub struct CwdConfig {
41    pub enabled: bool,
42    pub full_path: bool,
43}
44
45impl Default for CwdConfig {
46    fn default() -> Self {
47        CwdConfig {
48            enabled: true,
49            full_path: true,
50        }
51    }
52}
53
54#[derive(Debug, Deserialize, Serialize)]
55#[serde(default)]
56pub struct GitConfig {
57    pub enabled: bool,
58}
59
60impl Default for GitConfig {
61    fn default() -> Self {
62        GitConfig { enabled: true }
63    }
64}
65
66#[derive(Debug, Deserialize, Serialize)]
67#[serde(default)]
68pub struct ModelConfig {
69    pub enabled: bool,
70}
71
72impl Default for ModelConfig {
73    fn default() -> Self {
74        ModelConfig { enabled: true }
75    }
76}
77
78#[derive(Debug, Deserialize, Serialize)]
79#[serde(default)]
80pub struct ContextConfig {
81    pub enabled: bool,
82    pub show_decimals: bool,
83    pub show_details: bool,
84    pub autocompact_buffer_size: u64,
85}
86
87impl Default for ContextConfig {
88    fn default() -> Self {
89        ContextConfig {
90            enabled: true,
91            show_decimals: false,
92            show_details: true,
93            autocompact_buffer_size: 45_000,
94        }
95    }
96}
97
98#[derive(Debug, Deserialize, Serialize)]
99#[serde(default)]
100pub struct QuotaConfig {
101    pub enabled: bool,
102    pub show_details: bool,
103    pub cache_ttl: u64,
104}
105
106impl Default for QuotaConfig {
107    fn default() -> Self {
108        QuotaConfig {
109            enabled: true,
110            show_details: true,
111            cache_ttl: 0,
112        }
113    }
114}
115
116#[derive(Debug, Default, Deserialize, Serialize)]
117#[serde(default)]
118pub struct CostConfig {
119    pub enabled: bool,
120}
121
122#[derive(Debug, Default, Deserialize, Serialize)]
123#[serde(default)]
124pub struct SectionsConfig {
125    pub cwd: CwdConfig,
126    pub git: GitConfig,
127    pub model: ModelConfig,
128    pub context: ContextConfig,
129    pub quota: QuotaConfig,
130    pub cost: CostConfig,
131}
132
133#[derive(Debug, Deserialize, Serialize)]
134#[serde(default)]
135pub struct DisplayConfig {
136    pub multiline: bool,
137    pub default_terminal_width: usize,
138    pub use_arrows: bool,
139    pub arrow: String,
140    pub separator: String,
141    pub section_padding: usize,
142    pub show_background: bool,
143}
144
145impl Default for DisplayConfig {
146    fn default() -> Self {
147        DisplayConfig {
148            multiline: true,
149            default_terminal_width: 120,
150            use_arrows: false,
151            arrow: "\u{E0B0}".to_string(),
152            separator: "".to_string(),
153            section_padding: 1,
154            show_background: true,
155        }
156    }
157}
158
159/// Configuration loaded from ~/.claude/statusline/settings.json
160#[derive(Debug, Default, Deserialize, Serialize)]
161#[serde(default)]
162pub struct Config {
163    pub sections: SectionsConfig,
164    pub display: DisplayConfig,
165    #[serde(skip)] // Don't load theme from settings.json
166    pub theme: ThemeConfig,
167}
168
169fn get_config_dir() -> Option<std::path::PathBuf> {
170    #[cfg(unix)]
171    let home = std::env::var_os("HOME")?;
172    #[cfg(windows)]
173    let home = std::env::var_os("USERPROFILE")?;
174
175    Some(
176        std::path::Path::new(&home)
177            .join(".claude")
178            .join("statusline"),
179    )
180}
181
182fn load_theme(dir: &Path) -> ThemeConfig {
183    let path = dir.join("colors.json");
184    if !path.exists() {
185        let theme = ThemeConfig::default();
186        if let Ok(json) = serde_json::to_string_pretty(&theme) {
187            let _ = fs::write(&path, json);
188        }
189        return theme;
190    }
191
192    match std::fs::read_to_string(&path) {
193        Ok(content) => serde_json::from_str(&content).unwrap_or_else(|e| {
194            eprintln!("statusline warning: invalid colors.json: {}", e);
195            ThemeConfig::default()
196        }),
197        Err(_) => ThemeConfig::default(),
198    }
199}
200
201pub fn load_config() -> Config {
202    let dir = match get_config_dir() {
203        Some(d) => d,
204        None => return Config::default(),
205    };
206
207    if !dir.exists() {
208        let _ = fs::create_dir_all(&dir);
209    }
210
211    let config_path = dir.join("settings.json");
212    if !config_path.exists() {
213        let config = Config::default();
214        if let Ok(json) = serde_json::to_string_pretty(&config) {
215            let _ = fs::write(&config_path, json);
216        }
217        // Also ensure theme is created
218        let mut final_config = config;
219        final_config.theme = load_theme(&dir);
220        return final_config;
221    }
222
223    let mut config = match std::fs::read_to_string(&config_path) {
224        Ok(content) => serde_json::from_str::<Config>(&content).unwrap_or_else(|e| {
225            eprintln!("statusline warning: invalid settings.json: {}", e);
226            Config::default()
227        }),
228        Err(_) => Config::default(),
229    };
230
231    // Load theme from separate file
232    config.theme = load_theme(&dir);
233    config
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_defaults() {
242        let config = Config::default();
243        assert_eq!(config.display.separator, "");
244        assert_eq!(config.display.section_padding, 1);
245        assert!(config.sections.cwd.enabled);
246        assert!(!config.sections.context.show_decimals);
247        assert!(config.sections.context.show_details);
248        assert_eq!(config.theme.cwd.bg, Some((217, 119, 87)));
249    }
250
251    #[test]
252    fn test_theme_deserialization() {
253        let json = r#"{
254            "separator": [255, 0, 0],
255            "cwd": { "bg": null, "fg": [20, 20, 20], "muted": [30, 30, 30] },
256            "git": { "bg": [40, 40, 40], "fg": [50, 50, 50], "muted": [60, 60, 60] },
257            "model": { "bg": [70, 70, 70], "fg": [80, 80, 80], "muted": [90, 90, 90] },
258            "context": { "bg": [100, 100, 100], "fg": [110, 110, 110], "muted": [120, 120, 120] },
259            "quota_5h": { "bg": [130, 130, 130], "fg": [140, 140, 140], "muted": [150, 150, 150] },
260            "quota_7d": { "bg": [160, 160, 160], "fg": [170, 170, 170], "muted": [180, 180, 180] },
261            "cost": { "bg": [190, 190, 190], "fg": [200, 200, 200], "muted": [210, 210, 210] }
262        }"#;
263
264        let theme: ThemeConfig = serde_json::from_str(json).unwrap();
265        assert_eq!(theme.separator, (255, 0, 0));
266        assert_eq!(theme.cwd.bg, None);
267        assert_eq!(theme.git.bg, Some((40, 40, 40)));
268    }
269}