obsidian_logging/commands/
add.rs1use crate::config::{Config, ListType};
2use crate::template::get_template_content;
3use crate::utils::{extract_log_entries, format_time, get_log_path_for_date, parse_time};
4use chrono::{Duration, Local, NaiveTime, Timelike};
5use std::fs::{create_dir_all, read_to_string, write};
6
7fn parse_table_row(line: &str) -> Option<(String, String)> {
9 let parts: Vec<&str> = line.split('|').collect();
10 if parts.len() >= 4 {
11 let time = parts[1].trim();
12 let entry = parts[2].trim();
13 if !time.is_empty() && !entry.is_empty() {
14 return Some((time.to_string(), entry.to_string()));
15 }
16 }
17 None
18}
19
20fn parse_bullet_entry(line: &str) -> Option<(String, String)> {
22 let content = line.trim_start_matches(['-', '*', ' ']);
23
24 let time_patterns = [
27 r"^(\d{1,2}:\d{2}:\d{2})\s+(.+)$",
29 r"^(\d{1,2}:\d{2})\s+(.+)$",
31 r"^(\d{1,2}:\d{2}:\d{2}\s+[AaPp][Mm])\s+(.+)$",
33 r"^(\d{1,2}:\d{2}\s+[AaPp][Mm])\s+(.+)$",
35 ];
36
37 for pattern in &time_patterns {
38 if let Ok(regex) = regex::Regex::new(pattern) {
39 if let Some(captures) = regex.captures(content) {
40 let time = captures.get(1).unwrap().as_str().trim();
41 let entry = captures.get(2).unwrap().as_str().trim();
42 return Some((time.to_string(), entry.to_string()));
43 }
44 }
45 }
46
47 if let Some(space_pos) = content.find(' ') {
49 let (time, entry) = content.split_at(space_pos);
50 return Some((time.trim().to_string(), entry.trim().to_string()));
51 }
52
53 None
54}
55
56pub fn handle_with_time(
57 mut args: impl Iterator<Item = String>,
58 config: &Config,
59 silent: bool,
60 category: Option<&str>,
61) {
62 let time_str = args.next().expect("Expected time as first argument");
63 let mut sentence_parts = Vec::new();
64
65 if let Some(next_word) = args.next() {
67 if next_word.eq_ignore_ascii_case("am") || next_word.eq_ignore_ascii_case("pm") {
68 let time_with_period = format!("{} {}", time_str, next_word);
69 if let Some(time) = parse_time(&time_with_period) {
70 sentence_parts.extend(args);
71 handle_plain_entry_with_time(sentence_parts, Some(time), config, silent, category);
72 return;
73 } else {
74 sentence_parts.push(time_str);
76 sentence_parts.push(next_word);
77 sentence_parts.extend(args);
78 handle_plain_entry_with_time(sentence_parts, None, config, silent, category);
79 return;
80 }
81 } else {
82 sentence_parts.push(next_word);
83 }
84 }
85
86 if let Some(time) = parse_time(&time_str) {
88 sentence_parts.extend(args);
89 handle_plain_entry_with_time(sentence_parts, Some(time), config, silent, category);
90 } else {
91 sentence_parts.insert(0, time_str);
93 sentence_parts.extend(args);
94 handle_plain_entry_with_time(sentence_parts, None, config, silent, category);
95 }
96}
97
98pub fn handle_plain_entry(
99 first_arg: String,
100 args: impl Iterator<Item = String>,
101 config: &Config,
102 silent: bool,
103 category: Option<&str>,
104) {
105 let mut sentence_parts = vec![first_arg];
106 sentence_parts.extend(args);
107 handle_plain_entry_with_time(sentence_parts, None, config, silent, category);
108}
109
110pub fn handle_plain_entry_with_time(
111 sentence_parts: Vec<String>,
112 time_override: Option<NaiveTime>,
113 config: &Config,
114 silent: bool,
115 category: Option<&str>,
116) {
117 let sentence = sentence_parts.join(" ");
118 let now = Local::now();
119 let date = now.date_naive();
120 let time = time_override.unwrap_or_else(|| {
121 NaiveTime::from_hms_opt(now.hour(), now.minute(), now.second()).unwrap()
122 });
123
124 let file_path = get_log_path_for_date(date, config);
125 create_dir_all(file_path.parent().unwrap()).expect("Could not create log directory");
126
127 let is_new_file = !file_path.exists();
128 let content = if is_new_file {
129 get_template_content(config)
130 } else {
131 read_to_string(&file_path).unwrap_or_default()
132 };
133
134 let section_header = config.get_section_header_for_category(category);
135 let (before_log, after_log, entries, detected_type) =
136 extract_log_entries(&content, section_header, &config.list_type, config, false);
137
138 let effective_type = if is_new_file || entries.is_empty() {
141 config.list_type.clone()
142 } else {
143 detected_type
144 };
145
146 let parsed_entries: Vec<(String, String)> = entries
148 .iter()
149 .filter_map(|e| {
150 if e.starts_with("| ") {
151 parse_table_row(e)
152 } else if e.starts_with("- ") || e.starts_with("* ") {
153 parse_bullet_entry(e)
154 } else {
155 None
156 }
157 })
158 .collect();
159
160 let normalized_existing: Vec<(NaiveTime, String)> = parsed_entries
163 .iter()
164 .filter_map(|(time_str, entry)| parse_time(time_str).map(|t| (t, entry.clone())))
165 .collect();
166
167 let mut final_time = time;
169
170 while normalized_existing
173 .iter()
174 .any(|(existing_time, _)| *existing_time == final_time)
175 {
176 final_time += Duration::seconds(1);
178 }
179
180 let mut all_entries: Vec<(NaiveTime, String)> = normalized_existing;
183 all_entries.push((final_time, sentence.clone()));
184
185 all_entries.sort_by(|a, b| a.0.cmp(&b.0));
187
188 let normalized_entries: Vec<(String, String)> = all_entries
191 .iter()
192 .map(|(parsed_time, entry)| {
193 let normalized_time = format_time(*parsed_time, &config.time_format);
194 (normalized_time, entry.clone())
195 })
196 .collect();
197
198 let formatted_entries = match effective_type {
200 ListType::Bullet => normalized_entries
201 .into_iter()
202 .map(|(time, entry)| format!("* {} {}", time, entry))
203 .collect(),
204 ListType::Table => {
205 let mut max_time_width = config.time_label.len();
207 let mut max_entry_width = config.event_label.len();
208
209 for (time, entry) in &normalized_entries {
210 max_time_width = max_time_width.max(time.len());
211 max_entry_width = max_entry_width.max(entry.len());
212 }
213
214 let mut table = Vec::new();
216 table.push(format!(
218 "| {} | {} |",
219 config.time_label, config.event_label
220 ));
221 table.push(format!(
222 "| {} | {} |",
223 "-".repeat(max_time_width),
224 "-".repeat(max_entry_width)
225 ));
226 table.extend(
227 normalized_entries
228 .into_iter()
229 .map(|(time, entry)| format!("| {} | {} |", time, entry)),
230 );
231 table
232 }
233 };
234
235 let new_content = format!(
236 "{}{}\n\n{}\n{}",
237 before_log,
238 section_header,
239 formatted_entries.join("\n"),
240 if after_log.is_empty() {
241 String::new()
242 } else {
243 format!("\n{}", after_log)
244 }
245 );
246
247 write(&file_path, new_content.trim_end().to_string() + "\n")
248 .expect("Error writing logs to file");
249
250 if !silent {
251 println!("Logged.");
252 }
253}