use chrono::{Duration, Local};
use temporis;
use std::io::{self, Write};
use crate::storage::files;
use crate::storage::ics;
use crate::calendar::events::{Event, EventList};
use crate::display::{list, calendar, event as event_display};
pub fn handle_list(
query: Vec<String>,
calendar: Option<String>,
from: Option<String>,
to: Option<String>,
limit: Option<usize>,
show_ids: bool
) -> Result<(), String> {
let events = load_events_from_calendar(calendar)?;
let from_date = if let Some(from_str) = from {
parse_date(&from_str)?
} else {
chrono::Local::now().date_naive()
};
let to_date = if let Some(to_str) = to {
parse_date(&to_str)?
} else {
from_date + chrono::Duration::days(3000)
};
let query_string = if query.is_empty() {
None
} else {
Some(query.join(" "))
};
let options = list::ListOptions {
show_ids,
from_date,
to_date,
query: query_string,
limit,
};
list::show_events(&events, &options);
Ok(())
}
pub fn handle_add(
name: Vec<String>,
at: String,
to: Option<String>,
calendar: Option<String>,
location: Option<String>,
description: Option<String>,
repeat: Option<String>,
every: Option<u32>,
until: Option<String>,
) -> Result<(), String> {
let calendar_name = calendar.unwrap_or_else(|| "personal".to_string());
let event_name = name.join(" ");
if event_name.trim().is_empty() {
return Err("Event name cannot be empty".to_string());
}
if repeat.is_some() || every.is_some() || until.is_some() {
return Err("Recurring events not yet implemented in new version".to_string());
}
ensure_calendar_exists(&calendar_name)?;
let start = parse_datetime(&at)?;
let end = if let Some(to_str) = to {
parse_datetime(&to_str)?
} else {
start + Duration::hours(1)
};
if end <= start {
return Err("End time must be after start time".to_string());
}
let event = Event::new_with_details(event_name, start, end, location, description);
save_event_to_calendar(&event, &calendar_name)?;
println!("Event added successfully");
Ok(())
}
pub fn handle_edit(
id: String,
calendar_name: String,
name: Option<String>,
at: Option<String>,
to: Option<String>,
location: Option<String>,
description: Option<String>,
) -> Result<(), String> {
let calendar_path = files::calendar_path(&calendar_name)?;
let mut events = load_events_from_calendar(Some(calendar_name.clone()))?;
let event = events.find_by_id_mut(&id)
.ok_or_else(|| format!("Event with ID '{}' not found", id))?;
let new_start = if let Some(at_str) = at {
Some(parse_datetime(&at_str)?)
} else {
None
};
let new_end = if let Some(to_str) = to {
Some(parse_datetime(&to_str)?)
} else {
None
};
if let (Some(start), Some(end)) = (new_start, new_end) {
if end <= start {
return Err("End time must be after start time".to_string());
}
}
event.update(name, new_start, new_end, location, description);
let original_file_path = files::find_event_file(&calendar_path, &id)?;
ics::save_event_to_file(event, &original_file_path)?;
println!("Event updated successfully");
Ok(())
}
pub fn handle_delete(id: String, calendar_name: String, force: bool) -> Result<(), String> {
let calendar_path = files::calendar_path(&calendar_name)?;
let events = load_events_from_calendar(Some(calendar_name))?;
let event = events.find_by_id(&id)
.ok_or_else(|| format!("Event with ID '{}' not found", id))?;
let base_id = if id.contains('-') {
let parts: Vec<&str> = id.split('-').collect();
if parts.len() > 4 && parts.last().unwrap().parse::<u32>().is_ok() {
parts[0..parts.len()-1].join("-")
} else {
id.clone()
}
} else {
id.clone()
};
if !force {
if base_id != id {
println!("This is instance #{} of a recurring event.", id.split('-').last().unwrap());
println!("Deleting will remove the ENTIRE recurring series:");
}
event_display::show_event_for_deletion(event);
print!("Are you sure? (y/N) ");
io::stdout().flush().map_err(|e| format!("IO error: {}", e))?;
let mut input = String::new();
io::stdin().read_line(&mut input).map_err(|e| format!("IO error: {}", e))?;
if input.trim().to_lowercase() != "y" {
println!("Deletion cancelled");
return Ok(());
}
}
files::delete_ics_file(&calendar_path, &base_id)?;
if base_id != id {
println!("Entire recurring series deleted successfully");
} else {
println!("Event deleted successfully");
}
Ok(())
}
pub fn handle_show(id: String, calendar_name: String) -> Result<(), String> {
let events = load_events_from_calendar(Some(calendar_name))?;
let event = events.find_by_id(&id)
.ok_or_else(|| format!("Event with ID '{}' not found", id))?;
event_display::show_event_details(event);
Ok(())
}
pub fn handle_view(
date: Option<String>,
mode: crate::args::ViewMode,
calendar_name: Option<String>,
number: Option<u32>,
) -> Result<(), String> {
let events = load_events_from_calendar(calendar_name)?;
let target_date = if let Some(date_str) = date {
parse_date(&date_str)?
} else {
Local::now().date_naive()
};
let options = calendar::CalendarOptions {
date: target_date,
mode, number: number.unwrap_or(1),
};
calendar::show_calendar(&events, &options);
Ok(())
}
fn load_events_from_calendar(calendar_name: Option<String>) -> Result<EventList, String> {
if let Some(name) = calendar_name {
let calendar_path = files::calendar_path(&name)?;
let ics_files = files::list_ics_files(&calendar_path)?;
let mut all_events = Vec::new();
for file_path in ics_files {
let mut events = ics::load_events_from_file(&file_path)?;
all_events.append(&mut events);
}
Ok(EventList::from_events(all_events))
} else {
let calendar_names = files::list_calendar_names()?;
let mut all_events = Vec::new();
for name in calendar_names {
let calendar_path = files::calendar_path(&name)?;
let ics_files = files::list_ics_files(&calendar_path)?;
for file_path in ics_files {
let mut events = ics::load_events_from_file(&file_path)?;
all_events.append(&mut events);
}
}
Ok(EventList::from_events(all_events))
}
}
fn save_event_to_calendar(event: &Event, calendar_name: &str) -> Result<(), String> {
let calendar_path = files::calendar_path(calendar_name)?;
let file_path = calendar_path.join(format!("{}.ics", event.id));
ics::save_event_to_file(event, &file_path)
}
fn ensure_calendar_exists(name: &str) -> Result<(), String> {
if name == "personal" {
files::ensure_personal_calendar()?;
} else {
if files::calendar_path(name).is_err() {
files::create_calendar(name)?;
}
}
Ok(())
}
fn parse_date(date_str: &str) -> Result<chrono::NaiveDate, String> {
temporis::parse_date(date_str)
.map_err(|e| format!("Invalid date '{}': {}", date_str, e))
}
fn parse_datetime(datetime_str: &str) -> Result<chrono::NaiveDateTime, String> {
if datetime_str.contains('@') {
let parts: Vec<&str> = datetime_str.split('@').collect();
if parts.len() != 2 {
return Err("Datetime must be in 'date@time' format".to_string());
}
let date = parse_date(parts[0])?;
let time = parse_time(parts[1])?;
Ok(date.and_time(time))
} else {
Err("Datetime must contain '@' separator (e.g., 'tomorrow@2pm')".to_string())
}
}
fn parse_time(time_str: &str) -> Result<chrono::NaiveTime, String> {
if let Ok(time) = chrono::NaiveTime::parse_from_str(time_str, "%H:%M") {
return Ok(time);
}
if let Ok(hour) = time_str.parse::<u32>() {
if hour < 24 {
return chrono::NaiveTime::from_hms_opt(hour, 0, 0)
.ok_or("Invalid hour".to_string());
}
}
Err(format!("Invalid time format: '{}'. Use HH:MM or just hour", time_str))
}