aboutsummaryrefslogtreecommitdiffstats
path: root/src/config.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/config.rs')
-rw-r--r--src/config.rs134
1 files changed, 134 insertions, 0 deletions
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..4e81bbe
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,134 @@
+//! 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<String>,
+ /// Seconds to wait for each request.
+ pub timeout: Option<u64>,
+ /// Cache database.
+ pub cache: Option<String>,
+ /// Hunspell `.dic` used for suggestions.
+ #[serde(rename = "diccionario")]
+ pub dictionary: Option<String>,
+ /// Fixed output width; defaults to the terminal width.
+ #[serde(rename = "ancho")]
+ pub width: Option<usize>,
+}
+
+#[derive(Debug, Clone)]
+pub struct Config {
+ pub source: String,
+ pub timeout: u64,
+ pub cache: PathBuf,
+ pub dictionary: Option<PathBuf>,
+ pub width: Option<usize>,
+}
+
+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<Config> {
+ 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"));
+ }
+}