mps-rs 1.1.0

MPS — plain-text personal productivity CLI (Rust)
Documentation
//! Configuration — loads and writes `~/.mps_config.yaml`.
//!
//! Handles both Ruby-style symbol-key YAML (`:storage_dir:`) and
//! standard string-key YAML (`storage_dir:`).

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::error::MpsError;

fn default_git_remote()      -> String  { "origin".into() }
fn default_git_branch()      -> String  { "master".into() }
fn default_command()         -> String  { "open".into() }
fn default_type_aliases()    -> HashMap<String, String> { HashMap::new() }
fn default_command_aliases() -> HashMap<String, String> { HashMap::new() }

/// Mirrors ~/.mps_config.yaml written by the Ruby gem.
/// Ruby uses symbol keys (:storage_dir) but the load() normaliser strips them.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    pub mps_dir:         PathBuf,
    pub storage_dir:     PathBuf,
    pub log_file:        PathBuf,
    #[serde(default = "default_git_remote")]
    pub git_remote:      String,
    #[serde(default = "default_git_branch")]
    pub git_branch:      String,
    /// Which command `mps` (bare invocation) runs. Default: "open". Ruby supports "list".
    #[serde(default = "default_command")]
    pub default_command: String,
    /// Short-hand element-type aliases: e.g. {"t": "task", "n": "note"}
    /// Accepts the legacy "aliases" key for backward compatibility with existing configs.
    #[serde(default = "default_type_aliases", alias = "aliases")]
    pub type_aliases:    HashMap<String, String>,
    /// Short-hand command aliases: e.g. {"a": "append", "+": "append"}
    #[serde(default = "default_command_aliases")]
    pub command_aliases: HashMap<String, String>,
}

impl Config {
    /// Default config values using the user home directory.
    pub fn default_config() -> Result<Self, MpsError> {
        let home = dirs::home_dir()
            .ok_or_else(|| MpsError::ConfigInvalid("cannot determine home directory".into()))?;
        let mps_dir = home.join(".mps");
        Ok(Config {
            storage_dir:     mps_dir.join("mps"),
            log_file:        mps_dir.join("mps.log"),
            mps_dir,
            git_remote:      "origin".into(),
            git_branch:      "master".into(),
            default_command: "open".into(),
            type_aliases:    HashMap::new(),
            command_aliases: HashMap::new(),
        })
    }

    /// Load config from a YAML file. Handles both string and symbol-prefixed keys
    /// (Ruby writes :storage_dir, Rust writes storage_dir).
    pub fn load(path: &Path) -> Result<Self, MpsError> {
        if !path.exists() {
            return Err(MpsError::ConfigNotFound(path.to_path_buf()));
        }
        let content = std::fs::read_to_string(path)?;

        // Normalise Ruby-style symbol keys (:key:) to plain keys (key:) before parsing.
        let normalised = content
            .lines()
            .map(|line| {
                if let Some(rest) = line.strip_prefix(':') {
                    rest.to_string()
                } else {
                    line.to_string()
                }
            })
            .collect::<Vec<_>>()
            .join("\n");

        let cfg: Config = serde_yaml::from_str(&normalised)
            .map_err(|e| MpsError::ConfigInvalid(e.to_string()))?;
        Ok(cfg)
    }

    /// Write default config to path. Does nothing if the file already exists.
    pub fn init(path: &Path) -> Result<(), MpsError> {
        if path.exists() {
            return Ok(());
        }
        let cfg = Self::default_config()?;
        let yaml = serde_yaml::to_string(&cfg)?;
        std::fs::write(path, yaml)?;
        Ok(())
    }

    /// Ensure mps_dir, storage_dir exist and log_file is present.
    pub fn ensure_dirs(&self) -> Result<(), MpsError> {
        std::fs::create_dir_all(&self.mps_dir)?;
        std::fs::create_dir_all(&self.storage_dir)?;
        if !self.log_file.exists() {
            std::fs::write(&self.log_file, "")?;
        }
        Ok(())
    }
}

/// Resolve the config path: explicit arg > MPS_CONFIG env > default.
pub fn default_config_path() -> PathBuf {
    std::env::var("MPS_CONFIG")
        .map(PathBuf::from)
        .unwrap_or_else(|_| {
            dirs::home_dir()
                .unwrap_or_else(|| PathBuf::from("."))
                .join(".mps_config.yaml")
        })
}