1use serde::{Deserialize, Serialize};
2use std::env;
3use std::fs;
4use std::path::PathBuf;
5use std::str::FromStr;
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 std::fmt::Display for ListType {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 match self {
139 ListType::Bullet => write!(f, "bullet"),
140 ListType::Table => write!(f, "table"),
141 }
142 }
143}
144
145impl std::fmt::Display for TimeFormat {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 match self {
148 TimeFormat::Hour12 => write!(f, "12"),
149 TimeFormat::Hour24 => write!(f, "24"),
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 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", }
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> =
293 map.next_value()?;
294 phrases = phrases_map;
295 }
296 _ => {
297 if key.starts_with("section_header_") {
299 let value: String = map.next_value()?;
300 category_headers.insert(key, value);
301 } else {
302 let _: serde_yaml::Value = map.next_value()?;
304 }
305 }
306 }
307 }
308
309 Ok(Config {
310 vault: vault.unwrap_or_default(),
311 file_path_format: file_path_format.unwrap_or_else(|| {
312 if cfg!(windows) {
313 "10-Journal\\{year}\\{month}\\{date}.md".to_string()
314 } else {
315 "10-Journal/{year}/{month}/{date}.md".to_string()
316 }
317 }),
318 section_header: section_header.unwrap_or_else(|| "## π".to_string()),
319 list_type: list_type.unwrap_or(ListType::Bullet),
320 template_path,
321 locale,
322 time_format: time_format.unwrap_or_else(default_time_format),
323 time_label: time_label.unwrap_or_else(default_time_label),
324 event_label: event_label.unwrap_or_else(default_event_label),
325 category_headers,
326 phrases,
327 })
328 }
329 }
330
331 deserializer.deserialize_map(ConfigVisitor)
332 }
333}
334
335impl Default for Config {
336 fn default() -> Self {
337 let vault_dir = env::var("OBSIDIAN_VAULT_DIR").unwrap_or_else(|_| "".to_string());
338
339 Config {
340 vault: vault_dir,
341 file_path_format: if cfg!(windows) {
342 "10-Journal\\{year}\\{month}\\{date}.md".to_string()
343 } else {
344 "10-Journal/{year}/{month}/{date}.md".to_string()
345 },
346 section_header: "## π".to_string(),
347 list_type: ListType::Bullet,
348 template_path: None,
349 locale: None,
350 time_format: TimeFormat::Hour24,
351 time_label: default_time_label(),
352 event_label: default_event_label(),
353 category_headers: std::collections::HashMap::new(),
354 phrases: std::collections::HashMap::new(),
355 }
356 }
357}
358
359impl Config {
360 pub fn with_list_type(&self, list_type: ListType) -> Self {
361 let mut config = self.clone();
362 config.list_type = list_type;
363 config
364 }
365
366 pub fn with_time_format(&self, time_format: TimeFormat) -> Self {
367 let mut config = self.clone();
368 config.time_format = time_format;
369 config
370 }
371
372 pub fn get_section_header_for_category(&self, category: Option<&str>) -> &str {
375 if let Some(cat) = category {
376 let key = format!("section_header_{}", cat);
377 self.category_headers
378 .get(&key)
379 .map(|s| s.as_str())
380 .unwrap_or(&self.section_header)
381 } else {
382 &self.section_header
383 }
384 }
385
386 pub fn initialize() -> Config {
387 let config_dir = get_config_dir();
388 let config_path = config_dir.join("obsidian-logging.yaml");
389
390 let mut config: Config = if let Ok(config_str) = fs::read_to_string(&config_path) {
392 serde_yaml::from_str(&config_str).unwrap_or_default()
393 } else {
394 Config::default()
395 };
396
397 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 let app_data = env::var("APPDATA").expect("APPDATA environment variable not set");
410 PathBuf::from(app_data).join("obsidian-logging")
411 } else {
412 let home = env::var("HOME").expect("HOME environment variable not set");
414 PathBuf::from(home).join(".config").join("obsidian-logging")
415 }
416}