obsidian_logging/commands/
add.rs1use chrono::{Local, NaiveTime, Timelike};
2use std::fs::{create_dir_all, read_to_string, write};
3use crate::config::{Config, ListType};
4use crate::utils::{get_log_path_for_date, extract_log_entries, format_time, parse_time};
5use crate::template::get_template_content;
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(|c| c == '-' || c == '*' || c == ' ');
23
24 let time_patterns = [
27 r"^(\d{1,2}:\d{2})\s+(.+)$",
29 r"^(\d{1,2}:\d{2}\s+[AaPp][Mm])\s+(.+)$",
31 ];
32
33 for pattern in &time_patterns {
34 if let Ok(regex) = regex::Regex::new(pattern) {
35 if let Some(captures) = regex.captures(content) {
36 let time = captures.get(1).unwrap().as_str().trim();
37 let entry = captures.get(2).unwrap().as_str().trim();
38 return Some((time.to_string(), entry.to_string()));
39 }
40 }
41 }
42
43 if let Some(space_pos) = content.find(' ') {
45 let (time, entry) = content.split_at(space_pos);
46 return Some((time.trim().to_string(), entry.trim().to_string()));
47 }
48
49 None
50}
51
52pub fn handle_with_time(mut args: impl Iterator<Item=String>, config: &Config, silent: bool, category: Option<&str>) {
53 let time_str = args.next().expect("Expected time as first argument");
54 let mut sentence_parts = Vec::new();
55
56 if let Some(next_word) = args.next() {
58 if next_word.eq_ignore_ascii_case("am") || next_word.eq_ignore_ascii_case("pm") {
59 let time_with_period = format!("{} {}", time_str, next_word);
60 if let Some(time) = parse_time(&time_with_period) {
61 sentence_parts.extend(args);
62 handle_plain_entry_with_time(sentence_parts, Some(time), config, silent, category);
63 return;
64 } else {
65 sentence_parts.push(time_str);
67 sentence_parts.push(next_word);
68 sentence_parts.extend(args);
69 handle_plain_entry_with_time(sentence_parts, None, config, silent, category);
70 return;
71 }
72 } else {
73 sentence_parts.push(next_word);
74 }
75 }
76
77 if let Some(time) = parse_time(&time_str) {
79 sentence_parts.extend(args);
80 handle_plain_entry_with_time(sentence_parts, Some(time), config, silent, category);
81 } else {
82 sentence_parts.insert(0, time_str);
84 sentence_parts.extend(args);
85 handle_plain_entry_with_time(sentence_parts, None, config, silent, category);
86 }
87}
88
89pub fn handle_plain_entry(first_arg: String, args: impl Iterator<Item=String>, config: &Config, silent: bool, category: Option<&str>) {
90 let mut sentence_parts = vec![first_arg];
91 sentence_parts.extend(args);
92 handle_plain_entry_with_time(sentence_parts, None, config, silent, category);
93}
94
95pub fn handle_plain_entry_with_time(sentence_parts: Vec<String>, time_override: Option<NaiveTime>, config: &Config, silent: bool, category: Option<&str>) {
96 let sentence = sentence_parts.join(" ");
97 let now = Local::now();
98 let date = now.date_naive();
99 let time = time_override.unwrap_or_else(|| NaiveTime::from_hms_opt(now.hour(), now.minute(), 0).unwrap());
100 let time_str = format_time(time, &config.time_format);
101
102 let file_path = get_log_path_for_date(date, config);
103 create_dir_all(file_path.parent().unwrap()).expect("Could not create log directory");
104
105 let is_new_file = !file_path.exists();
106 let content = if is_new_file {
107 get_template_content(config)
108 } else {
109 read_to_string(&file_path).unwrap_or_default()
110 };
111
112 let section_header = config.get_section_header_for_category(category);
113 let (before_log, after_log, entries, detected_type) = extract_log_entries(&content, section_header, &config.list_type, config, false);
114
115 let effective_type = if is_new_file {
118 config.list_type.clone()
119 } else if entries.is_empty() {
120 config.list_type.clone()
121 } else {
122 detected_type
123 };
124
125 let mut parsed_entries: Vec<(String, String)> = entries.iter()
127 .filter_map(|e| {
128 if e.starts_with("| ") {
129 parse_table_row(e)
130 } else if e.starts_with("- ") || e.starts_with("* ") {
131 parse_bullet_entry(e)
132 } else {
133 None
134 }
135 })
136 .collect();
137
138 parsed_entries.push((time_str, sentence));
140
141 parsed_entries.sort_by(|a, b| {
143 let time_a = parse_time(&a.0).unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
144 let time_b = parse_time(&b.0).unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
145 time_a.cmp(&time_b)
146 });
147
148 let formatted_entries = match effective_type {
150 ListType::Bullet => {
151 parsed_entries.into_iter()
152 .map(|(time, entry)| format!("* {} {}", time, entry))
153 .collect()
154 }
155 ListType::Table => {
156 let mut max_time_width = config.time_label.len();
158 let mut max_entry_width = config.event_label.len();
159
160 for (time, entry) in &parsed_entries {
161 max_time_width = max_time_width.max(time.len());
162 max_entry_width = max_entry_width.max(entry.len());
163 }
164
165 let mut table = Vec::new();
167 table.push(format!("| {} | {} |", config.time_label, config.event_label));
169 table.push(format!("| {} | {} |",
170 "-".repeat(max_time_width),
171 "-".repeat(max_entry_width)
172 ));
173 table.extend(parsed_entries.into_iter().map(|(time, entry)| {
174 format!("| {} | {} |", time, entry)
175 }));
176 table
177 }
178 };
179
180 let new_content = format!(
181 "{}{}\n\n{}\n{}",
182 before_log,
183 section_header,
184 formatted_entries.join("\n"),
185 if after_log.is_empty() {
186 String::new()
187 } else {
188 format!("\n{}", after_log)
189 }
190 );
191
192 write(&file_path, new_content.trim_end().to_string() + "\n").expect("Error writing logs to file");
193
194 if !silent {
195 println!("Logged.");
196 }
197}
198
199
200