/ src / config.rs
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  }