1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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"));
}
}
|