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), 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), 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#[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: false,
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: false,
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: 60,
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
157pub 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)] 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#[derive(Debug, Default, Deserialize, Serialize)]
191#[serde(default)]
192pub struct Config {
193 pub sections: SectionsConfig,
194 pub display: DisplayConfig,
195 #[serde(skip)] 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 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 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.cwd.show_username);
273 assert!(!config.sections.git.show_repo_name);
274 assert_eq!(config.sections.quota.cache_ttl, 60);
275 assert!(!config.sections.context.show_decimals);
276 assert!(config.sections.context.show_token_counts);
277 assert_eq!(config.theme.cwd.background, Some((217, 119, 87)));
278 }
279
280 #[test]
281 fn test_theme_deserialization() {
282 let json = r#"{
283 "separator": [255, 0, 0],
284 "cwd": { "background": null, "foreground": [20, 20, 20], "details": [30, 30, 30] },
285 "git": { "background": [40, 40, 40], "foreground": [50, 50, 50], "details": [60, 60, 60] },
286 "model": { "background": [70, 70, 70], "foreground": [80, 80, 80], "details": [90, 90, 90] },
287 "context": { "background": [100, 100, 100], "foreground": [110, 110, 110], "details": [120, 120, 120] },
288 "quota_5h": { "background": [130, 130, 130], "foreground": [140, 140, 140], "details": [150, 150, 150] },
289 "quota_7d": { "background": [160, 160, 160], "foreground": [170, 170, 170], "details": [180, 180, 180] },
290 "cost": { "background": [190, 190, 190], "foreground": [200, 200, 200], "details": [210, 210, 210] }
291 }"#;
292
293 let theme: ThemeConfig = serde_json::from_str(json).unwrap();
294 assert_eq!(theme.separator, (255, 0, 0));
295 assert_eq!(theme.cwd.background, None);
296 assert_eq!(theme.git.background, Some((40, 40, 40)));
297 }
298
299 #[test]
300 fn test_root_settings_sample_deserializes() {
301 let sample = include_str!("../settings.json");
302 let config: Config = serde_json::from_str(sample).expect("root settings.json should parse");
303
304 assert_eq!(config.sections.quota.cache_ttl, 60);
305 assert_eq!(config.sections.context.display_mode, "used");
306 assert!(!config.sections.cwd.show_username);
307 }
308
309 #[test]
310 fn test_representative_config_snippet_deserializes() {
311 let sample = r#"
312 {
313 "sections": {
314 "cwd": { "enabled": true, "full_path": false, "show_username": true },
315 "git": { "enabled": true, "show_repo_name": true, "show_diff_stats": false },
316 "model": { "enabled": true, "show_output_style": true, "show_thinking_mode": true },
317 "context": { "enabled": true, "show_decimals": true, "show_token_counts": true, "display_mode": "remaining" },
318 "quota": { "enabled": true, "show_time_remaining": false, "cache_ttl": 300 },
319 "cost": { "enabled": true, "show_durations": false }
320 },
321 "display": {
322 "multiline": false,
323 "default_terminal_width": 140,
324 "use_powerline": false,
325 "segment_separator": " | ",
326 "details_separator": " | ",
327 "section_padding": 2,
328 "show_background": false
329 }
330 }
331 "#;
332
333 let config: Config =
334 serde_json::from_str(sample).expect("representative config should parse");
335
336 assert_eq!(config.sections.context.display_mode, "remaining");
337 assert_eq!(config.sections.quota.cache_ttl, 300);
338 assert!(!config.display.show_background);
339 }
340}