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 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> = map.next_value()?;
293 phrases = phrases_map;
294 }
295 _ => {
296 if key.starts_with("section_header_") {
298 let value: String = map.next_value()?;
299 category_headers.insert(key, value);
300 } else {
301 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 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 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 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}
417