use crate::config::{Config, ListType};
use crate::template::get_template_content;
use crate::utils::{extract_log_entries, format_time, get_log_path_for_date, parse_time};
use chrono::{Duration, Local, NaiveTime, Timelike};
use std::fs::{create_dir_all, read_to_string, write};
fn parse_table_row(line: &str) -> Option<(String, String)> {
let parts: Vec<&str> = line.split('|').collect();
if parts.len() >= 4 {
let time = parts[1].trim();
let entry = parts[2].trim();
if !time.is_empty() && !entry.is_empty() {
return Some((time.to_string(), entry.to_string()));
}
}
None
}
fn parse_bullet_entry(line: &str) -> Option<(String, String)> {
let content = line.trim_start_matches(['-', '*', ' ']);
let time_patterns = [
r"^(\d{1,2}:\d{2}:\d{2})\s+(.+)$",
r"^(\d{1,2}:\d{2})\s+(.+)$",
r"^(\d{1,2}:\d{2}:\d{2}\s+[AaPp][Mm])\s+(.+)$",
r"^(\d{1,2}:\d{2}\s+[AaPp][Mm])\s+(.+)$",
];
for pattern in &time_patterns {
if let Ok(regex) = regex::Regex::new(pattern) {
if let Some(captures) = regex.captures(content) {
let time = captures.get(1).unwrap().as_str().trim();
let entry = captures.get(2).unwrap().as_str().trim();
return Some((time.to_string(), entry.to_string()));
}
}
}
if let Some(space_pos) = content.find(' ') {
let (time, entry) = content.split_at(space_pos);
return Some((time.trim().to_string(), entry.trim().to_string()));
}
None
}
pub fn handle_with_time(
mut args: impl Iterator<Item = String>,
config: &Config,
silent: bool,
category: Option<&str>,
) {
let time_str = args.next().expect("Expected time as first argument");
let mut sentence_parts = Vec::new();
if let Some(next_word) = args.next() {
if next_word.eq_ignore_ascii_case("am") || next_word.eq_ignore_ascii_case("pm") {
let time_with_period = format!("{} {}", time_str, next_word);
if let Some(time) = parse_time(&time_with_period) {
sentence_parts.extend(args);
handle_plain_entry_with_time(sentence_parts, Some(time), config, silent, category);
return;
} else {
sentence_parts.push(time_str);
sentence_parts.push(next_word);
sentence_parts.extend(args);
handle_plain_entry_with_time(sentence_parts, None, config, silent, category);
return;
}
} else {
sentence_parts.push(next_word);
}
}
if let Some(time) = parse_time(&time_str) {
sentence_parts.extend(args);
handle_plain_entry_with_time(sentence_parts, Some(time), config, silent, category);
} else {
sentence_parts.insert(0, time_str);
sentence_parts.extend(args);
handle_plain_entry_with_time(sentence_parts, None, config, silent, category);
}
}
pub fn handle_plain_entry(
first_arg: String,
args: impl Iterator<Item = String>,
config: &Config,
silent: bool,
category: Option<&str>,
) {
let mut sentence_parts = vec![first_arg];
sentence_parts.extend(args);
handle_plain_entry_with_time(sentence_parts, None, config, silent, category);
}
pub fn handle_plain_entry_with_time(
sentence_parts: Vec<String>,
time_override: Option<NaiveTime>,
config: &Config,
silent: bool,
category: Option<&str>,
) {
let sentence = sentence_parts.join(" ");
let now = Local::now();
let date = now.date_naive();
let time = time_override.unwrap_or_else(|| {
NaiveTime::from_hms_opt(now.hour(), now.minute(), now.second()).unwrap()
});
let file_path = get_log_path_for_date(date, config);
create_dir_all(file_path.parent().unwrap()).expect("Could not create log directory");
let is_new_file = !file_path.exists();
let content = if is_new_file {
get_template_content(config)
} else {
read_to_string(&file_path).unwrap_or_default()
};
let section_header = config.get_section_header_for_category(category);
let (before_log, after_log, entries, detected_type) =
extract_log_entries(&content, section_header, &config.list_type, config, false);
let effective_type = if is_new_file || entries.is_empty() {
config.list_type.clone()
} else {
detected_type
};
let parsed_entries: Vec<(String, String)> = entries
.iter()
.filter_map(|e| {
if e.starts_with("| ") {
parse_table_row(e)
} else if e.starts_with("- ") || e.starts_with("* ") {
parse_bullet_entry(e)
} else {
None
}
})
.collect();
let normalized_existing: Vec<(NaiveTime, String)> = parsed_entries
.iter()
.filter_map(|(time_str, entry)| parse_time(time_str).map(|t| (t, entry.clone())))
.collect();
let mut final_time = time;
while normalized_existing
.iter()
.any(|(existing_time, _)| *existing_time == final_time)
{
final_time += Duration::seconds(1);
}
let mut all_entries: Vec<(NaiveTime, String)> = normalized_existing;
all_entries.push((final_time, sentence.clone()));
all_entries.sort_by_key(|a| a.0);
let normalized_entries: Vec<(String, String)> = all_entries
.iter()
.map(|(parsed_time, entry)| {
let normalized_time = format_time(*parsed_time, &config.time_format);
(normalized_time, entry.clone())
})
.collect();
let formatted_entries = match effective_type {
ListType::Bullet => normalized_entries
.into_iter()
.map(|(time, entry)| format!("* {} {}", time, entry))
.collect(),
ListType::Table => {
let mut max_time_width = config.time_label.len();
let mut max_entry_width = config.event_label.len();
for (time, entry) in &normalized_entries {
max_time_width = max_time_width.max(time.len());
max_entry_width = max_entry_width.max(entry.len());
}
let mut table = Vec::new();
table.push(format!(
"| {} | {} |",
config.time_label, config.event_label
));
table.push(format!(
"| {} | {} |",
"-".repeat(max_time_width),
"-".repeat(max_entry_width)
));
table.extend(
normalized_entries
.into_iter()
.map(|(time, entry)| format!("| {} | {} |", time, entry)),
);
table
}
};
let new_content = format!(
"{}{}\n\n{}\n{}",
before_log,
section_header,
formatted_entries.join("\n"),
if after_log.is_empty() {
String::new()
} else {
format!("\n{}", after_log)
}
);
write(&file_path, new_content.trim_end().to_string() + "\n")
.expect("Error writing logs to file");
if !silent {
println!("Logged.");
}
}