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, } 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 { let default_file_path: Result = env::var("XDG_CONFIG_HOME") .ok() .map(Into::::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 = 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, }