use crate::cli::{Config as CliConfig, LlmTool};
use crate::utils::error::CodeDigestError;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ConfigFile {
#[serde(default)]
pub defaults: Defaults,
#[serde(default)]
pub priorities: Vec<Priority>,
#[serde(default)]
pub ignore: Vec<String>,
#[serde(default)]
pub include: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Defaults {
pub max_tokens: Option<usize>,
#[serde(default)]
pub llm_tool: Option<String>,
#[serde(default)]
pub progress: bool,
#[serde(default)]
pub verbose: bool,
#[serde(default)]
pub quiet: bool,
pub directory: Option<PathBuf>,
pub output_file: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Priority {
pub pattern: String,
pub weight: f32,
}
impl ConfigFile {
pub fn load_from_file(path: &Path) -> Result<Self, CodeDigestError> {
if !path.exists() {
return Err(CodeDigestError::InvalidPath(format!(
"Configuration file does not exist: {}",
path.display()
)));
}
let content = std::fs::read_to_string(path).map_err(|e| {
CodeDigestError::ConfigError(format!(
"Failed to read config file {}: {}",
path.display(),
e
))
})?;
let config: ConfigFile = toml::from_str(&content).map_err(|e| {
CodeDigestError::ConfigError(format!(
"Failed to parse config file {}: {}",
path.display(),
e
))
})?;
Ok(config)
}
pub fn load_default() -> Result<Option<Self>, CodeDigestError> {
let local_config = Path::new(".code-digest.toml");
if local_config.exists() {
return Ok(Some(Self::load_from_file(local_config)?));
}
let rc_config = Path::new(".digestrc.toml");
if rc_config.exists() {
return Ok(Some(Self::load_from_file(rc_config)?));
}
if let Some(home) = dirs::home_dir() {
let home_config = home.join(".code-digest.toml");
if home_config.exists() {
return Ok(Some(Self::load_from_file(&home_config)?));
}
}
Ok(None)
}
pub fn apply_to_cli_config(&self, cli_config: &mut CliConfig) {
cli_config.custom_priorities = self.priorities.clone();
if cli_config.max_tokens.is_none() && self.defaults.max_tokens.is_some() {
cli_config.max_tokens = self.defaults.max_tokens;
}
if let Some(ref tool_str) = self.defaults.llm_tool {
if cli_config.llm_tool == LlmTool::default() {
match tool_str.as_str() {
"gemini" => cli_config.llm_tool = LlmTool::Gemini,
"codex" => cli_config.llm_tool = LlmTool::Codex,
_ => {} }
}
}
if !cli_config.progress && self.defaults.progress {
cli_config.progress = self.defaults.progress;
}
if !cli_config.verbose && self.defaults.verbose {
cli_config.verbose = self.defaults.verbose;
}
if !cli_config.quiet && self.defaults.quiet {
cli_config.quiet = self.defaults.quiet;
}
let current_paths = cli_config.get_directories();
if current_paths.len() == 1
&& current_paths[0] == PathBuf::from(".")
&& self.defaults.directory.is_some()
{
cli_config.paths = Some(vec![self.defaults.directory.clone().unwrap()]);
}
if cli_config.output_file.is_none() && self.defaults.output_file.is_some() {
cli_config.output_file = self.defaults.output_file.clone();
}
}
}
pub fn create_example_config() -> String {
let example = ConfigFile {
defaults: Defaults {
max_tokens: Some(150000),
llm_tool: Some("gemini".to_string()),
progress: true,
verbose: false,
quiet: false,
directory: None,
output_file: None,
},
priorities: vec![
Priority { pattern: "src/**/*.rs".to_string(), weight: 100.0 },
Priority { pattern: "src/main.rs".to_string(), weight: 150.0 },
Priority { pattern: "tests/**/*.rs".to_string(), weight: 50.0 },
Priority { pattern: "docs/**/*.md".to_string(), weight: 30.0 },
Priority { pattern: "*.toml".to_string(), weight: 80.0 },
Priority { pattern: "*.json".to_string(), weight: 60.0 },
],
ignore: vec![
"target/**".to_string(),
"node_modules/**".to_string(),
"*.pyc".to_string(),
".env".to_string(),
],
include: vec!["!important/**".to_string()],
};
toml::to_string_pretty(&example)
.unwrap_or_else(|_| "# Failed to generate example config".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_config_file_parsing() {
let config_content = r#"
ignore = [
"target/**",
"node_modules/**"
]
include = [
"!important/**"
]
[defaults]
max_tokens = 100000
llm_tool = "gemini"
progress = true
[[priorities]]
pattern = "src/**/*.rs"
weight = 100.0
[[priorities]]
pattern = "tests/**/*.rs"
weight = 50.0
"#;
let config: ConfigFile = toml::from_str(config_content).unwrap();
assert_eq!(config.defaults.max_tokens, Some(100000));
assert_eq!(config.defaults.llm_tool, Some("gemini".to_string()));
assert!(config.defaults.progress);
assert_eq!(config.priorities.len(), 2);
assert_eq!(config.priorities[0].pattern, "src/**/*.rs");
assert_eq!(config.priorities[0].weight, 100.0);
assert_eq!(config.ignore.len(), 2);
assert_eq!(config.include.len(), 1);
}
#[test]
fn test_config_file_loading() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.toml");
let config_content = r#"
[defaults]
max_tokens = 50000
progress = true
"#;
fs::write(&config_path, config_content).unwrap();
let config = ConfigFile::load_from_file(&config_path).unwrap();
assert_eq!(config.defaults.max_tokens, Some(50000));
assert!(config.defaults.progress);
}
#[test]
fn test_apply_to_cli_config() {
let config_file = ConfigFile {
defaults: Defaults {
max_tokens: Some(75000),
llm_tool: Some("codex".to_string()),
progress: true,
verbose: true,
quiet: false,
directory: Some(PathBuf::from("/tmp")),
output_file: Some(PathBuf::from("output.md")),
},
priorities: vec![],
ignore: vec![],
include: vec![],
};
let mut cli_config = CliConfig {
prompt: None,
paths: Some(vec![PathBuf::from(".")]),
repo: None,
read_stdin: false,
output_file: None,
max_tokens: None,
llm_tool: LlmTool::default(),
quiet: false,
verbose: false,
config: None,
progress: false,
copy: false,
enhanced_context: false,
custom_priorities: vec![],
};
config_file.apply_to_cli_config(&mut cli_config);
assert_eq!(cli_config.max_tokens, Some(75000));
assert_eq!(cli_config.llm_tool, LlmTool::Codex);
assert!(cli_config.progress);
assert!(cli_config.verbose);
assert_eq!(cli_config.get_directories(), vec![PathBuf::from("/tmp")]);
assert_eq!(cli_config.output_file, Some(PathBuf::from("output.md")));
}
#[test]
fn test_example_config_generation() {
let example = create_example_config();
assert!(example.contains("[defaults]"));
assert!(example.contains("max_tokens"));
assert!(example.contains("[[priorities]]"));
assert!(example.contains("pattern"));
assert!(example.contains("weight"));
}
}