1use crate::cli::{Config as CliConfig, LlmTool};
8use crate::utils::error::ContextCreatorError;
9use serde::{Deserialize, Serialize};
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone, Serialize, Deserialize, Default)]
14pub struct ConfigFile {
15 #[serde(default)]
17 pub defaults: Defaults,
18
19 #[serde(default)]
21 pub priorities: Vec<Priority>,
22
23 #[serde(default)]
25 pub ignore: Vec<String>,
26
27 #[serde(default)]
29 pub include: Vec<String>,
30
31 #[serde(default)]
33 pub tokens: TokenLimits,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, Default)]
38pub struct Defaults {
39 pub max_tokens: Option<usize>,
41
42 #[serde(default)]
44 pub llm_tool: Option<String>,
45
46 #[serde(default)]
48 pub progress: bool,
49
50 #[serde(default)]
52 pub verbose: bool,
53
54 #[serde(default)]
56 pub quiet: bool,
57
58 pub directory: Option<PathBuf>,
60
61 pub output_file: Option<PathBuf>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct Priority {
68 pub pattern: String,
70 pub weight: f32,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize, Default)]
76pub struct TokenLimits {
77 pub gemini: Option<usize>,
79 pub codex: Option<usize>,
81}
82
83impl ConfigFile {
84 pub fn load_from_file(path: &Path) -> Result<Self, ContextCreatorError> {
86 if !path.exists() {
87 return Err(ContextCreatorError::InvalidPath(format!(
88 "Configuration file does not exist: {}",
89 path.display()
90 )));
91 }
92
93 let content = std::fs::read_to_string(path).map_err(|e| {
94 ContextCreatorError::ConfigError(format!(
95 "Failed to read config file {}: {}",
96 path.display(),
97 e
98 ))
99 })?;
100
101 let config: ConfigFile = toml::from_str(&content).map_err(|e| {
102 ContextCreatorError::ConfigError(format!(
103 "Failed to parse config file {}: {}",
104 path.display(),
105 e
106 ))
107 })?;
108
109 Ok(config)
110 }
111
112 pub fn load_default() -> Result<Option<Self>, ContextCreatorError> {
114 let local_config = Path::new(".context-creator.toml");
116 if local_config.exists() {
117 return Ok(Some(Self::load_from_file(local_config)?));
118 }
119
120 let rc_config = Path::new(".contextrc.toml");
122 if rc_config.exists() {
123 return Ok(Some(Self::load_from_file(rc_config)?));
124 }
125
126 if let Some(home) = dirs::home_dir() {
128 let home_config = home.join(".context-creator.toml");
129 if home_config.exists() {
130 return Ok(Some(Self::load_from_file(&home_config)?));
131 }
132 }
133
134 Ok(None)
135 }
136
137 pub fn apply_to_cli_config(&self, cli_config: &mut CliConfig) {
139 cli_config.custom_priorities = self.priorities.clone();
141
142 cli_config.config_token_limits = Some(self.tokens.clone());
144
145 if cli_config.max_tokens.is_none() && self.defaults.max_tokens.is_some() {
147 cli_config.config_defaults_max_tokens = self.defaults.max_tokens;
148 }
149
150 if let Some(ref tool_str) = self.defaults.llm_tool {
151 if cli_config.llm_tool == LlmTool::default() {
153 match tool_str.as_str() {
154 "gemini" => cli_config.llm_tool = LlmTool::Gemini,
155 "codex" => cli_config.llm_tool = LlmTool::Codex,
156 _ => {} }
158 }
159 }
160
161 if !cli_config.progress && self.defaults.progress {
163 cli_config.progress = self.defaults.progress;
164 }
165
166 if cli_config.verbose == 0 && self.defaults.verbose {
167 cli_config.verbose = 1; }
169
170 if !cli_config.quiet && self.defaults.quiet {
171 cli_config.quiet = self.defaults.quiet;
172 }
173
174 let current_paths = cli_config.get_directories();
177 if current_paths.len() == 1
178 && current_paths[0] == PathBuf::from(".")
179 && self.defaults.directory.is_some()
180 && cli_config.remote.is_none()
181 {
182 cli_config.paths = Some(vec![self.defaults.directory.clone().unwrap()]);
183 }
184
185 if cli_config.output_file.is_none() && self.defaults.output_file.is_some() {
187 cli_config.output_file = self.defaults.output_file.clone();
188 }
189
190 if cli_config.ignore.is_none() && !self.ignore.is_empty() {
193 cli_config.ignore = Some(self.ignore.clone());
194 }
195
196 if cli_config.include.is_none() && !self.include.is_empty() {
199 cli_config.include = Some(self.include.clone());
200 }
201 }
202}
203
204pub fn create_example_config() -> String {
206 let example = ConfigFile {
207 defaults: Defaults {
208 max_tokens: Some(150000),
209 llm_tool: Some("gemini".to_string()),
210 progress: true,
211 verbose: false,
212 quiet: false,
213 directory: None,
214 output_file: None,
215 },
216 tokens: TokenLimits {
217 gemini: Some(2_000_000),
218 codex: Some(1_500_000),
219 },
220 priorities: vec![
221 Priority {
222 pattern: "src/**/*.rs".to_string(),
223 weight: 100.0,
224 },
225 Priority {
226 pattern: "src/main.rs".to_string(),
227 weight: 150.0,
228 },
229 Priority {
230 pattern: "tests/**/*.rs".to_string(),
231 weight: 50.0,
232 },
233 Priority {
234 pattern: "docs/**/*.md".to_string(),
235 weight: 30.0,
236 },
237 Priority {
238 pattern: "*.toml".to_string(),
239 weight: 80.0,
240 },
241 Priority {
242 pattern: "*.json".to_string(),
243 weight: 60.0,
244 },
245 ],
246 ignore: vec![
247 "target/**".to_string(),
248 "node_modules/**".to_string(),
249 "*.pyc".to_string(),
250 ".env".to_string(),
251 ],
252 include: vec!["!important/**".to_string()],
253 };
254
255 toml::to_string_pretty(&example)
256 .unwrap_or_else(|_| "# Failed to generate example config".to_string())
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use std::fs;
263 use tempfile::TempDir;
264
265 #[test]
266 fn test_config_file_parsing() {
267 let config_content = r#"
268ignore = [
269 "target/**",
270 "node_modules/**"
271]
272
273include = [
274 "!important/**"
275]
276
277[defaults]
278max_tokens = 100000
279llm_tool = "gemini"
280progress = true
281
282[[priorities]]
283pattern = "src/**/*.rs"
284weight = 100.0
285
286[[priorities]]
287pattern = "tests/**/*.rs"
288weight = 50.0
289"#;
290
291 let config: ConfigFile = toml::from_str(config_content).unwrap();
292
293 assert_eq!(config.defaults.max_tokens, Some(100000));
294 assert_eq!(config.defaults.llm_tool, Some("gemini".to_string()));
295 assert!(config.defaults.progress);
296 assert_eq!(config.priorities.len(), 2);
297 assert_eq!(config.priorities[0].pattern, "src/**/*.rs");
298 assert_eq!(config.priorities[0].weight, 100.0);
299 assert_eq!(config.ignore.len(), 2);
300 assert_eq!(config.include.len(), 1);
301 }
302
303 #[test]
304 fn test_config_file_loading() {
305 let temp_dir = TempDir::new().unwrap();
306 let config_path = temp_dir.path().join("config.toml");
307
308 let config_content = r#"
309[defaults]
310max_tokens = 50000
311progress = true
312"#;
313
314 fs::write(&config_path, config_content).unwrap();
315
316 let config = ConfigFile::load_from_file(&config_path).unwrap();
317 assert_eq!(config.defaults.max_tokens, Some(50000));
318 assert!(config.defaults.progress);
319 }
320
321 #[test]
322 fn test_apply_to_cli_config() {
323 let config_file = ConfigFile {
324 defaults: Defaults {
325 max_tokens: Some(75000),
326 llm_tool: Some("codex".to_string()),
327 progress: true,
328 verbose: true,
329 quiet: false,
330 directory: Some(PathBuf::from("/tmp")),
331 output_file: Some(PathBuf::from("output.md")),
332 },
333 tokens: TokenLimits::default(),
334 priorities: vec![],
335 ignore: vec![],
336 include: vec![],
337 };
338
339 let mut cli_config = CliConfig {
340 paths: Some(vec![PathBuf::from(".")]),
341 semantic_depth: 3,
342 ..CliConfig::default()
343 };
344
345 config_file.apply_to_cli_config(&mut cli_config);
346
347 assert_eq!(cli_config.config_defaults_max_tokens, Some(75000));
348 assert_eq!(cli_config.llm_tool, LlmTool::Codex);
349 assert!(cli_config.progress);
350 assert_eq!(cli_config.verbose, 1);
351 assert_eq!(cli_config.get_directories(), vec![PathBuf::from("/tmp")]);
352 assert_eq!(cli_config.output_file, Some(PathBuf::from("output.md")));
353 }
354
355 #[test]
356 fn test_example_config_generation() {
357 let example = create_example_config();
358 assert!(example.contains("[defaults]"));
359 assert!(example.contains("max_tokens"));
360 assert!(example.contains("[tokens]"));
361 assert!(example.contains("gemini"));
362 assert!(example.contains("codex"));
363 assert!(example.contains("[[priorities]]"));
364 assert!(example.contains("pattern"));
365 assert!(example.contains("weight"));
366 }
367
368 #[test]
369 fn test_token_limits_parsing() {
370 let config_content = r#"
371[tokens]
372gemini = 2000000
373codex = 1500000
374
375[defaults]
376max_tokens = 100000
377"#;
378
379 let config: ConfigFile = toml::from_str(config_content).unwrap();
380 assert_eq!(config.tokens.gemini, Some(2_000_000));
381 assert_eq!(config.tokens.codex, Some(1_500_000));
382 assert_eq!(config.defaults.max_tokens, Some(100_000));
383 }
384
385 #[test]
386 fn test_token_limits_partial_parsing() {
387 let config_content = r#"
388[tokens]
389gemini = 3000000
390# codex not specified, should use default
391
392[defaults]
393max_tokens = 150000
394"#;
395
396 let config: ConfigFile = toml::from_str(config_content).unwrap();
397 assert_eq!(config.tokens.gemini, Some(3_000_000));
398 assert_eq!(config.tokens.codex, None);
399 }
400
401 #[test]
402 fn test_token_limits_empty_section() {
403 let config_content = r#"
404[tokens]
405# No limits specified
406
407[defaults]
408max_tokens = 200000
409"#;
410
411 let config: ConfigFile = toml::from_str(config_content).unwrap();
412 assert_eq!(config.tokens.gemini, None);
413 assert_eq!(config.tokens.codex, None);
414 }
415
416 #[test]
417 fn test_apply_to_cli_config_with_token_limits() {
418 let config_file = ConfigFile {
419 defaults: Defaults {
420 max_tokens: Some(75000),
421 llm_tool: Some("gemini".to_string()),
422 progress: true,
423 verbose: false,
424 quiet: false,
425 directory: None,
426 output_file: None,
427 },
428 tokens: TokenLimits {
429 gemini: Some(2_500_000),
430 codex: Some(1_800_000),
431 },
432 priorities: vec![],
433 ignore: vec![],
434 include: vec![],
435 };
436
437 let mut cli_config = CliConfig {
438 paths: Some(vec![PathBuf::from(".")]),
439 semantic_depth: 3,
440 ..CliConfig::default()
441 };
442
443 config_file.apply_to_cli_config(&mut cli_config);
444
445 assert_eq!(cli_config.config_defaults_max_tokens, Some(75000)); assert!(cli_config.config_token_limits.is_some());
448 let token_limits = cli_config.config_token_limits.as_ref().unwrap();
449 assert_eq!(token_limits.gemini, Some(2_500_000));
450 assert_eq!(token_limits.codex, Some(1_800_000));
451 }
452}