Skip to main content

mps/
config.rs

1//! Configuration — loads and writes `~/.mps_config.yaml`.
2//!
3//! Handles both Ruby-style symbol-key YAML (`:storage_dir:`) and
4//! standard string-key YAML (`storage_dir:`).
5
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use serde::{Deserialize, Serialize};
9use crate::error::MpsError;
10
11fn default_git_remote()      -> String  { "origin".into() }
12fn default_git_branch()      -> String  { "master".into() }
13fn default_command()         -> String  { "open".into() }
14fn default_aliases()         -> HashMap<String, String> { HashMap::new() }
15
16/// Mirrors ~/.mps_config.yaml written by the Ruby gem.
17/// Ruby uses symbol keys (:storage_dir) but the load() normaliser strips them.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Config {
20    pub mps_dir:         PathBuf,
21    pub storage_dir:     PathBuf,
22    pub log_file:        PathBuf,
23    #[serde(default = "default_git_remote")]
24    pub git_remote:      String,
25    #[serde(default = "default_git_branch")]
26    pub git_branch:      String,
27    /// Which command `mps` (bare invocation) runs. Default: "open". Ruby supports "list".
28    #[serde(default = "default_command")]
29    pub default_command: String,
30    /// Short-hand type aliases: e.g. {"t": "task", "n": "note"}
31    #[serde(default = "default_aliases")]
32    pub aliases:         HashMap<String, String>,
33}
34
35impl Config {
36    /// Default config values using the user home directory.
37    pub fn default_config() -> Result<Self, MpsError> {
38        let home = dirs::home_dir()
39            .ok_or_else(|| MpsError::ConfigInvalid("cannot determine home directory".into()))?;
40        let mps_dir = home.join(".mps");
41        Ok(Config {
42            storage_dir:     mps_dir.join("mps"),
43            log_file:        mps_dir.join("mps.log"),
44            mps_dir,
45            git_remote:      "origin".into(),
46            git_branch:      "master".into(),
47            default_command: "open".into(),
48            aliases:         HashMap::new(),
49        })
50    }
51
52    /// Load config from a YAML file. Handles both string and symbol-prefixed keys
53    /// (Ruby writes :storage_dir, Rust writes storage_dir).
54    pub fn load(path: &Path) -> Result<Self, MpsError> {
55        if !path.exists() {
56            return Err(MpsError::ConfigNotFound(path.to_path_buf()));
57        }
58        let content = std::fs::read_to_string(path)?;
59
60        // Normalise Ruby-style symbol keys (:key:) to plain keys (key:) before parsing.
61        let normalised = content
62            .lines()
63            .map(|line| {
64                if let Some(rest) = line.strip_prefix(':') {
65                    rest.to_string()
66                } else {
67                    line.to_string()
68                }
69            })
70            .collect::<Vec<_>>()
71            .join("\n");
72
73        let cfg: Config = serde_yaml::from_str(&normalised)
74            .map_err(|e| MpsError::ConfigInvalid(e.to_string()))?;
75        Ok(cfg)
76    }
77
78    /// Write default config to path. Does nothing if the file already exists.
79    pub fn init(path: &Path) -> Result<(), MpsError> {
80        if path.exists() {
81            return Ok(());
82        }
83        let cfg = Self::default_config()?;
84        let yaml = serde_yaml::to_string(&cfg)?;
85        std::fs::write(path, yaml)?;
86        Ok(())
87    }
88
89    /// Ensure mps_dir, storage_dir exist and log_file is present.
90    pub fn ensure_dirs(&self) -> Result<(), MpsError> {
91        std::fs::create_dir_all(&self.mps_dir)?;
92        std::fs::create_dir_all(&self.storage_dir)?;
93        if !self.log_file.exists() {
94            std::fs::write(&self.log_file, "")?;
95        }
96        Ok(())
97    }
98}
99
100/// Resolve the config path: explicit arg > MPS_CONFIG env > default.
101pub fn default_config_path() -> PathBuf {
102    std::env::var("MPS_CONFIG")
103        .map(PathBuf::from)
104        .unwrap_or_else(|_| {
105            dirs::home_dir()
106                .unwrap_or_else(|| PathBuf::from("."))
107                .join(".mps_config.yaml")
108        })
109}