use std::path::Path;
use crate::cli_args::FuzzyMode;
use crate::error::AtomwriteError;
#[derive(Debug, Default, serde::Deserialize)]
#[serde(default)]
pub struct AtomwriteConfig {
pub defaults: DefaultsSection,
pub fuzzy: FuzzySection,
pub search: SearchSection,
}
#[derive(Debug, serde::Deserialize)]
#[serde(default)]
pub struct DefaultsSection {
pub backup: bool,
pub retention: u8,
pub line_ending: String,
pub max_filesize: u64,
}
impl Default for DefaultsSection {
fn default() -> Self {
Self {
backup: true,
retention: crate::constants::DEFAULT_BACKUP_RETENTION,
line_ending: "auto".into(),
max_filesize: 1_073_741_824,
}
}
}
#[derive(Debug, serde::Deserialize)]
#[serde(default)]
pub struct FuzzySection {
pub mode: String,
pub threshold: f64,
}
impl Default for FuzzySection {
fn default() -> Self {
Self {
mode: "auto".into(),
threshold: 0.70,
}
}
}
#[derive(Debug, serde::Deserialize)]
#[serde(default)]
pub struct SearchSection {
pub context: u32,
pub smart_case: bool,
}
impl Default for SearchSection {
fn default() -> Self {
Self {
context: 0,
smart_case: true,
}
}
}
pub fn load_config(workspace: &Path, explicit_path: Option<&Path>) -> AtomwriteConfig {
if let Some(path) = explicit_path {
return load_from_path(path);
}
let local = workspace.join(".atomwrite.toml");
if local.is_file() {
return load_from_path(&local);
}
if let Some(proj_dirs) = directories::ProjectDirs::from("", "", "atomwrite") {
let global = proj_dirs.config_dir().join("config.toml");
if global.is_file() {
return load_from_path(&global);
}
}
AtomwriteConfig::default()
}
fn load_from_path(path: &Path) -> AtomwriteConfig {
match std::fs::read_to_string(path) {
Ok(content) => match toml::from_str(&content) {
Ok(config) => {
tracing::debug!(path = %path.display(), "loaded config");
config
}
Err(e) => {
tracing::warn!(
path = %path.display(),
error = %e,
"malformed config; using defaults"
);
AtomwriteConfig::default()
}
},
Err(e) => {
tracing::warn!(
path = %path.display(),
error = %e,
"cannot read config; using defaults"
);
AtomwriteConfig::default()
}
}
}
pub fn validate_fuzzy(cfg: &FuzzySection) -> Result<(), AtomwriteError> {
let mode = cfg.mode.trim().to_ascii_lowercase();
match mode.as_str() {
"auto" | "aggressive" => {}
"off" | "exact" | "disabled" | "false" | "0" => {
return Err(AtomwriteError::InvalidInput {
reason: format!(
"config [fuzzy] mode = \"{}\" is not allowed since v0.1.30; use \"auto\" or \"aggressive\"",
cfg.mode
),
});
}
other => {
return Err(AtomwriteError::InvalidInput {
reason: format!(
"config [fuzzy] mode = \"{other}\" is invalid; use \"auto\" or \"aggressive\""
),
});
}
}
if !(0.0..=1.0).contains(&cfg.threshold) {
return Err(AtomwriteError::InvalidInput {
reason: format!(
"config [fuzzy] threshold = {} is out of range; must be between 0.0 and 1.0",
cfg.threshold
),
});
}
Ok(())
}
pub fn resolve_fuzzy(
cli_mode: FuzzyMode,
cli_threshold: Option<f64>,
cfg: &FuzzySection,
) -> Result<(FuzzyMode, Option<f64>), AtomwriteError> {
validate_fuzzy(cfg)?;
if matches!(cli_mode, FuzzyMode::Off) {
return Err(AtomwriteError::InvalidInput {
reason: "fuzzy mode 'off' was removed in v0.1.30; use auto (default) or aggressive"
.into(),
});
}
let cfg_mode = match cfg.mode.trim().to_ascii_lowercase().as_str() {
"aggressive" => FuzzyMode::Aggressive,
_ => FuzzyMode::Auto,
};
let mode = match cli_mode {
FuzzyMode::Aggressive => FuzzyMode::Aggressive,
FuzzyMode::Auto => cfg_mode,
FuzzyMode::Off => FuzzyMode::Auto, };
let threshold = match cli_threshold {
Some(t) => Some(t),
None if (cfg.threshold - 0.70).abs() > f64::EPSILON => Some(cfg.threshold),
None => None,
};
Ok((mode, threshold))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn missing_file_returns_defaults() {
let dir = tempdir().unwrap();
let config = load_config(dir.path(), None);
assert!(config.defaults.backup);
assert_eq!(config.defaults.retention, 5);
assert_eq!(config.fuzzy.mode, "auto");
}
#[test]
fn local_config_overrides_defaults() {
let dir = tempdir().unwrap();
std::fs::write(
dir.path().join(".atomwrite.toml"),
"[defaults]\nbackup = false\nretention = 3\n",
)
.unwrap();
let config = load_config(dir.path(), None);
assert!(!config.defaults.backup);
assert_eq!(config.defaults.retention, 3);
}
#[test]
fn malformed_toml_warns_uses_defaults() {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join(".atomwrite.toml"), "not valid { toml").unwrap();
let config = load_config(dir.path(), None);
assert!(config.defaults.backup);
}
#[test]
fn explicit_path_takes_precedence() {
let dir = tempdir().unwrap();
let custom = dir.path().join("custom.toml");
std::fs::write(&custom, "[fuzzy]\nthreshold = 0.95\n").unwrap();
let config = load_config(dir.path(), Some(&custom));
assert!((config.fuzzy.threshold - 0.95).abs() < f64::EPSILON);
}
}