93 lines
2.8 KiB
Rust
93 lines
2.8 KiB
Rust
|
|
use std::{
|
||
|
|
env,
|
||
|
|
ffi::{OsStr, OsString},
|
||
|
|
fs,
|
||
|
|
path::PathBuf,
|
||
|
|
};
|
||
|
|
|
||
|
|
use chrono_tz::Tz;
|
||
|
|
use color_eyre::{
|
||
|
|
Result,
|
||
|
|
eyre::{Context as _, eyre},
|
||
|
|
};
|
||
|
|
use kdl::KdlDocument;
|
||
|
|
|
||
|
|
#[derive(Debug)]
|
||
|
|
pub(crate) struct Config {
|
||
|
|
pub(crate) locations: Vec<Location>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Config {
|
||
|
|
/// Reads ttz.kdl config from a file.
|
||
|
|
///
|
||
|
|
/// Uses the provided path if `Some`; otherwise defaults to `ttz.kdl` within
|
||
|
|
/// `$XDG_CONFIG_HOME` if set or `~/.config` if not.
|
||
|
|
pub(crate) fn load_from_file(file_path: Option<&OsStr>) -> Result<Self> {
|
||
|
|
let default_file_path: Result<OsString> = env::var("XDG_CONFIG_HOME")
|
||
|
|
.ok()
|
||
|
|
.map(Into::<PathBuf>::into)
|
||
|
|
.or(env::home_dir().map(|mut buf| {
|
||
|
|
buf.push(".config");
|
||
|
|
buf
|
||
|
|
}))
|
||
|
|
.map(|mut buf| {
|
||
|
|
buf.push("ttz.kdl");
|
||
|
|
buf.into()
|
||
|
|
})
|
||
|
|
.ok_or(eyre!("Unable to determine config home"));
|
||
|
|
let file_path = if let Some(file_path) = file_path {
|
||
|
|
file_path
|
||
|
|
} else {
|
||
|
|
&default_file_path?
|
||
|
|
};
|
||
|
|
if !fs::exists(file_path)? {
|
||
|
|
return Err(eyre!("Unable to find config file"));
|
||
|
|
}
|
||
|
|
let config_raw = fs::read_to_string(file_path)?;
|
||
|
|
|
||
|
|
let doc: KdlDocument = config_raw
|
||
|
|
.parse()
|
||
|
|
.wrap_err(eyre!("Unable to parse config file"))?;
|
||
|
|
|
||
|
|
let mut locations: Vec<Location> = Vec::with_capacity(doc.nodes().len());
|
||
|
|
for node in doc.nodes() {
|
||
|
|
if node.name().value() != "location" {
|
||
|
|
return Err(eyre!(
|
||
|
|
"Config parse error: only `location` nodes are allowed"
|
||
|
|
));
|
||
|
|
}
|
||
|
|
let name = node
|
||
|
|
.entry(0)
|
||
|
|
.and_then(|entry| entry.value().as_string())
|
||
|
|
.ok_or(eyre!(
|
||
|
|
"Config parse error: each node must have an argument for its name"
|
||
|
|
))?
|
||
|
|
.to_owned();
|
||
|
|
let tz_raw: &str = node
|
||
|
|
.entry("tz")
|
||
|
|
.ok_or(eyre!(
|
||
|
|
"Config parse error: each node must have a `tz` property"
|
||
|
|
))?
|
||
|
|
.value()
|
||
|
|
.as_string()
|
||
|
|
.ok_or(eyre!("Config parse error: tz must be a string"))?;
|
||
|
|
let tz: Tz = tz_raw.parse().wrap_err(eyre!(
|
||
|
|
"Config parse error: not a chrono-tz time zone: {tz_raw}",
|
||
|
|
))?;
|
||
|
|
locations.push(Location { name, tz })
|
||
|
|
}
|
||
|
|
if locations.is_empty() {
|
||
|
|
return Err(eyre!(
|
||
|
|
"Config parse error: must have at least one `location`"
|
||
|
|
));
|
||
|
|
}
|
||
|
|
Ok(Self { locations })
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Clone, Debug)]
|
||
|
|
pub(crate) struct Location {
|
||
|
|
pub(crate) name: String,
|
||
|
|
pub(crate) tz: Tz,
|
||
|
|
}
|