use serde::{Deserialize, Serialize, Deserializer};
use std::collections::HashMap;
use std::path::PathBuf;
use std::env;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableHeaders {
pub time: String,
pub content: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct Config {
pub vaults: HashMap<String, VaultConfig>,
pub default_vault: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultConfig {
pub name: String,
#[serde(deserialize_with = "deserialize_path_with_tilde_expansion")]
pub path: PathBuf,
pub locale: String,
pub phrases: HashMap<String, String>,
pub section_header: Option<String>,
pub section_header_work: Option<String>,
pub section_header_personal: Option<String>,
pub section_header_health: Option<String>,
pub section_header_meetings: Option<String>,
pub table_headers: Option<TableHeaders>,
pub date_format: Option<String>,
#[serde(deserialize_with = "deserialize_template_file_with_expansion")]
pub template_file: Option<String>,
pub file_path_format: Option<String>,
pub list_type: Option<NoteFormat>,
pub section_name: Option<String>,
pub weekly_format: Option<String>,
pub monthly_format: Option<String>,
pub quarterly_format: Option<String>,
pub yearly_format: Option<String>,
pub note_format: Option<NoteFormat>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum NoteFormat {
#[serde(rename = "bullet")]
Bullet,
#[serde(rename = "table")]
Table,
}
impl Default for NoteFormat {
fn default() -> Self {
NoteFormat::Bullet
}
}
fn deserialize_path_with_tilde_expansion<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
where
D: Deserializer<'de>,
{
let path_str = String::deserialize(deserializer)?;
Ok(expand_tilde(&path_str))
}
fn deserialize_template_file_with_expansion<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let template_file: Option<String> = Option::deserialize(deserializer)?;
Ok(template_file.map(|path| expand_tilde(&path).to_string_lossy().to_string()))
}
impl Default for Config {
fn default() -> Self {
Self {
vaults: HashMap::new(),
default_vault: None,
}
}
}
impl<'de> Deserialize<'de> for Config {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct RawConfig {
vaults: HashMap<String, VaultConfig>,
default_vault: Option<String>,
}
let mut raw = RawConfig::deserialize(deserializer)?;
for (_name, vault) in raw.vaults.iter_mut() {
if let Some(path_str) = vault.path.to_str() {
vault.path = expand_tilde(path_str);
}
if let Some(tpl) = vault.template_file.clone() {
let expanded = expand_tilde(&tpl).to_string_lossy().to_string();
vault.template_file = Some(expanded);
}
}
Ok(Config { vaults: raw.vaults, default_vault: raw.default_vault })
}
}
fn expand_tilde(path: &str) -> PathBuf {
if path.starts_with("~/") {
let home_dir = if cfg!(windows) {
env::var("USERPROFILE")
.or_else(|_| env::var("HOME"))
.ok()
} else {
env::var("HOME").ok()
};
if let Some(home) = home_dir {
PathBuf::from(home).join(&path[2..])
} else {
PathBuf::from(path)
}
} else if path == "~" {
let home_dir = if cfg!(windows) {
env::var("USERPROFILE")
.or_else(|_| env::var("HOME"))
.ok()
} else {
env::var("HOME").ok()
};
if let Some(home) = home_dir {
PathBuf::from(home)
} else {
PathBuf::from(path)
}
} else {
expand_windows_env_vars(path)
}
}
fn expand_windows_env_vars(path: &str) -> PathBuf {
if !cfg!(windows) {
return PathBuf::from(path);
}
let mut result = path.to_string();
let mut start = 0;
while let Some(var_start) = result[start..].find('%') {
let actual_start = start + var_start;
let var_start_pos = actual_start + 1;
if let Some(var_end) = result[var_start_pos..].find('%') {
let var_end_pos = var_start_pos + var_end;
let var_name = &result[var_start_pos..var_end_pos];
if let Ok(var_value) = env::var(var_name) {
result.replace_range(actual_start..=var_end_pos, &var_value);
start = actual_start + var_value.len();
} else {
start = var_end_pos + 1;
}
} else {
break;
}
}
PathBuf::from(result)
}
impl VaultConfig {
pub fn new(name: String, path: String, locale: String) -> Self {
Self {
name,
path: expand_tilde(&path),
locale,
phrases: HashMap::new(),
section_header: None,
section_header_work: None,
section_header_personal: None,
section_header_health: None,
section_header_meetings: None,
table_headers: None,
date_format: None,
template_file: None,
file_path_format: None,
list_type: None,
section_name: None,
weekly_format: None,
monthly_format: None,
quarterly_format: None,
yearly_format: None,
note_format: None,
}
}
pub fn get_section_header(&self, category: Option<&str>) -> Option<&String> {
if let Some(cat) = category {
match cat {
"work" => return self.section_header_work.as_ref(),
"personal" => return self.section_header_personal.as_ref(),
"health" => return self.section_header_health.as_ref(),
"meetings" => return self.section_header_meetings.as_ref(),
_ => {} }
}
self.section_header.as_ref().or(self.section_name.as_ref())
}
pub fn test_config(name: &str, path: &str) -> Self {
Self {
name: name.to_string(),
path: std::path::PathBuf::from(path),
locale: "en-US".to_string(),
phrases: HashMap::new(),
section_header: None,
section_header_work: None,
section_header_personal: None,
section_header_health: None,
section_header_meetings: None,
table_headers: None,
date_format: None,
template_file: None,
file_path_format: None,
list_type: None,
section_name: None,
weekly_format: None,
monthly_format: None,
quarterly_format: None,
yearly_format: None,
note_format: None,
}
}
}
impl Config {
pub fn new() -> Self {
Self::default()
}
pub fn add_vault(&mut self, vault: VaultConfig) {
self.vaults.insert(vault.name.clone(), vault);
}
pub fn get_vault(&self, name: &str) -> Option<&VaultConfig> {
self.vaults.get(name)
}
pub fn get_default_vault(&self) -> Option<&VaultConfig> {
if let Some(default_name) = &self.default_vault {
self.vaults.get(default_name)
} else {
self.vaults.values().next()
}
}
pub fn set_default_vault(&mut self, vault_name: &str) -> Result<(), String> {
if self.vaults.contains_key(vault_name) {
self.default_vault = Some(vault_name.to_string());
Ok(())
} else {
Err(format!("Vault '{}' not found", vault_name))
}
}
pub fn clear_default_vault(&mut self) {
self.default_vault = None;
}
pub fn unset_default_vault(&mut self) {
self.default_vault = None;
}
pub fn remove_vault(&mut self, vault_name: &str) -> Result<(), String> {
if self.vaults.remove(vault_name).is_some() {
if let Some(default_name) = &self.default_vault {
if default_name == vault_name {
self.default_vault = None;
}
}
Ok(())
} else {
Err(format!("Vault '{}' not found", vault_name))
}
}
}