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}
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#[derive(Debug, Default, Deserialize, Serialize)]
161#[serde(default)]
162pub struct Config {
163 pub sections: SectionsConfig,
164 pub display: DisplayConfig,
165 #[serde(skip)] 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 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 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}