1use crate::error::MpsError;
7use crate::meta::MetaConfig;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12pub use crate::meta::NotifyConfig;
14
15fn default_git_remote() -> String {
16 "origin".into()
17}
18fn default_git_branch() -> String {
19 "master".into()
20}
21fn default_command() -> String {
22 "open".into()
23}
24fn default_type_aliases() -> HashMap<String, String> {
25 HashMap::new()
26}
27fn default_command_aliases() -> HashMap<String, String> {
28 HashMap::new()
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Config {
35 pub mps_dir: PathBuf,
36 pub storage_dir: PathBuf,
37 pub log_file: PathBuf,
38 #[serde(default = "default_git_remote")]
39 pub git_remote: String,
40 #[serde(default = "default_git_branch")]
41 pub git_branch: String,
42 #[serde(default = "default_command")]
44 pub default_command: String,
45 #[serde(default = "default_type_aliases", alias = "aliases")]
48 pub type_aliases: HashMap<String, String>,
49 #[serde(default = "default_command_aliases")]
51 pub command_aliases: HashMap<String, String>,
52 #[serde(default)]
54 pub custom_tags: Vec<String>,
55 #[serde(default)]
57 pub notify: NotifyConfig,
58}
59
60impl Config {
61 pub fn default_config() -> Result<Self, MpsError> {
63 let home = dirs::home_dir()
64 .ok_or_else(|| MpsError::ConfigInvalid("cannot determine home directory".into()))?;
65 let mps_dir = home.join(".mps");
66 Ok(Config {
67 storage_dir: mps_dir.join("mps"),
68 log_file: mps_dir.join("mps.log"),
69 mps_dir,
70 git_remote: "origin".into(),
71 git_branch: "master".into(),
72 default_command: "open".into(),
73 type_aliases: HashMap::new(),
74 command_aliases: HashMap::new(),
75 custom_tags: Vec::new(),
76 notify: NotifyConfig::default(),
77 })
78 }
79
80 pub fn merge_meta(&mut self, meta: &MetaConfig) {
88 for (k, v) in &meta.type_aliases {
89 self.type_aliases
90 .entry(k.clone())
91 .or_insert_with(|| v.clone());
92 }
93 for (k, v) in &meta.command_aliases {
94 self.command_aliases
95 .entry(k.clone())
96 .or_insert_with(|| v.clone());
97 }
98 if let Some(ref dc) = meta.default_command {
99 self.default_command = dc.clone();
100 }
101 for t in &meta.custom_tags {
102 if !self.custom_tags.contains(t) {
103 self.custom_tags.push(t.clone());
104 }
105 }
106 let def = NotifyConfig::default();
110 let n = &meta.notify;
111 if !n.enabled {
112 self.notify.enabled = false;
113 }
114 if !n.notify_open_tasks {
115 self.notify.notify_open_tasks = false;
116 }
117 if n.task_notify_at.is_some() {
118 self.notify.task_notify_at = n.task_notify_at.clone();
119 }
120 if !n.open_task_tags.is_empty() {
121 self.notify.open_task_tags = n.open_task_tags.clone();
122 }
123 if n.window_minutes != def.window_minutes {
124 self.notify.window_minutes = n.window_minutes;
125 }
126 if n.task_cooldown_minutes != def.task_cooldown_minutes {
127 self.notify.task_cooldown_minutes = n.task_cooldown_minutes;
128 }
129 if n.overdue_days != def.overdue_days {
130 self.notify.overdue_days = n.overdue_days;
131 }
132 }
133
134 pub fn load(path: &Path) -> Result<Self, MpsError> {
137 if !path.exists() {
138 return Err(MpsError::ConfigNotFound(path.to_path_buf()));
139 }
140 let content = std::fs::read_to_string(path)?;
141
142 let normalised = content
144 .lines()
145 .map(|line| {
146 if let Some(rest) = line.strip_prefix(':') {
147 rest.to_string()
148 } else {
149 line.to_string()
150 }
151 })
152 .collect::<Vec<_>>()
153 .join("\n");
154
155 let cfg: Config = serde_yaml::from_str(&normalised)
156 .map_err(|e| MpsError::ConfigInvalid(e.to_string()))?;
157 Ok(cfg)
158 }
159
160 pub fn init(path: &Path) -> Result<(), MpsError> {
162 if path.exists() {
163 return Ok(());
164 }
165 let cfg = Self::default_config()?;
166 let yaml = serde_yaml::to_string(&cfg)?;
167 std::fs::write(path, yaml)?;
168 Ok(())
169 }
170
171 pub fn save(&self, path: &Path) -> Result<(), MpsError> {
173 let yaml = serde_yaml::to_string(self)?;
174 let tmp = path.with_extension(format!("yaml.tmp.{}", std::process::id()));
175 std::fs::write(&tmp, &yaml)?;
176 std::fs::rename(&tmp, path)?;
177 Ok(())
178 }
179
180 pub fn ensure_dirs(&self) -> Result<(), MpsError> {
182 std::fs::create_dir_all(&self.mps_dir)?;
183 std::fs::create_dir_all(&self.storage_dir)?;
184 if !self.log_file.exists() {
185 std::fs::write(&self.log_file, "")?;
186 }
187 Ok(())
188 }
189}
190
191pub fn default_config_path() -> PathBuf {
193 std::env::var("MPS_CONFIG")
194 .map(PathBuf::from)
195 .unwrap_or_else(|_| {
196 dirs::home_dir()
197 .unwrap_or_else(|| PathBuf::from("."))
198 .join(".mps_config.yaml")
199 })
200}