summaryrefslogtreecommitdiff
path: root/src/config.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/config.rs')
-rw-r--r--src/config.rs130
1 files changed, 130 insertions, 0 deletions
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..6995e24
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,130 @@
+//! 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<PathBuf>,
+ editor: Option<String>,
+ entries: Option<PathBuf>,
+}
+
+impl InputConfiguration {
+ fn read(filename: &Path) -> Result<Self, JournalError> {
+ 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<Self, JournalError> {
+ 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()
+ }
+}