//! Configuration file handling. use crate::error::JournalError; use crate::opt::Opt; use directories_next::ProjectDirs; use serde::Deserialize; use std::default::Default; use std::path::{Path, PathBuf}; const APP: &str = "jt"; // The configuration file we read. // // Some of the fields are optional in the file. We will use default // values for those, or get them command line options. #[derive(Debug, Default, Deserialize)] #[serde(deny_unknown_fields)] struct InputConfiguration { dirname: Option, editor: Option, entries: Option, } impl InputConfiguration { fn read(filename: &Path) -> Result { let text = std::fs::read(filename) .map_err(|err| JournalError::ReadConfig(filename.to_path_buf(), err))?; let config = serde_yaml::from_slice(&text) .map_err(|err| JournalError::ConfigSyntax(filename.to_path_buf(), err))?; Ok(config) } } /// The run-time configuration. /// /// This is the configuration as read from the configuration file, if /// any, and with all command line options applied. Nothing here is /// optional. #[derive(Debug, Deserialize)] pub struct Configuration { /// The directory where the journal is stored. pub dirname: PathBuf, /// The editor to open for editing journal entry drafts. pub editor: String, /// The directory where new entries are put. /// /// This is the full path name, not relative to `dirname`. pub entries: PathBuf, } impl Configuration { /// Read configuration file. /// /// The configuration is read from the file specified by the user /// on the command line, or from a default location following the /// XDG base directory specification. Note that only one of those /// is read. /// /// It's OK for the default configuration file to be missing, but /// if one is specified by the user explicitly, that one MUST /// exist. pub fn read(opt: &Opt) -> Result { let proj_dirs = ProjectDirs::from("", "", APP).expect("could not figure out home directory"); let filename = match &opt.global.config { Some(path) => { if !path.exists() { return Err(JournalError::ConfigMissing(path.to_path_buf())); } path.to_path_buf() } None => proj_dirs.config_dir().to_path_buf().join("config.yaml"), }; let input = if filename.exists() { InputConfiguration::read(&filename)? } else { InputConfiguration::default() }; let dirname = if let Some(path) = &opt.global.dirname { path.to_path_buf() } else if let Some(path) = &input.dirname { expand_tilde(path) } else { proj_dirs.data_dir().to_path_buf() }; Ok(Self { dirname: dirname.clone(), editor: if let Some(name) = &opt.global.editor { name.to_string() } else if let Some(name) = &input.editor { name.to_string() } else { "/usr/bin/editor".to_string() }, entries: if let Some(entries) = &opt.global.entries { dirname.join(entries) } else if let Some(entries) = &input.entries { dirname.join(entries) } else { dirname.join("entries") }, }) } /// Write configuration to stdout. pub fn dump(&self) { println!("{:#?}", self); } } fn expand_tilde(path: &Path) -> PathBuf { if path.starts_with("~/") { if let Some(home) = std::env::var_os("HOME") { let mut expanded = PathBuf::from(home); for comp in path.components().skip(1) { expanded.push(comp); } expanded } else { path.to_path_buf() } } else { path.to_path_buf() } }