//! Optional configuration in `~/.config/drae/config.toml`. //! //! Everything has a sensible default: the file is only needed to change //! something. Command-line flags always win over it. The keys are in Spanish //! because the users are Spanish speakers. //! //! ```toml //! fuente = "auto" # auto | dle | rae-api //! timeout = 15 //! # cache = "~/.cache/drae/entries.db" //! # diccionario = "/usr/share/hunspell/es_AR.dic" //! ``` use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use serde::Deserialize; use crate::cache::Cache; #[derive(Debug, Clone, Default, Deserialize)] #[serde(deny_unknown_fields)] pub struct ConfigFile { /// Where to read from: `auto` (rae-api with fallback), `dle` or `rae-api`. #[serde(rename = "fuente")] pub source: Option, /// Seconds to wait for each request. pub timeout: Option, /// Cache database. pub cache: Option, /// Hunspell `.dic` used for suggestions. #[serde(rename = "diccionario")] pub dictionary: Option, /// Fixed output width; defaults to the terminal width. #[serde(rename = "ancho")] pub width: Option, } #[derive(Debug, Clone)] pub struct Config { pub source: String, pub timeout: u64, pub cache: PathBuf, pub dictionary: Option, pub width: Option, } impl Default for Config { fn default() -> Self { Config { source: "auto".into(), timeout: 15, cache: Cache::default_path(), dictionary: None, width: None, } } } pub fn default_path() -> PathBuf { let base = std::env::var_os("XDG_CONFIG_HOME") .map(PathBuf::from) .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config"))) .unwrap_or_else(|| PathBuf::from(".config")); base.join("drae").join("config.toml") } /// Loads the file if it exists. A missing file is not an error. pub fn load(explicit: Option<&Path>) -> Result { let path = explicit.map(PathBuf::from).unwrap_or_else(default_path); let mut cfg = Config::default(); let Ok(raw) = std::fs::read_to_string(&path) else { return Ok(cfg); }; let file: ConfigFile = toml::from_str(&raw) .with_context(|| format!("no se pudo leer la configuración {}", path.display()))?; if let Some(s) = file.source { cfg.source = s; } if let Some(t) = file.timeout { cfg.timeout = t; } if let Some(c) = file.cache { cfg.cache = expand_tilde(&c); } if let Some(d) = file.dictionary { cfg.dictionary = Some(expand_tilde(&d)); } cfg.width = file.width.or(cfg.width); Ok(cfg) } fn expand_tilde(s: &str) -> PathBuf { match s.strip_prefix("~/") { Some(rest) => match std::env::var_os("HOME") { Some(home) => PathBuf::from(home).join(rest), None => PathBuf::from(s), }, None => PathBuf::from(s), } } #[cfg(test)] mod tests { use super::*; #[test] fn missing_file_yields_defaults() { let cfg = load(Some(Path::new("/does/not/exist/config.toml"))).unwrap(); assert_eq!(cfg.source, "auto"); assert_eq!(cfg.timeout, 15); } #[test] fn parses_a_config_file() { let dir = std::env::temp_dir().join(format!("drae-cfg-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let p = dir.join("config.toml"); std::fs::write(&p, "fuente = \"dle\"\ntimeout = 30\nancho = 72\n").unwrap(); let cfg = load(Some(&p)).unwrap(); assert_eq!(cfg.source, "dle"); assert_eq!(cfg.timeout, 30); assert_eq!(cfg.width, Some(72)); std::fs::remove_dir_all(&dir).ok(); } #[test] fn expands_a_leading_tilde() { std::env::set_var("HOME", "/home/test"); assert_eq!(expand_tilde("~/x/y.db"), PathBuf::from("/home/test/x/y.db")); assert_eq!(expand_tilde("/abs/x.db"), PathBuf::from("/abs/x.db")); } }