mj 0.4.3

My Journal - personal tool to capture ideas, work with journals, notes and tasks in your favourite text $EDITOR.
Documentation
use clap::ArgMatches;
use dirs;
use serde_derive::{Serialize, Deserialize};
use std::default::Default;
use std::path::{Path, PathBuf};
use toml;

struct ConfigRef {
  pub path: PathBuf,
}

impl Default for ConfigRef {
  fn default() -> Self {
    let path =
      dirs::config_dir().expect("cannot find config dir")
        .join("my-journal")
        .join("config");
    ConfigRef { path }
  }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigOpt {
  pub global_vault_root: Option<PathBuf>,
  pub view_delimiter: Option<String>,
  pub input_delimiter: Option<String>,
  pub task_not_done_prefix: Option<String>,
  pub task_not_done_display_prefix: Option<String>,
  pub task_in_progress_prefix: Option<String>,
  pub task_in_progress_display_prefix: Option<String>,
  pub task_done_prefix: Option<String>,
  pub task_done_display_prefix: Option<String>,
}

impl ConfigOpt {
  fn deserialize(value: &str) -> Option<Self> {
    toml::from_str(value).ok()
  }

  pub fn load(path: PathBuf) -> Option<Self> {
    if path.exists() {
      let toml = std::fs::read_to_string(&path).expect("cannot read config file");
      Some(ConfigOpt::deserialize(toml.as_str()).expect("cannot parse config file"))
    } else {
      None
    }
  }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
  pub global_vault_root: PathBuf,
  pub view_delimiter: String,
  pub input_delimiter: String,
  pub task_not_done_prefix: String,
  pub task_not_done_display_prefix: String,
  pub task_in_progress_prefix: String,
  pub task_in_progress_display_prefix: String,
  pub task_done_prefix: String,
  pub task_done_display_prefix: String,
}

impl Config {

  fn merge_field<T>(input: &Option<T>, output: &mut T, is_new: &mut bool)
  where
    T: PartialEq,
    T: Clone,
  {
    if input.is_none() {
      *is_new = true;
    }
    if let Some(value) = input {
      if *output != *value {
        *output = value.to_owned();
      }
    }
  }

  pub fn merge(base: &Config, opt: &ConfigOpt) -> (Self, bool) {
    let mut result: Config = base.clone();
    let mut is_new = false;
    Config::merge_field(&opt.global_vault_root, &mut result.global_vault_root, &mut is_new);
    Config::merge_field(&opt.view_delimiter, &mut result.view_delimiter, &mut is_new);
    Config::merge_field(&opt.input_delimiter, &mut result.input_delimiter, &mut is_new);
    Config::merge_field(&opt.task_not_done_prefix, &mut result.task_not_done_prefix, &mut is_new);
    Config::merge_field(&opt.task_not_done_display_prefix, &mut result.task_not_done_display_prefix, &mut is_new);
    Config::merge_field(&opt.task_in_progress_prefix, &mut result.task_in_progress_prefix, &mut is_new);
    Config::merge_field(&opt.task_in_progress_display_prefix, &mut result.task_in_progress_display_prefix, &mut is_new);
    Config::merge_field(&opt.task_done_prefix, &mut result.task_done_prefix, &mut is_new);
    Config::merge_field(&opt.task_done_display_prefix, &mut result.task_done_display_prefix, &mut is_new);
    (result, is_new)
  }

  pub fn serialize(&self) -> Option<String> {
    toml::to_string_pretty(self).ok()
  }

  #[allow(dead_code)] // used only in tests
  pub fn deserialize(value: &str) -> Option<Self> {
    toml::from_str(value).ok()
  }

  pub fn clone(&self) -> Self {
    Config {
      global_vault_root: self.global_vault_root.to_owned(),
      view_delimiter: self.view_delimiter.to_owned(),
      input_delimiter: self.input_delimiter.to_owned(),
      task_not_done_prefix: self.task_not_done_prefix.to_owned(),
      task_not_done_display_prefix: self.task_not_done_display_prefix.to_owned(),
      task_in_progress_prefix: self.task_in_progress_prefix.to_owned(),
      task_in_progress_display_prefix: self.task_in_progress_display_prefix.to_owned(),
      task_done_prefix: self.task_done_prefix.to_owned(),
      task_done_display_prefix: self.task_done_display_prefix.to_owned(),
    }
  }
}

impl Default for Config {
  fn default() -> Self {
    let global_vault_root = dirs::data_local_dir()
      .expect("cannot find data local dir")
      .join("my-journal");
    let view_delimiter = String::from("");
    let input_delimiter = String::from("::");
    let task_not_done_prefix = String::from("-");
    let task_not_done_display_prefix = String::from("");
    let task_in_progress_prefix = String::from("*");
    let task_in_progress_display_prefix = String::from("");
    let task_done_prefix = String::from("+");
    let task_done_display_prefix = String::from("");
    Config {
      global_vault_root,
      view_delimiter,
      input_delimiter,
      task_not_done_prefix,
      task_not_done_display_prefix,
      task_in_progress_prefix,
      task_in_progress_display_prefix,
      task_done_prefix,
      task_done_display_prefix,
    }
  }
}

pub fn prep(matches: &ArgMatches) -> Config {
  let ConfigRef { path } = match_args(matches);
  let (config, write_to_disk) =
    if path.exists() {
      let toml = std::fs::read_to_string(&path).expect("cannot read config file");
      let opt = ConfigOpt::deserialize(toml.as_str()).expect("cannot parse config file");
      Config::merge(&Default::default(), &opt)
    } else {
      let config: Config = Default::default();
      (config, true)
    };
  if write_to_disk {
    let toml = config.serialize().expect("cannot serialize default configuration");
    if let Some(parent) = path.parent() {
      std::fs::create_dir_all(parent).expect("cannot create dir for configuration files");
    }
    std::fs::write(&path, toml.as_bytes()).expect("cannot write default config file");
  }
  // Vault::detect might overwrite some configuration entries
  config
}

fn match_args(matches: &ArgMatches) -> ConfigRef {
  match matches.value_of("config") {
    Some(value) => {
      let path = Path::new(value).to_path_buf();
      ConfigRef { path }
    },
    None => Default::default(),
  }
}