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    pub show_username: bool,
44}
45
46impl Default for CwdConfig {
47    fn default() -> Self {
48        CwdConfig {
49            enabled: true,
50            full_path: true,
51            show_username: true,
52        }
53    }
54}
55
56#[derive(Debug, Deserialize, Serialize)]
57#[serde(default)]
58pub struct GitConfig {
59    pub enabled: bool,
60    pub show_repo_name: bool,
61    pub show_diff_stats: bool,
62}
63
64impl Default for GitConfig {
65    fn default() -> Self {
66        GitConfig {
67            enabled: true,
68            show_repo_name: true,
69            show_diff_stats: true,
70        }
71    }
72}
73
74#[derive(Debug, Deserialize, Serialize)]
75#[serde(default)]
76pub struct ModelConfig {
77    pub enabled: bool,
78    pub show_output_style: bool,
79    pub show_thinking_mode: bool,
80}
81
82impl Default for ModelConfig {
83    fn default() -> Self {
84        ModelConfig {
85            enabled: true,
86            show_output_style: false,
87            show_thinking_mode: true,
88        }
89    }
90}
91
92#[derive(Debug, Deserialize, Serialize)]
93#[serde(default)]
94pub struct ContextConfig {
95    pub enabled: bool,
96    pub show_decimals: bool,
97    pub show_token_counts: bool,
98    pub display_mode: String,
99}
100
101impl Default for ContextConfig {
102    fn default() -> Self {
103        ContextConfig {
104            enabled: true,
105            show_decimals: false,
106            show_token_counts: true,
107            display_mode: "used".to_string(),
108        }
109    }
110}
111
112#[derive(Debug, Deserialize, Serialize)]
113#[serde(default)]
114pub struct QuotaConfig {
115    pub enabled: bool,
116    pub show_time_remaining: bool,
117    pub cache_ttl: u64,
118}
119
120impl Default for QuotaConfig {
121    fn default() -> Self {
122        QuotaConfig {
123            enabled: true,
124            show_time_remaining: true,
125            cache_ttl: 0,
126        }
127    }
128}
129
130#[derive(Debug, Deserialize, Serialize)]
131#[serde(default)]
132pub struct CostConfig {
133    pub enabled: bool,
134    pub show_durations: bool,
135}
136
137impl Default for CostConfig {
138    fn default() -> Self {
139        CostConfig {
140            enabled: false,
141            show_durations: true,
142        }
143    }
144}
145
146#[derive(Debug, Default, Deserialize, Serialize)]
147#[serde(default)]
148pub struct SectionsConfig {
149    pub cwd: CwdConfig,
150    pub git: GitConfig,
151    pub model: ModelConfig,
152    pub context: ContextConfig,
153    pub quota: QuotaConfig,
154    pub cost: CostConfig,
155}
156
157/// Powerline arrow character (hardcoded, not user-configurable)
158pub const POWERLINE_ARROW: &str = "\u{E0B0}";
159
160#[derive(Debug, Deserialize, Serialize)]
161#[serde(default)]
162pub struct DisplayConfig {
163    pub multiline: bool,
164    pub default_terminal_width: usize,
165    pub use_powerline: bool,
166    #[serde(skip)] // Arrow is hardcoded, not configurable
167    pub arrow: String,
168    pub segment_separator: String,
169    pub details_separator: String,
170    pub section_padding: usize,
171    pub show_background: bool,
172}
173
174impl Default for DisplayConfig {
175    fn default() -> Self {
176        DisplayConfig {
177            multiline: true,
178            default_terminal_width: 120,
179            use_powerline: false,
180            arrow: POWERLINE_ARROW.to_string(),
181            segment_separator: "".to_string(),
182            details_separator: ", ".to_string(),
183            section_padding: 1,
184            show_background: true,
185        }
186    }
187}
188
189/// Configuration loaded from ~/.claude/statusline/settings.json
190#[derive(Debug, Default, Deserialize, Serialize)]
191#[serde(default)]
192pub struct Config {
193    pub sections: SectionsConfig,
194    pub display: DisplayConfig,
195    #[serde(skip)] // Don't load theme from settings.json
196    pub theme: ThemeConfig,
197}
198
199fn get_config_dir() -> Option<std::path::PathBuf> {
200    Some(
201        crate::utils::get_home_dir()?
202            .join(".claude")
203            .join("statusline"),
204    )
205}
206
207fn load_theme(dir: &Path) -> ThemeConfig {
208    let path = dir.join("colors.json");
209    if !path.exists() {
210        let theme = ThemeConfig::default();
211        if let Ok(json) = serde_json::to_string_pretty(&theme) {
212            let _ = fs::write(&path, json);
213        }
214        return theme;
215    }
216
217    match std::fs::read_to_string(&path) {
218        Ok(content) => serde_json::from_str(&content).unwrap_or_else(|e| {
219            eprintln!("statusline warning: invalid colors.json: {}", e);
220            ThemeConfig::default()
221        }),
222        Err(_) => ThemeConfig::default(),
223    }
224}
225
226pub fn load_config() -> Config {
227    let dir = match get_config_dir() {
228        Some(d) => d,
229        None => return Config::default(),
230    };
231
232    if !dir.exists() {
233        let _ = fs::create_dir_all(&dir);
234    }
235
236    let config_path = dir.join("settings.json");
237    if !config_path.exists() {
238        let config = Config::default();
239        if let Ok(json) = serde_json::to_string_pretty(&config) {
240            let _ = fs::write(&config_path, json);
241        }
242        // Also ensure theme is created
243        let mut final_config = config;
244        final_config.theme = load_theme(&dir);
245        return final_config;
246    }
247
248    let mut config = match std::fs::read_to_string(&config_path) {
249        Ok(content) => serde_json::from_str::<Config>(&content).unwrap_or_else(|e| {
250            eprintln!("statusline warning: invalid settings.json: {}", e);
251            Config::default()
252        }),
253        Err(_) => Config::default(),
254    };
255
256    // Load theme from separate file
257    config.theme = load_theme(&dir);
258    config
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_defaults() {
267        let config = Config::default();
268        assert_eq!(config.display.segment_separator, "");
269        assert_eq!(config.display.details_separator, ", ");
270        assert_eq!(config.display.section_padding, 1);
271        assert!(config.sections.cwd.enabled);
272        assert!(!config.sections.context.show_decimals);
273        assert!(config.sections.context.show_token_counts);
274        assert_eq!(config.theme.cwd.background, Some((217, 119, 87)));
275    }
276
277    #[test]
278    fn test_theme_deserialization() {
279        let json = r#"{
280            "separator": [255, 0, 0],
281            "cwd": { "background": null, "foreground": [20, 20, 20], "details": [30, 30, 30] },
282            "git": { "background": [40, 40, 40], "foreground": [50, 50, 50], "details": [60, 60, 60] },
283            "model": { "background": [70, 70, 70], "foreground": [80, 80, 80], "details": [90, 90, 90] },
284            "context": { "background": [100, 100, 100], "foreground": [110, 110, 110], "details": [120, 120, 120] },
285            "quota_5h": { "background": [130, 130, 130], "foreground": [140, 140, 140], "details": [150, 150, 150] },
286            "quota_7d": { "background": [160, 160, 160], "foreground": [170, 170, 170], "details": [180, 180, 180] },
287            "cost": { "background": [190, 190, 190], "foreground": [200, 200, 200], "details": [210, 210, 210] }
288        }"#;
289
290        let theme: ThemeConfig = serde_json::from_str(json).unwrap();
291        assert_eq!(theme.separator, (255, 0, 0));
292        assert_eq!(theme.cwd.background, None);
293        assert_eq!(theme.git.background, Some((40, 40, 40)));
294    }
295}