mj 0.4.3

My Journal - personal tool to capture ideas, work with journals, notes and tasks in your favourite text $EDITOR.
Documentation
use crate::core::*;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};

pub type QueryResult = Vec<(String, Vec<String>)>;

#[derive(Debug)]
pub struct Vault {
  pub config: Config,
  pub root: PathBuf,
  pub ideas: PathBuf,
  pub journals: PathBuf,
  pub notes: PathBuf,
  pub tasks: PathBuf,
}

impl Vault {
  fn list_paths(&self) -> Vec<PathBuf> {
    vec![
      self.ideas.to_path_buf(),
      self.journals.to_path_buf(),
      self.notes.to_path_buf(),
      self.tasks.to_path_buf(),
    ]
  }

  pub fn new<P: AsRef<Path>>(config: &Config, root: P) -> Self {
    let path = root.as_ref().to_path_buf();
    Vault {
      config: config.clone(),
      root: root.as_ref().to_path_buf(),
      ideas: path.join("ideas"),
      journals: path.join("journals"),
      notes: path.join("notes"),
      tasks: path.join("tasks"),
    }
  }

  pub fn is_bootstrapped(&self) -> bool {
    for path in self.list_paths() {
      let dir_present = path.exists() && path.is_dir();
      if !dir_present {
        return false;
      }
    }
    true
  }

  pub fn bootstrap(&self) -> Result<()> {
    fs::create_dir_all(&self.ideas)?;
    fs::File::create(&self.ideas.join(".gitkeep"))?;
    fs::create_dir_all(&self.journals)?;
    fs::File::create(&self.journals.join(".gitkeep"))?;
    fs::create_dir_all(&self.notes)?;
    fs::File::create(&self.notes.join(".gitkeep"))?;
    fs::create_dir_all(&self.tasks)?;
    fs::File::create(&self.tasks.join(".gitkeep"))?;
    Ok(())
  }

  pub fn detect(config: &Config) -> Result<Vault> {
    Vault::current(config).or(Vault::global_preped(config))
  }

  fn load_local_config(&self, config: &Config) -> Option<Config> {
    // load vault's local configuration
    if let Some(local) = ConfigOpt::load(self.root.join(".mjconfig")) {
      let (config, _) = Config::merge(config, &local);
      Some(config)
    } else {
      None
    }
  }

  fn current(config: &Config) -> Result<Vault> {
    let mut path = std::env::current_dir()?;
    let mut counter = 5;
    while counter >= 0 {
      let vault = Vault::new(config, &path);
      if vault.is_bootstrapped() {
        if let Some(config) = vault.load_local_config(config) {
          return Ok(Vault::new(&config, &path))
        }
        return Ok(vault)
      }
      if let Some(p) = path.parent() {
        path = p.to_path_buf();
      }
      counter -= 1;
    }
    Err(Error::VaultNotFound)
  }

  fn global_preped(config: &Config) -> Result<Vault> {
    let global_vault_root = &config.global_vault_root;
    if !global_vault_root.exists() {
      fs::create_dir_all(global_vault_root)?;
    }
    let vault = Vault::new(config, global_vault_root);
    if !vault.is_bootstrapped() {
      vault.bootstrap()?;
    }
    if let Some(config) = vault.load_local_config(config) {
      return Ok(Vault::new(&config, &global_vault_root))
    }
    Ok(vault)
  }

  pub fn list_ideas(&self) -> Result<QueryResult> {
    let base = Path::new(&self.ideas);
    let reader = std::fs::read_dir(&base)?;
    let mut files: Vec<String> =
      reader
        .filter(|x| x.is_ok()).map(|x| x.unwrap().path())
        .filter(|x| x.as_path().extension().unwrap_or(std::ffi::OsStr::new("")) == "md")
        .map(|x| x.as_path().file_stem().unwrap_or(OsStr::new("")).to_str().unwrap_or("").to_owned())
        .filter(|x| x != "")
        .collect();
    files.sort();
    let mut result: QueryResult = vec![];
    for file in files {
      result.push((file, vec![]));
    }
    Ok(result)
  }

  pub fn list_journals(&self) -> Result<QueryResult> {
    let base = Path::new(&self.journals);
    let reader = std::fs::read_dir(&base)?;
    let mut dates: Vec<_> =
      reader
        .filter(|x| x.is_ok()).map(|x| x.unwrap().path())
        .filter(|x| x.as_path().is_dir())
        .map(|x| x.as_path().file_name().unwrap_or(OsStr::new("")).to_str().unwrap_or("").to_owned())
        .filter(|x| x != "")
        .collect();
    dates.sort_by(|a, b| b.cmp(a)); // descending order
    let mut result: QueryResult = vec![];
    for date in dates {
      result.push((date, vec![]));
    }
    Ok(result)
  }

  pub fn list_notes(&self, show_notes: bool, category: &Option<String>) -> Result<QueryResult> {
    let base = Path::new(&self.notes);
    let reader = std::fs::read_dir(&base)?;
    let mut categories: Vec<_> =
      reader
        .filter(|x| x.is_ok()).map(|x| x.unwrap().path())
        .filter(|x| x.as_path().is_dir())
        .map(|x| x.as_path().file_name().unwrap_or(OsStr::new("")).to_str().unwrap_or("").to_owned())
        .filter(|x| x != "")
        .collect();
    categories.sort();
    let category = category.as_ref().unwrap_or(&String::from("")).to_owned();
    let mut result: QueryResult = vec![];
    for cat in categories {
      if category == "" || cat == category {
        if show_notes {
          let path = base.join(&cat);
          let notes_reader = std::fs::read_dir(&path)?;
          let mut notes: Vec<_> =
            notes_reader
              .filter(|x| x.is_ok()).map(|x| x.unwrap().path())
              .filter(|x| x.as_path().extension().unwrap_or(std::ffi::OsStr::new("")) == "md")
              .map(|x| x.as_path().file_stem().unwrap_or(OsStr::new("")).to_str().unwrap_or("").to_owned())
              .filter(|x| x != "")
              .collect();
          notes.sort();
          result.push((cat, notes));
        } else {
          result.push((cat, vec![]));
        }
      }
    }
    Ok(result)
  }

  pub fn list_tasks(&self, show_tasks: bool, project: &Option<String>) -> Result<QueryResult> {
    let base = Path::new(&self.tasks);
    let reader = std::fs::read_dir(&base)?;
    let mut projects: Vec<_> =
      reader
        .filter(|x| x.is_ok()).map(|x| x.unwrap().path())
        .filter(|x| x.as_path().is_dir())
        .map(|x| x.as_path().file_name().unwrap_or(OsStr::new("")).to_str().unwrap_or("").to_owned())
        .filter(|x| x != "")
        .collect();
    projects.sort();
    let project = project.as_ref().unwrap_or(&String::from("")).to_owned();
    let mut result: QueryResult = vec![];
    for proj in projects {
      if project == "" || proj == project {
        if show_tasks {
          let path = base.join(&proj).join("tasks").with_extension("md");
          result.push((proj, self.parse_tasks(path)?));
        } else {
          result.push((proj, vec![]));
        }
      }
    }
    Ok(result)
  }

  fn parse_tasks(&self, path: PathBuf) -> Result<Vec<String>> {
    let mut result: Vec<String> = vec![];
    let content = std::fs::read_to_string(path)?;
    let mut lines = content.as_str().lines().into_iter()
      .map(|x| x.to_owned())
      .collect::<Vec<String>>();
    lines.dedup();
    for line in lines {
      result.push(line.to_owned());
    }
    Ok(result)
  }
}