obsidian_logging/
config.rs

1use std::path::PathBuf;
2use std::fs;
3use serde::{Serialize, Deserialize};
4use std::str::FromStr;
5use std::env;
6
7#[derive(Debug, PartialEq, Clone, Serialize)]
8pub enum ListType {
9    Bullet,
10    Table,
11}
12
13#[derive(Debug, PartialEq, Clone)]
14pub enum TimeFormat {
15    Hour12,
16    Hour24,
17}
18
19impl Serialize for TimeFormat {
20    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
21    where
22        S: serde::Serializer,
23    {
24        match self {
25            TimeFormat::Hour12 => serializer.serialize_str("12"),
26            TimeFormat::Hour24 => serializer.serialize_str("24"),
27        }
28    }
29}
30
31impl<'de> Deserialize<'de> for ListType {
32    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
33    where
34        D: serde::Deserializer<'de>,
35    {
36        let s = String::deserialize(deserializer)?;
37        match s.to_lowercase().as_str() {
38            "bullet" => Ok(ListType::Bullet),
39            "table" => Ok(ListType::Table),
40            _ => Err(serde::de::Error::custom(format!(
41                "Invalid list type '{}'. Expected 'bullet' or 'table' (case insensitive)",
42                s
43            ))),
44        }
45    }
46}
47
48impl<'de> Deserialize<'de> for TimeFormat {
49    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
50    where
51        D: serde::Deserializer<'de>,
52    {
53        use serde::de::Visitor;
54        use std::fmt;
55
56        struct TimeFormatVisitor;
57
58        impl<'de> Visitor<'de> for TimeFormatVisitor {
59            type Value = TimeFormat;
60
61            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
62                formatter.write_str("a string or integer representing time format (12 or 24)")
63            }
64
65            fn visit_str<E>(self, value: &str) -> Result<TimeFormat, E>
66            where
67                E: serde::de::Error,
68            {
69                match value.to_lowercase().as_str() {
70                    "12" | "12h" | "12hour" => Ok(TimeFormat::Hour12),
71                    "24" | "24h" | "24hour" => Ok(TimeFormat::Hour24),
72                    _ => Err(E::custom(format!(
73                        "Invalid time format '{}'. Expected '12' or '24' (case insensitive)",
74                        value
75                    ))),
76                }
77            }
78
79            fn visit_u64<E>(self, value: u64) -> Result<TimeFormat, E>
80            where
81                E: serde::de::Error,
82            {
83                match value {
84                    12 => Ok(TimeFormat::Hour12),
85                    24 => Ok(TimeFormat::Hour24),
86                    _ => Err(E::custom(format!(
87                        "Invalid time format '{}'. Expected 12 or 24",
88                        value
89                    ))),
90                }
91            }
92
93            fn visit_i64<E>(self, value: i64) -> Result<TimeFormat, E>
94            where
95                E: serde::de::Error,
96            {
97                match value {
98                    12 => Ok(TimeFormat::Hour12),
99                    24 => Ok(TimeFormat::Hour24),
100                    _ => Err(E::custom(format!(
101                        "Invalid time format '{}'. Expected 12 or 24",
102                        value
103                    ))),
104                }
105            }
106        }
107
108        deserializer.deserialize_any(TimeFormatVisitor)
109    }
110}
111
112impl FromStr for ListType {
113    type Err = ();
114
115    fn from_str(input: &str) -> Result<Self, Self::Err> {
116        match input.to_lowercase().as_str() {
117            "bullet" => Ok(ListType::Bullet),
118            "table" => Ok(ListType::Table),
119            _ => Err(()),
120        }
121    }
122}
123
124impl FromStr for TimeFormat {
125    type Err = ();
126
127    fn from_str(input: &str) -> Result<Self, Self::Err> {
128        match input.to_lowercase().as_str() {
129            "12" | "12h" | "12hour" => Ok(TimeFormat::Hour12),
130            "24" | "24h" | "24hour" => Ok(TimeFormat::Hour24),
131            _ => Err(()),
132        }
133    }
134}
135
136impl ToString for ListType {
137    fn to_string(&self) -> String {
138        match self {
139            ListType::Bullet => "bullet".to_string(),
140            ListType::Table => "table".to_string(),
141        }
142    }
143}
144
145impl ToString for TimeFormat {
146    fn to_string(&self) -> String {
147        match self {
148            TimeFormat::Hour12 => "12".to_string(),
149            TimeFormat::Hour24 => "24".to_string(),
150        }
151    }
152}
153
154#[derive(Debug, Clone, Serialize)]
155pub struct Config {
156    pub vault: String,
157    pub file_path_format: String,
158    pub section_header: String,
159    pub list_type: ListType,
160    pub template_path: Option<String>,
161    pub locale: Option<String>,
162    pub time_format: TimeFormat,
163    pub time_label: String,
164    pub event_label: String,
165    pub category_headers: std::collections::HashMap<String, String>,
166    pub phrases: std::collections::HashMap<String, String>,
167}
168
169fn default_time_format() -> TimeFormat {
170    TimeFormat::Hour24
171}
172
173fn default_time_label() -> String {
174    "Tidspunkt".to_string()
175}
176
177fn default_event_label() -> String {
178    "Hendelse".to_string()
179}
180
181impl Config {
182    /// Get the conjunction word based on the configured locale
183    pub fn get_conjunction(&self) -> &'static str {
184        match self.locale.as_deref() {
185            Some("no") | Some("nb") | Some("nn") => "og",
186            Some("da") => "og",
187            Some("sv") => "och",
188            Some("de") => "und",
189            Some("fr") => "et",
190            Some("es") => "y",
191            Some("it") => "e",
192            Some("pt") => "e",
193            Some("ru") => "ΠΈ",
194            Some("ja") => "と",
195            Some("ko") => "와",
196            Some("zh") => "ε’Œ",
197            _ => "and", // Default to English
198        }
199    }
200}
201
202impl<'de> Deserialize<'de> for Config {
203    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
204    where
205        D: serde::Deserializer<'de>,
206    {
207        use serde::de::{self, MapAccess, Visitor};
208        use std::fmt;
209
210        struct ConfigVisitor;
211
212        impl<'de> Visitor<'de> for ConfigVisitor {
213            type Value = Config;
214
215            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
216                formatter.write_str("a YAML configuration object")
217            }
218
219            fn visit_map<V>(self, mut map: V) -> Result<Config, V::Error>
220            where
221                V: MapAccess<'de>,
222            {
223                let mut vault = None;
224                let mut file_path_format = None;
225                let mut section_header = None;
226                let mut list_type = None;
227                let mut template_path = None;
228                let mut locale = None;
229                let mut time_format = None;
230                let mut time_label = None;
231                let mut event_label = None;
232                let mut category_headers = std::collections::HashMap::new();
233                let mut phrases = std::collections::HashMap::new();
234
235                while let Some(key) = map.next_key::<String>()? {
236                    match key.as_str() {
237                        "vault" => {
238                            if vault.is_some() {
239                                return Err(de::Error::duplicate_field("vault"));
240                            }
241                            vault = Some(map.next_value()?);
242                        }
243                        "file_path_format" => {
244                            if file_path_format.is_some() {
245                                return Err(de::Error::duplicate_field("file_path_format"));
246                            }
247                            file_path_format = Some(map.next_value()?);
248                        }
249                        "section_header" => {
250                            if section_header.is_some() {
251                                return Err(de::Error::duplicate_field("section_header"));
252                            }
253                            section_header = Some(map.next_value()?);
254                        }
255                        "list_type" => {
256                            if list_type.is_some() {
257                                return Err(de::Error::duplicate_field("list_type"));
258                            }
259                            list_type = Some(map.next_value()?);
260                        }
261                        "template_path" => {
262                            if template_path.is_some() {
263                                return Err(de::Error::duplicate_field("template_path"));
264                            }
265                            template_path = Some(map.next_value()?);
266                        }
267                        "locale" => {
268                            if locale.is_some() {
269                                return Err(de::Error::duplicate_field("locale"));
270                            }
271                            locale = Some(map.next_value()?);
272                        }
273                        "time_format" => {
274                            if time_format.is_some() {
275                                return Err(de::Error::duplicate_field("time_format"));
276                            }
277                            time_format = Some(map.next_value()?);
278                        }
279                        "time_label" => {
280                            if time_label.is_some() {
281                                return Err(de::Error::duplicate_field("time_label"));
282                            }
283                            time_label = Some(map.next_value()?);
284                        }
285                        "event_label" => {
286                            if event_label.is_some() {
287                                return Err(de::Error::duplicate_field("event_label"));
288                            }
289                            event_label = Some(map.next_value()?);
290                        }
291                        "phrases" => {
292                            let phrases_map: std::collections::HashMap<String, String> = map.next_value()?;
293                            phrases = phrases_map;
294                        }
295                        _ => {
296                            // Check if this is a category header (starts with "section_header_")
297                            if key.starts_with("section_header_") {
298                                let value: String = map.next_value()?;
299                                category_headers.insert(key, value);
300                            } else {
301                                // Skip unknown fields
302                                let _: serde_yaml::Value = map.next_value()?;
303                            }
304                        }
305                    }
306                }
307
308                Ok(Config {
309                    vault: vault.unwrap_or_default(),
310                    file_path_format: file_path_format.unwrap_or_else(|| {
311                        if cfg!(windows) {
312                            "10-Journal\\{year}\\{month}\\{date}.md".to_string()
313                        } else {
314                            "10-Journal/{year}/{month}/{date}.md".to_string()
315                        }
316                    }),
317                    section_header: section_header.unwrap_or_else(|| "## πŸ•—".to_string()),
318                    list_type: list_type.unwrap_or(ListType::Bullet),
319                    template_path,
320                    locale,
321                    time_format: time_format.unwrap_or_else(default_time_format),
322                    time_label: time_label.unwrap_or_else(default_time_label),
323                    event_label: event_label.unwrap_or_else(default_event_label),
324                    category_headers,
325                    phrases,
326                })
327            }
328        }
329
330        deserializer.deserialize_map(ConfigVisitor)
331    }
332}
333
334impl Default for Config {
335    fn default() -> Self {
336        let vault_dir = env::var("OBSIDIAN_VAULT_DIR").unwrap_or_else(|_| "".to_string());
337        
338        Config {
339            vault: vault_dir,
340            file_path_format: if cfg!(windows) {
341                "10-Journal\\{year}\\{month}\\{date}.md".to_string()
342            } else {
343                "10-Journal/{year}/{month}/{date}.md".to_string()
344            },
345            section_header: "## πŸ•—".to_string(),
346            list_type: ListType::Bullet,
347            template_path: None,
348            locale: None,
349            time_format: TimeFormat::Hour24,
350            time_label: default_time_label(),
351            event_label: default_event_label(),
352            category_headers: std::collections::HashMap::new(),
353            phrases: std::collections::HashMap::new(),
354        }
355    }
356}
357
358impl Config {
359    pub fn with_list_type(&self, list_type: ListType) -> Self {
360        let mut config = self.clone();
361        config.list_type = list_type;
362        config
363    }
364
365    pub fn with_time_format(&self, time_format: TimeFormat) -> Self {
366        let mut config = self.clone();
367        config.time_format = time_format;
368        config
369    }
370
371    /// Get the section header for a specific category
372    /// Returns the default section_header if no category-specific header is found
373    pub fn get_section_header_for_category(&self, category: Option<&str>) -> &str {
374        if let Some(cat) = category {
375            let key = format!("section_header_{}", cat);
376            self.category_headers.get(&key).map(|s| s.as_str()).unwrap_or(&self.section_header)
377        } else {
378            &self.section_header
379        }
380    }
381
382    pub fn initialize() -> Config {
383        let config_dir = get_config_dir();
384        let config_path = config_dir.join("obsidian-logging.yaml");
385
386        // Try to read config file
387        let mut config = if let Ok(config_str) = fs::read_to_string(&config_path) {
388            if let Ok(config) = serde_yaml::from_str(&config_str) {
389                config
390            } else {
391                Config::default()
392            }
393        } else {
394            Config::default()
395        };
396
397        // Override vault setting with environment variable if set
398        if let Ok(vault_dir) = env::var("OBSIDIAN_VAULT_DIR") {
399            config.vault = vault_dir;
400        }
401
402        config
403    }
404}
405
406fn get_config_dir() -> PathBuf {
407    if cfg!(windows) {
408        // On Windows, use %APPDATA%\obsidian-logging
409        let app_data = env::var("APPDATA").expect("APPDATA environment variable not set");
410        PathBuf::from(app_data).join("obsidian-logging")
411    } else {
412        // On Unix, use ~/.config/obsidian-logging
413        let home = env::var("HOME").expect("HOME environment variable not set");
414        PathBuf::from(home).join(".config").join("obsidian-logging")
415    }
416}
417