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();
93 let n = &meta.notify;
94 let meta_notify_is_non_default =
95 n.task_notify_at.is_some()
96 || !n.open_task_tags.is_empty()
97 || n.window_minutes != def.window_minutes
98 || n.overdue_days != def.overdue_days
99 || n.task_cooldown_minutes != def.task_cooldown_minutes
100 || !n.enabled
101 || !n.notify_open_tasks;
102 if meta_notify_is_non_default {
103 self.notify = n.clone();
104 }
105 }
106
107 pub fn load(path: &Path) -> Result<Self, MpsError> {
110 if !path.exists() {
111 return Err(MpsError::ConfigNotFound(path.to_path_buf()));
112 }
113 let content = std::fs::read_to_string(path)?;
114
115 let normalised = content
117 .lines()
118 .map(|line| {
119 if let Some(rest) = line.strip_prefix(':') {
120 rest.to_string()
121 } else {
122 line.to_string()
123 }
124 })
125 .collect::<Vec<_>>()
126 .join("\n");
127
128 let cfg: Config = serde_yaml::from_str(&normalised)
129 .map_err(|e| MpsError::ConfigInvalid(e.to_string()))?;
130 Ok(cfg)
131 }
132
133 pub fn init(path: &Path) -> Result<(), MpsError> {
135 if path.exists() {
136 return Ok(());
137 }
138 let cfg = Self::default_config()?;
139 let yaml = serde_yaml::to_string(&cfg)?;
140 std::fs::write(path, yaml)?;
141 Ok(())
142 }
143
144 pub fn ensure_dirs(&self) -> Result<(), MpsError> {
146 std::fs::create_dir_all(&self.mps_dir)?;
147 std::fs::create_dir_all(&self.storage_dir)?;
148 if !self.log_file.exists() {
149 std::fs::write(&self.log_file, "")?;
150 }
151 Ok(())
152 }
153}
154
155pub fn default_config_path() -> PathBuf {
157 std::env::var("MPS_CONFIG")
158 .map(PathBuf::from)
159 .unwrap_or_else(|_| {
160 dirs::home_dir()
161 .unwrap_or_else(|| PathBuf::from("."))
162 .join(".mps_config.yaml")
163 })
164}