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_serve_port() -> u16 {
16 3000
17}
18fn default_serve_host() -> String {
19 "127.0.0.1".into()
20}
21fn default_git_remote() -> String {
22 "origin".into()
23}
24fn default_git_branch() -> String {
25 "master".into()
26}
27fn default_command() -> String {
28 "open".into()
29}
30fn default_type_aliases() -> HashMap<String, String> {
31 HashMap::new()
32}
33fn default_command_aliases() -> HashMap<String, String> {
34 HashMap::new()
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ServeConfig {
40 #[serde(default = "default_serve_port")]
41 pub port: u16,
42 #[serde(default = "default_serve_host")]
43 pub host: String,
44 #[serde(default)]
46 pub token: String,
47}
48
49impl Default for ServeConfig {
50 fn default() -> Self {
51 Self {
52 port: default_serve_port(),
53 host: default_serve_host(),
54 token: String::new(),
55 }
56 }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Config {
63 pub mps_dir: PathBuf,
64 pub storage_dir: PathBuf,
65 pub log_file: PathBuf,
66 #[serde(default = "default_git_remote")]
67 pub git_remote: String,
68 #[serde(default = "default_git_branch")]
69 pub git_branch: String,
70 #[serde(default = "default_command")]
72 pub default_command: String,
73 #[serde(default = "default_type_aliases", alias = "aliases")]
76 pub type_aliases: HashMap<String, String>,
77 #[serde(default = "default_command_aliases")]
79 pub command_aliases: HashMap<String, String>,
80 #[serde(default)]
82 pub custom_tags: Vec<String>,
83 #[serde(default)]
85 pub notify: NotifyConfig,
86 #[serde(default)]
88 pub serve: ServeConfig,
89}
90
91impl Config {
92 pub fn default_config() -> Result<Self, MpsError> {
94 let home = dirs::home_dir()
95 .ok_or_else(|| MpsError::ConfigInvalid("cannot determine home directory".into()))?;
96 let mps_dir = home.join(".mps");
97 Ok(Config {
98 storage_dir: mps_dir.join("mps"),
99 log_file: mps_dir.join("mps.log"),
100 mps_dir,
101 git_remote: "origin".into(),
102 git_branch: "master".into(),
103 default_command: "open".into(),
104 type_aliases: HashMap::new(),
105 command_aliases: HashMap::new(),
106 custom_tags: Vec::new(),
107 notify: NotifyConfig::default(),
108 serve: ServeConfig::default(),
109 })
110 }
111
112 pub fn merge_meta(&mut self, meta: &MetaConfig) {
120 for (k, v) in &meta.type_aliases {
121 self.type_aliases
122 .entry(k.clone())
123 .or_insert_with(|| v.clone());
124 }
125 for (k, v) in &meta.command_aliases {
126 self.command_aliases
127 .entry(k.clone())
128 .or_insert_with(|| v.clone());
129 }
130 if let Some(ref dc) = meta.default_command {
131 self.default_command = dc.clone();
132 }
133 for t in &meta.custom_tags {
134 if !self.custom_tags.contains(t) {
135 self.custom_tags.push(t.clone());
136 }
137 }
138 let def = NotifyConfig::default();
142 let n = &meta.notify;
143 if !n.enabled {
144 self.notify.enabled = false;
145 }
146 if !n.notify_open_tasks {
147 self.notify.notify_open_tasks = false;
148 }
149 if n.task_notify_at.is_some() {
150 self.notify.task_notify_at = n.task_notify_at.clone();
151 }
152 if !n.open_task_tags.is_empty() {
153 self.notify.open_task_tags = n.open_task_tags.clone();
154 }
155 if n.window_minutes != def.window_minutes {
156 self.notify.window_minutes = n.window_minutes;
157 }
158 if n.task_cooldown_minutes != def.task_cooldown_minutes {
159 self.notify.task_cooldown_minutes = n.task_cooldown_minutes;
160 }
161 if n.overdue_days != def.overdue_days {
162 self.notify.overdue_days = n.overdue_days;
163 }
164 }
165
166 pub fn load(path: &Path) -> Result<Self, MpsError> {
169 if !path.exists() {
170 return Err(MpsError::ConfigNotFound(path.to_path_buf()));
171 }
172 let content = std::fs::read_to_string(path)?;
173
174 let normalised = content
176 .lines()
177 .map(|line| {
178 if let Some(rest) = line.strip_prefix(':') {
179 rest.to_string()
180 } else {
181 line.to_string()
182 }
183 })
184 .collect::<Vec<_>>()
185 .join("\n");
186
187 let cfg: Config = serde_yaml::from_str(&normalised)
188 .map_err(|e| MpsError::ConfigInvalid(e.to_string()))?;
189 Ok(cfg)
190 }
191
192 pub fn init(path: &Path) -> Result<(), MpsError> {
194 if path.exists() {
195 return Ok(());
196 }
197 let cfg = Self::default_config()?;
198 let yaml = serde_yaml::to_string(&cfg)?;
199 std::fs::write(path, yaml)?;
200 Ok(())
201 }
202
203 pub fn save(&self, path: &Path) -> Result<(), MpsError> {
205 let yaml = serde_yaml::to_string(self)?;
206 let tmp = path.with_extension(format!("yaml.tmp.{}", std::process::id()));
207 std::fs::write(&tmp, &yaml)?;
208 std::fs::rename(&tmp, path)?;
209 Ok(())
210 }
211
212 pub fn ensure_dirs(&self) -> Result<(), MpsError> {
214 std::fs::create_dir_all(&self.mps_dir)?;
215 std::fs::create_dir_all(&self.storage_dir)?;
216 if !self.log_file.exists() {
217 std::fs::write(&self.log_file, "")?;
218 }
219 Ok(())
220 }
221}
222
223pub fn default_config_path() -> PathBuf {
225 std::env::var("MPS_CONFIG")
226 .map(PathBuf::from)
227 .unwrap_or_else(|_| {
228 dirs::home_dir()
229 .unwrap_or_else(|| PathBuf::from("."))
230 .join(".mps_config.yaml")
231 })
232}