config.rs
1 //! Configuration file handling. 2 3 use crate::error::JournalError; 4 use crate::opt::Opt; 5 6 use directories_next::ProjectDirs; 7 use serde::Deserialize; 8 use std::default::Default; 9 use std::path::{Path, PathBuf}; 10 11 const APP: &str = "jt"; 12 13 // The configuration file we read. 14 // 15 // Some of the fields are optional in the file. We will use default 16 // values for those, or get them command line options. 17 #[derive(Debug, Default, Deserialize)] 18 #[serde(deny_unknown_fields)] 19 struct InputConfiguration { 20 dirname: Option<PathBuf>, 21 editor: Option<String>, 22 entries: Option<PathBuf>, 23 } 24 25 impl InputConfiguration { 26 fn read(filename: &Path) -> Result<Self, JournalError> { 27 let text = std::fs::read(filename) 28 .map_err(|err| JournalError::ReadConfig(filename.to_path_buf(), err))?; 29 let config = serde_yaml::from_slice(&text) 30 .map_err(|err| JournalError::ConfigSyntax(filename.to_path_buf(), err))?; 31 Ok(config) 32 } 33 } 34 35 /// The run-time configuration. 36 /// 37 /// This is the configuration as read from the configuration file, if 38 /// any, and with all command line options applied. Nothing here is 39 /// optional. 40 #[derive(Debug, Deserialize)] 41 pub struct Configuration { 42 /// The directory where the journal is stored. 43 pub dirname: PathBuf, 44 45 /// The editor to open for editing journal entry drafts. 46 pub editor: String, 47 48 /// The directory where new entries are put. 49 /// 50 /// This is the full path name, not relative to `dirname`. 51 pub entries: PathBuf, 52 } 53 54 impl Configuration { 55 /// Read configuration file. 56 /// 57 /// The configuration is read from the file specified by the user 58 /// on the command line, or from a default location following the 59 /// XDG base directory specification. Note that only one of those 60 /// is read. 61 /// 62 /// It's OK for the default configuration file to be missing, but 63 /// if one is specified by the user explicitly, that one MUST 64 /// exist. 65 pub fn read(opt: &Opt) -> Result<Self, JournalError> { 66 let proj_dirs = 67 ProjectDirs::from("", "", APP).expect("could not figure out home directory"); 68 let filename = match &opt.global.config { 69 Some(path) => { 70 if !path.exists() { 71 return Err(JournalError::ConfigMissing(path.to_path_buf())); 72 } 73 path.to_path_buf() 74 } 75 None => proj_dirs.config_dir().to_path_buf().join("config.yaml"), 76 }; 77 let input = if filename.exists() { 78 InputConfiguration::read(&filename)? 79 } else { 80 InputConfiguration::default() 81 }; 82 83 let dirname = if let Some(path) = &opt.global.dirname { 84 path.to_path_buf() 85 } else if let Some(path) = &input.dirname { 86 expand_tilde(path) 87 } else { 88 proj_dirs.data_dir().to_path_buf() 89 }; 90 91 Ok(Self { 92 dirname: dirname.clone(), 93 editor: if let Some(name) = &opt.global.editor { 94 name.to_string() 95 } else if let Some(name) = &input.editor { 96 name.to_string() 97 } else { 98 "/usr/bin/editor".to_string() 99 }, 100 entries: if let Some(entries) = &opt.global.entries { 101 dirname.join(entries) 102 } else if let Some(entries) = &input.entries { 103 dirname.join(entries) 104 } else { 105 dirname.join("entries") 106 }, 107 }) 108 } 109 110 /// Write configuration to stdout. 111 pub fn dump(&self) { 112 println!("{self:#?}"); 113 } 114 } 115 116 fn expand_tilde(path: &Path) -> PathBuf { 117 if path.starts_with("~/") { 118 if let Some(home) = std::env::var_os("HOME") { 119 let mut expanded = PathBuf::from(home); 120 for comp in path.components().skip(1) { 121 expanded.push(comp); 122 } 123 expanded 124 } else { 125 path.to_path_buf() 126 } 127 } else { 128 path.to_path_buf() 129 } 130 }