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