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 prompt: None,
341 paths: Some(vec![PathBuf::from(".")]),
342 include: None,
343 ignore: None,
344 remote: None,
345 read_stdin: false,
346 output_file: None,
347 max_tokens: None,
348 llm_tool: LlmTool::default(),
349 quiet: false,
350 verbose: 0,
351 log_format: crate::cli::LogFormat::default(),
352 config: None,
353 progress: false,
354 copy: false,
355 enhanced_context: false,
356 trace_imports: false,
357 include_callers: false,
358 include_types: false,
359 semantic_depth: 3,
360 custom_priorities: vec![],
361 config_token_limits: None,
362 config_defaults_max_tokens: None,
363 };
364
365 config_file.apply_to_cli_config(&mut cli_config);
366
367 assert_eq!(cli_config.config_defaults_max_tokens, Some(75000));
368 assert_eq!(cli_config.llm_tool, LlmTool::Codex);
369 assert!(cli_config.progress);
370 assert_eq!(cli_config.verbose, 1);
371 assert_eq!(cli_config.get_directories(), vec![PathBuf::from("/tmp")]);
372 assert_eq!(cli_config.output_file, Some(PathBuf::from("output.md")));
373 }
374
375 #[test]
376 fn test_example_config_generation() {
377 let example = create_example_config();
378 assert!(example.contains("[defaults]"));
379 assert!(example.contains("max_tokens"));
380 assert!(example.contains("[tokens]"));
381 assert!(example.contains("gemini"));
382 assert!(example.contains("codex"));
383 assert!(example.contains("[[priorities]]"));
384 assert!(example.contains("pattern"));
385 assert!(example.contains("weight"));
386 }
387
388 #[test]
389 fn test_token_limits_parsing() {
390 let config_content = r#"
391[tokens]
392gemini = 2000000
393codex = 1500000
394
395[defaults]
396max_tokens = 100000
397"#;
398
399 let config: ConfigFile = toml::from_str(config_content).unwrap();
400 assert_eq!(config.tokens.gemini, Some(2_000_000));
401 assert_eq!(config.tokens.codex, Some(1_500_000));
402 assert_eq!(config.defaults.max_tokens, Some(100_000));
403 }
404
405 #[test]
406 fn test_token_limits_partial_parsing() {
407 let config_content = r#"
408[tokens]
409gemini = 3000000
410# codex not specified, should use default
411
412[defaults]
413max_tokens = 150000
414"#;
415
416 let config: ConfigFile = toml::from_str(config_content).unwrap();
417 assert_eq!(config.tokens.gemini, Some(3_000_000));
418 assert_eq!(config.tokens.codex, None);
419 }
420
421 #[test]
422 fn test_token_limits_empty_section() {
423 let config_content = r#"
424[tokens]
425# No limits specified
426
427[defaults]
428max_tokens = 200000
429"#;
430
431 let config: ConfigFile = toml::from_str(config_content).unwrap();
432 assert_eq!(config.tokens.gemini, None);
433 assert_eq!(config.tokens.codex, None);
434 }
435
436 #[test]
437 fn test_apply_to_cli_config_with_token_limits() {
438 let config_file = ConfigFile {
439 defaults: Defaults {
440 max_tokens: Some(75000),
441 llm_tool: Some("gemini".to_string()),
442 progress: true,
443 verbose: false,
444 quiet: false,
445 directory: None,
446 output_file: None,
447 },
448 tokens: TokenLimits {
449 gemini: Some(2_500_000),
450 codex: Some(1_800_000),
451 },
452 priorities: vec![],
453 ignore: vec![],
454 include: vec![],
455 };
456
457 let mut cli_config = CliConfig {
458 prompt: None,
459 paths: Some(vec![PathBuf::from(".")]),
460 include: None,
461 ignore: None,
462 remote: None,
463 read_stdin: false,
464 output_file: None,
465 max_tokens: None,
466 llm_tool: LlmTool::default(),
467 quiet: false,
468 verbose: 0,
469 log_format: crate::cli::LogFormat::default(),
470 config: None,
471 progress: false,
472 copy: false,
473 enhanced_context: false,
474 trace_imports: false,
475 include_callers: false,
476 include_types: false,
477 semantic_depth: 3,
478 custom_priorities: vec![],
479 config_token_limits: None,
480 config_defaults_max_tokens: None,
481 };
482
483 config_file.apply_to_cli_config(&mut cli_config);
484
485 assert_eq!(cli_config.config_defaults_max_tokens, Some(75000)); assert!(cli_config.config_token_limits.is_some());
488 let token_limits = cli_config.config_token_limits.as_ref().unwrap();
489 assert_eq!(token_limits.gemini, Some(2_500_000));
490 assert_eq!(token_limits.codex, Some(1_800_000));
491 }
492}