1use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use serde::{Deserialize, Serialize};
9use crate::error::MpsError;
10use crate::meta::MetaConfig;
11
12pub use crate::meta::NotifyConfig;
14
15fn default_git_remote() -> String { "origin".into() }
16fn default_git_branch() -> String { "master".into() }
17fn default_command() -> String { "open".into() }
18fn default_type_aliases() -> HashMap<String, String> { HashMap::new() }
19fn default_command_aliases() -> HashMap<String, String> { HashMap::new() }
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct Config {
25 pub mps_dir: PathBuf,
26 pub storage_dir: PathBuf,
27 pub log_file: PathBuf,
28 #[serde(default = "default_git_remote")]
29 pub git_remote: String,
30 #[serde(default = "default_git_branch")]
31 pub git_branch: String,
32 #[serde(default = "default_command")]
34 pub default_command: String,
35 #[serde(default = "default_type_aliases", alias = "aliases")]
38 pub type_aliases: HashMap<String, String>,
39 #[serde(default = "default_command_aliases")]
41 pub command_aliases: HashMap<String, String>,
42 #[serde(default)]
44 pub custom_tags: Vec<String>,
45 #[serde(default)]
47 pub notify: NotifyConfig,
48}
49
50impl Config {
51 pub fn default_config() -> Result<Self, MpsError> {
53 let home = dirs::home_dir()
54 .ok_or_else(|| MpsError::ConfigInvalid("cannot determine home directory".into()))?;
55 let mps_dir = home.join(".mps");
56 Ok(Config {
57 storage_dir: mps_dir.join("mps"),
58 log_file: mps_dir.join("mps.log"),
59 mps_dir,
60 git_remote: "origin".into(),
61 git_branch: "master".into(),
62 default_command: "open".into(),
63 type_aliases: HashMap::new(),
64 command_aliases: HashMap::new(),
65 custom_tags: Vec::new(),
66 notify: NotifyConfig::default(),
67 })
68 }
69
70 pub fn merge_meta(&mut self, meta: &MetaConfig) {
78 for (k, v) in &meta.type_aliases {
79 self.type_aliases.entry(k.clone()).or_insert_with(|| v.clone());
80 }
81 for (k, v) in &meta.command_aliases {
82 self.command_aliases.entry(k.clone()).or_insert_with(|| v.clone());
83 }
84 if let Some(ref dc) = meta.default_command {
85 self.default_command = dc.clone();
86 }
87 for t in &meta.custom_tags {
88 if !self.custom_tags.contains(t) {
89 self.custom_tags.push(t.clone());
90 }
91 }
92 let def = NotifyConfig::default();
96 let n = &meta.notify;
97 if !n.enabled { self.notify.enabled = false; }
98 if !n.notify_open_tasks { self.notify.notify_open_tasks = false; }
99 if n.task_notify_at.is_some() { self.notify.task_notify_at = n.task_notify_at.clone(); }
100 if !n.open_task_tags.is_empty() { self.notify.open_task_tags = n.open_task_tags.clone(); }
101 if n.window_minutes != def.window_minutes { self.notify.window_minutes = n.window_minutes; }
102 if n.task_cooldown_minutes != def.task_cooldown_minutes { self.notify.task_cooldown_minutes = n.task_cooldown_minutes; }
103 if n.overdue_days != def.overdue_days { self.notify.overdue_days = n.overdue_days; }
104 }
105
106 pub fn load(path: &Path) -> Result<Self, MpsError> {
109 if !path.exists() {
110 return Err(MpsError::ConfigNotFound(path.to_path_buf()));
111 }
112 let content = std::fs::read_to_string(path)?;
113
114 let normalised = content
116 .lines()
117 .map(|line| {
118 if let Some(rest) = line.strip_prefix(':') {
119 rest.to_string()
120 } else {
121 line.to_string()
122 }
123 })
124 .collect::<Vec<_>>()
125 .join("\n");
126
127 let cfg: Config = serde_yaml::from_str(&normalised)
128 .map_err(|e| MpsError::ConfigInvalid(e.to_string()))?;
129 Ok(cfg)
130 }
131
132 pub fn init(path: &Path) -> Result<(), MpsError> {
134 if path.exists() {
135 return Ok(());
136 }
137 let cfg = Self::default_config()?;
138 let yaml = serde_yaml::to_string(&cfg)?;
139 std::fs::write(path, yaml)?;
140 Ok(())
141 }
142
143 pub fn ensure_dirs(&self) -> Result<(), MpsError> {
145 std::fs::create_dir_all(&self.mps_dir)?;
146 std::fs::create_dir_all(&self.storage_dir)?;
147 if !self.log_file.exists() {
148 std::fs::write(&self.log_file, "")?;
149 }
150 Ok(())
151 }
152}
153
154pub fn default_config_path() -> PathBuf {
156 std::env::var("MPS_CONFIG")
157 .map(PathBuf::from)
158 .unwrap_or_else(|_| {
159 dirs::home_dir()
160 .unwrap_or_else(|| PathBuf::from("."))
161 .join(".mps_config.yaml")
162 })
163}