1use crate::cli::{Config as CliConfig, LlmTool};
8use crate::utils::error::CodeDigestError;
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
32#[derive(Debug, Clone, Serialize, Deserialize, Default)]
34pub struct Defaults {
35 pub max_tokens: Option<usize>,
37
38 #[serde(default)]
40 pub llm_tool: Option<String>,
41
42 #[serde(default)]
44 pub progress: bool,
45
46 #[serde(default)]
48 pub verbose: bool,
49
50 #[serde(default)]
52 pub quiet: bool,
53
54 pub directory: Option<PathBuf>,
56
57 pub output_file: Option<PathBuf>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct Priority {
64 pub pattern: String,
66 pub weight: f32,
68}
69
70impl ConfigFile {
71 pub fn load_from_file(path: &Path) -> Result<Self, CodeDigestError> {
73 if !path.exists() {
74 return Err(CodeDigestError::InvalidPath(format!(
75 "Configuration file does not exist: {}",
76 path.display()
77 )));
78 }
79
80 let content = std::fs::read_to_string(path).map_err(|e| {
81 CodeDigestError::ConfigError(format!(
82 "Failed to read config file {}: {}",
83 path.display(),
84 e
85 ))
86 })?;
87
88 let config: ConfigFile = toml::from_str(&content).map_err(|e| {
89 CodeDigestError::ConfigError(format!(
90 "Failed to parse config file {}: {}",
91 path.display(),
92 e
93 ))
94 })?;
95
96 Ok(config)
97 }
98
99 pub fn load_default() -> Result<Option<Self>, CodeDigestError> {
101 let local_config = Path::new(".code-digest.toml");
103 if local_config.exists() {
104 return Ok(Some(Self::load_from_file(local_config)?));
105 }
106
107 let rc_config = Path::new(".digestrc.toml");
109 if rc_config.exists() {
110 return Ok(Some(Self::load_from_file(rc_config)?));
111 }
112
113 if let Some(home) = dirs::home_dir() {
115 let home_config = home.join(".code-digest.toml");
116 if home_config.exists() {
117 return Ok(Some(Self::load_from_file(&home_config)?));
118 }
119 }
120
121 Ok(None)
122 }
123
124 pub fn apply_to_cli_config(&self, cli_config: &mut CliConfig) {
126 cli_config.custom_priorities = self.priorities.clone();
128
129 if cli_config.max_tokens.is_none() && self.defaults.max_tokens.is_some() {
131 cli_config.max_tokens = self.defaults.max_tokens;
132 }
133
134 if let Some(ref tool_str) = self.defaults.llm_tool {
135 if cli_config.llm_tool == LlmTool::default() {
137 match tool_str.as_str() {
138 "gemini" => cli_config.llm_tool = LlmTool::Gemini,
139 "codex" => cli_config.llm_tool = LlmTool::Codex,
140 _ => {} }
142 }
143 }
144
145 if !cli_config.progress && self.defaults.progress {
147 cli_config.progress = self.defaults.progress;
148 }
149
150 if !cli_config.verbose && self.defaults.verbose {
151 cli_config.verbose = self.defaults.verbose;
152 }
153
154 if !cli_config.quiet && self.defaults.quiet {
155 cli_config.quiet = self.defaults.quiet;
156 }
157
158 let current_paths = cli_config.get_directories();
160 if current_paths.len() == 1
161 && current_paths[0] == PathBuf::from(".")
162 && self.defaults.directory.is_some()
163 {
164 cli_config.paths = Some(vec![self.defaults.directory.clone().unwrap()]);
165 }
166
167 if cli_config.output_file.is_none() && self.defaults.output_file.is_some() {
169 cli_config.output_file = self.defaults.output_file.clone();
170 }
171 }
172}
173
174pub fn create_example_config() -> String {
176 let example = ConfigFile {
177 defaults: Defaults {
178 max_tokens: Some(150000),
179 llm_tool: Some("gemini".to_string()),
180 progress: true,
181 verbose: false,
182 quiet: false,
183 directory: None,
184 output_file: None,
185 },
186 priorities: vec![
187 Priority { pattern: "src/**/*.rs".to_string(), weight: 100.0 },
188 Priority { pattern: "src/main.rs".to_string(), weight: 150.0 },
189 Priority { pattern: "tests/**/*.rs".to_string(), weight: 50.0 },
190 Priority { pattern: "docs/**/*.md".to_string(), weight: 30.0 },
191 Priority { pattern: "*.toml".to_string(), weight: 80.0 },
192 Priority { pattern: "*.json".to_string(), weight: 60.0 },
193 ],
194 ignore: vec![
195 "target/**".to_string(),
196 "node_modules/**".to_string(),
197 "*.pyc".to_string(),
198 ".env".to_string(),
199 ],
200 include: vec!["!important/**".to_string()],
201 };
202
203 toml::to_string_pretty(&example)
204 .unwrap_or_else(|_| "# Failed to generate example config".to_string())
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use std::fs;
211 use tempfile::TempDir;
212
213 #[test]
214 fn test_config_file_parsing() {
215 let config_content = r#"
216ignore = [
217 "target/**",
218 "node_modules/**"
219]
220
221include = [
222 "!important/**"
223]
224
225[defaults]
226max_tokens = 100000
227llm_tool = "gemini"
228progress = true
229
230[[priorities]]
231pattern = "src/**/*.rs"
232weight = 100.0
233
234[[priorities]]
235pattern = "tests/**/*.rs"
236weight = 50.0
237"#;
238
239 let config: ConfigFile = toml::from_str(config_content).unwrap();
240
241 assert_eq!(config.defaults.max_tokens, Some(100000));
242 assert_eq!(config.defaults.llm_tool, Some("gemini".to_string()));
243 assert!(config.defaults.progress);
244 assert_eq!(config.priorities.len(), 2);
245 assert_eq!(config.priorities[0].pattern, "src/**/*.rs");
246 assert_eq!(config.priorities[0].weight, 100.0);
247 assert_eq!(config.ignore.len(), 2);
248 assert_eq!(config.include.len(), 1);
249 }
250
251 #[test]
252 fn test_config_file_loading() {
253 let temp_dir = TempDir::new().unwrap();
254 let config_path = temp_dir.path().join("config.toml");
255
256 let config_content = r#"
257[defaults]
258max_tokens = 50000
259progress = true
260"#;
261
262 fs::write(&config_path, config_content).unwrap();
263
264 let config = ConfigFile::load_from_file(&config_path).unwrap();
265 assert_eq!(config.defaults.max_tokens, Some(50000));
266 assert!(config.defaults.progress);
267 }
268
269 #[test]
270 fn test_apply_to_cli_config() {
271 let config_file = ConfigFile {
272 defaults: Defaults {
273 max_tokens: Some(75000),
274 llm_tool: Some("codex".to_string()),
275 progress: true,
276 verbose: true,
277 quiet: false,
278 directory: Some(PathBuf::from("/tmp")),
279 output_file: Some(PathBuf::from("output.md")),
280 },
281 priorities: vec![],
282 ignore: vec![],
283 include: vec![],
284 };
285
286 let mut cli_config = CliConfig {
287 prompt: None,
288 paths: Some(vec![PathBuf::from(".")]),
289 repo: None,
290 read_stdin: false,
291 output_file: None,
292 max_tokens: None,
293 llm_tool: LlmTool::default(),
294 quiet: false,
295 verbose: false,
296 config: None,
297 progress: false,
298 copy: false,
299 enhanced_context: false,
300 custom_priorities: vec![],
301 };
302
303 config_file.apply_to_cli_config(&mut cli_config);
304
305 assert_eq!(cli_config.max_tokens, Some(75000));
306 assert_eq!(cli_config.llm_tool, LlmTool::Codex);
307 assert!(cli_config.progress);
308 assert!(cli_config.verbose);
309 assert_eq!(cli_config.get_directories(), vec![PathBuf::from("/tmp")]);
310 assert_eq!(cli_config.output_file, Some(PathBuf::from("output.md")));
311 }
312
313 #[test]
314 fn test_example_config_generation() {
315 let example = create_example_config();
316 assert!(example.contains("[defaults]"));
317 assert!(example.contains("max_tokens"));
318 assert!(example.contains("[[priorities]]"));
319 assert!(example.contains("pattern"));
320 assert!(example.contains("weight"));
321 }
322}