1use serde::{Deserialize, Serialize, Deserializer};
2use std::collections::HashMap;
3use std::path::PathBuf;
4use std::env;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct TableHeaders {
8 pub time: String,
9 pub content: String,
10}
11
12#[derive(Debug, Clone, Serialize)]
13pub struct Config {
14 pub vaults: HashMap<String, VaultConfig>,
15 pub default_vault: Option<String>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct VaultConfig {
20 pub name: String,
21 #[serde(deserialize_with = "deserialize_path_with_tilde_expansion")]
22 pub path: PathBuf,
23 pub locale: String,
24 pub phrases: HashMap<String, String>,
25 pub section_header: Option<String>,
26 pub section_header_work: Option<String>,
27 pub section_header_personal: Option<String>,
28 pub section_header_health: Option<String>,
29 pub section_header_meetings: Option<String>,
30 pub table_headers: Option<TableHeaders>,
31 pub date_format: Option<String>,
32 #[serde(deserialize_with = "deserialize_template_file_with_expansion")]
33 pub template_file: Option<String>,
34 pub file_path_format: Option<String>,
35 pub list_type: Option<NoteFormat>,
37 pub section_name: Option<String>,
39 pub weekly_format: Option<String>,
40 pub monthly_format: Option<String>,
41 pub quarterly_format: Option<String>,
42 pub yearly_format: Option<String>,
43 pub note_format: Option<NoteFormat>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47pub enum NoteFormat {
48 #[serde(rename = "bullet")]
49 Bullet,
50 #[serde(rename = "table")]
51 Table,
52}
53
54impl Default for NoteFormat {
55 fn default() -> Self {
56 NoteFormat::Bullet
57 }
58}
59
60fn deserialize_path_with_tilde_expansion<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
62where
63 D: Deserializer<'de>,
64{
65 let path_str = String::deserialize(deserializer)?;
66 Ok(expand_tilde(&path_str))
67}
68
69fn deserialize_template_file_with_expansion<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
71where
72 D: Deserializer<'de>,
73{
74 let template_file: Option<String> = Option::deserialize(deserializer)?;
75 Ok(template_file.map(|path| expand_tilde(&path).to_string_lossy().to_string()))
76}
77
78impl Default for Config {
79 fn default() -> Self {
80 Self {
81 vaults: HashMap::new(),
82 default_vault: None,
83 }
84 }
85}
86
87impl<'de> Deserialize<'de> for Config {
88 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
89 where
90 D: Deserializer<'de>,
91 {
92 #[derive(Deserialize)]
94 struct RawConfig {
95 vaults: HashMap<String, VaultConfig>,
96 default_vault: Option<String>,
97 }
98
99 let mut raw = RawConfig::deserialize(deserializer)?;
100
101 for (_name, vault) in raw.vaults.iter_mut() {
103 if let Some(path_str) = vault.path.to_str() {
105 vault.path = expand_tilde(path_str);
106 }
107 if let Some(tpl) = vault.template_file.clone() {
109 let expanded = expand_tilde(&tpl).to_string_lossy().to_string();
110 vault.template_file = Some(expanded);
111 }
112 }
113
114 Ok(Config { vaults: raw.vaults, default_vault: raw.default_vault })
115 }
116}
117
118fn expand_tilde(path: &str) -> PathBuf {
125 if path.starts_with("~/") {
127 let home_dir = if cfg!(windows) {
129 env::var("USERPROFILE")
131 .or_else(|_| env::var("HOME"))
132 .ok()
133 } else {
134 env::var("HOME").ok()
136 };
137
138 if let Some(home) = home_dir {
139 PathBuf::from(home).join(&path[2..])
140 } else {
141 PathBuf::from(path)
143 }
144 } else if path == "~" {
145 let home_dir = if cfg!(windows) {
147 env::var("USERPROFILE")
148 .or_else(|_| env::var("HOME"))
149 .ok()
150 } else {
151 env::var("HOME").ok()
152 };
153
154 if let Some(home) = home_dir {
155 PathBuf::from(home)
156 } else {
157 PathBuf::from(path)
158 }
159 } else {
160 expand_windows_env_vars(path)
162 }
163}
164
165fn expand_windows_env_vars(path: &str) -> PathBuf {
167 if !cfg!(windows) {
168 return PathBuf::from(path);
170 }
171
172 let mut result = path.to_string();
173 let mut start = 0;
174
175 while let Some(var_start) = result[start..].find('%') {
177 let actual_start = start + var_start;
178 let var_start_pos = actual_start + 1; if let Some(var_end) = result[var_start_pos..].find('%') {
181 let var_end_pos = var_start_pos + var_end;
182 let var_name = &result[var_start_pos..var_end_pos];
183
184 if let Ok(var_value) = env::var(var_name) {
186 result.replace_range(actual_start..=var_end_pos, &var_value);
188 start = actual_start + var_value.len();
189 } else {
190 start = var_end_pos + 1;
192 }
193 } else {
194 break;
196 }
197 }
198
199 PathBuf::from(result)
200}
201
202impl VaultConfig {
203 pub fn new(name: String, path: String, locale: String) -> Self {
205 Self {
206 name,
207 path: expand_tilde(&path),
208 locale,
209 phrases: HashMap::new(),
210 section_header: None,
211 section_header_work: None,
212 section_header_personal: None,
213 section_header_health: None,
214 section_header_meetings: None,
215 table_headers: None,
216 date_format: None,
217 template_file: None,
218 file_path_format: None,
219 list_type: None,
220 section_name: None,
221 weekly_format: None,
222 monthly_format: None,
223 quarterly_format: None,
224 yearly_format: None,
225 note_format: None,
226 }
227 }
228
229 pub fn get_section_header(&self, category: Option<&str>) -> Option<&String> {
231 if let Some(cat) = category {
232 match cat {
234 "work" => return self.section_header_work.as_ref(),
235 "personal" => return self.section_header_personal.as_ref(),
236 "health" => return self.section_header_health.as_ref(),
237 "meetings" => return self.section_header_meetings.as_ref(),
238 _ => {} }
240 }
241 self.section_header.as_ref().or(self.section_name.as_ref())
243 }
244
245 pub fn test_config(name: &str, path: &str) -> Self {
247 Self {
248 name: name.to_string(),
249 path: std::path::PathBuf::from(path),
250 locale: "en-US".to_string(),
251 phrases: HashMap::new(),
252 section_header: None,
253 section_header_work: None,
254 section_header_personal: None,
255 section_header_health: None,
256 section_header_meetings: None,
257 table_headers: None,
258 date_format: None,
259 template_file: None,
260 file_path_format: None,
261 list_type: None,
262 section_name: None,
263 weekly_format: None,
264 monthly_format: None,
265 quarterly_format: None,
266 yearly_format: None,
267 note_format: None,
268 }
269 }
270}
271
272impl Config {
273 pub fn new() -> Self {
274 Self::default()
275 }
276
277 pub fn add_vault(&mut self, vault: VaultConfig) {
278 self.vaults.insert(vault.name.clone(), vault);
279 }
280
281 pub fn get_vault(&self, name: &str) -> Option<&VaultConfig> {
282 self.vaults.get(name)
283 }
284
285 pub fn get_default_vault(&self) -> Option<&VaultConfig> {
286 if let Some(default_name) = &self.default_vault {
287 self.vaults.get(default_name)
288 } else {
289 self.vaults.values().next()
291 }
292 }
293
294 pub fn set_default_vault(&mut self, vault_name: &str) -> Result<(), String> {
295 if self.vaults.contains_key(vault_name) {
296 self.default_vault = Some(vault_name.to_string());
297 Ok(())
298 } else {
299 Err(format!("Vault '{}' not found", vault_name))
300 }
301 }
302
303 pub fn clear_default_vault(&mut self) {
304 self.default_vault = None;
305 }
306
307 pub fn unset_default_vault(&mut self) {
308 self.default_vault = None;
309 }
310
311 pub fn remove_vault(&mut self, vault_name: &str) -> Result<(), String> {
312 if self.vaults.remove(vault_name).is_some() {
313 if let Some(default_name) = &self.default_vault {
315 if default_name == vault_name {
316 self.default_vault = None;
317 }
318 }
319 Ok(())
320 } else {
321 Err(format!("Vault '{}' not found", vault_name))
322 }
323 }
324}
325