malakal 0.1.9

a day-planning calendar app based on egui
use std::path::PathBuf;

use anyhow::{anyhow, Context};
use chrono::Duration;
use serde::{Deserialize, Serialize};
use serde_with::{formats::Flexible, serde_as};
use toml::ser::to_string_pretty;

#[serde_as]
#[derive(Deserialize, Serialize, Debug)]
#[serde(default)]
pub struct Config {
  pub calendar_name: String,
  pub calendar_location: String,
  pub timezone: Option<String>,
  pub notifier_switch: bool,
  pub notifier_blacklist_processes: Vec<String>,
  #[serde_as(as = "serde_with::DurationMilliSeconds<i64, Flexible>")]
  pub notification_timeout: Duration,
  pub post_update_hook: Option<Vec<String>>,
  #[serde_as(as = "serde_with::DurationMilliSeconds<i64, Flexible>")]
  pub post_update_hook_delay: Duration,
}

pub const APP_NAME: &str = env!("CARGO_PKG_NAME");

impl Default for Config {
  fn default() -> Self {
    Self {
      calendar_name: "malakal".into(),
      calendar_location: format!("~/.calendar/{APP_NAME}"),
      timezone: None,
      notifier_switch: true,
      notification_timeout: Duration::seconds(5),
      notifier_blacklist_processes: vec![],
      post_update_hook: None,
      post_update_hook_delay: Duration::seconds(30),
    }
  }
}

impl Config {
  pub fn normalize(&mut self) -> anyhow::Result<()> {
    self.calendar_location =
      self.calendar_location.replace('~', &std::env::var("HOME")?);

    Ok(())
  }

  pub fn read_or_initialize() -> anyhow::Result<Config> {
    let config_file = {
      let mut dir = dirs::config_dir()
        .with_context(|| "Cannot find a directory to store config")?;
      dir.push(format!("{APP_NAME}/config.toml"));
      dir
    };

    log::info!("Loading config from {}", config_file.display());

    let dir = config_file.parent().ok_or_else(|| {
      anyhow!("Invalid config_file location: {config_file:?}")
    })?;

    if !dir.exists() {
      std::fs::create_dir_all(dir)?;
    }

    if !config_file.exists() {
      log::info!("Creating default config at {config_file:?}");
      let default_conf = to_string_pretty(&Config::default())?;
      std::fs::write(&config_file, default_conf)?;
    }

    let mut config: Config = toml::from_slice(&std::fs::read(config_file)?)?;
    config.normalize()?;

    let calendar_location = PathBuf::from(config.calendar_location.as_str());
    if !calendar_location.exists() {
      log::info!("Creating calendar directory at {calendar_location:?}");
      std::fs::create_dir_all(&calendar_location)?;
    }

    Ok(config)
  }
}