From 3c309e6088e1112b8aeea362704facac99b4d740 Mon Sep 17 00:00:00 2001 From: core Date: Mon, 23 Mar 2026 12:13:10 -0400 Subject: [PATCH] chore: code cleanup --- src/main.rs | 176 +++++++++++++++++++++++----------------------------- 1 file changed, 79 insertions(+), 97 deletions(-) diff --git a/src/main.rs b/src/main.rs index e0c03dc452ae90f124045ceb1f748d3302e0fc28..7c8277f90ff88ff63a6d10ee3039b2d70bc7c73f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,13 @@ use std::env::args; use std::str::FromStr; + use chrono::{DateTime, Local, NaiveTime, TimeDelta, TimeZone, Utc}; use chrono_tz::Tz; use icalendar::{Calendar, CalendarComponent, CalendarDateTime, Component, DatePerhapsTime, EventLike}; use rrule::RRuleSet; use walkdir::WalkDir; +// convert icalendar's DatePerhapsTime to UTC, treating floating times as local fn to_utc(dpt: DatePerhapsTime) -> Option> { match dpt { DatePerhapsTime::DateTime(cdt) => match cdt { @@ -31,13 +33,12 @@ fn to_utc(dpt: DatePerhapsTime) -> Option> { } } +// reconstruct a DTSTART iCalendar line for feeding into the rrule crate fn dpt_to_dtstart_string(dpt: &DatePerhapsTime) -> String { match dpt { DatePerhapsTime::DateTime(cdt) => match cdt { CalendarDateTime::Utc(dt) => format!("DTSTART:{}", dt.format("%Y%m%dT%H%M%SZ")), - CalendarDateTime::Floating(naive) => { - format!("DTSTART:{}", naive.format("%Y%m%dT%H%M%S")) - } + CalendarDateTime::Floating(naive) => format!("DTSTART:{}", naive.format("%Y%m%dT%H%M%S")), CalendarDateTime::WithTimezone { date_time, tzid } => { format!("DTSTART;TZID={}:{}", tzid, date_time.format("%Y%m%dT%H%M%S")) } @@ -46,16 +47,9 @@ fn dpt_to_dtstart_string(dpt: &DatePerhapsTime) -> String { } } -fn is_imminent(time_delta: TimeDelta) -> bool { - time_delta <= TimeDelta::hours(2) -} -fn is_soon(time_delta: TimeDelta) -> bool { - time_delta <= TimeDelta::hours(6) -} - -fn show_distant_event(time: DateTime, summary: &str) -> String { - format!("{} at {}", summary, time.format("%H:%M")) -} +fn is_imminent(td: TimeDelta) -> bool { td <= TimeDelta::hours(2) } +fn is_soon(td: TimeDelta) -> bool { td <= TimeDelta::hours(6) } +fn is_ending_imminent(td: TimeDelta) -> bool { td <= TimeDelta::minutes(30) } fn format_time_delta(td: TimeDelta) -> String { if td.num_hours() == 0 { @@ -65,41 +59,42 @@ fn format_time_delta(td: TimeDelta) -> String { } } -fn show_imminent(time_delta: TimeDelta, summary: &str, location: Option) -> String { +fn parenthesize_location(location: Option) -> String { + location.map(|u| format!("({}) ", u)).unwrap_or_default() +} + +// "some event at 13:15" +fn show_distant_event(time: DateTime, summary: &str) -> String { + format!("{} at {}", summary, time.format("%H:%M")) +} + +// "some event in 1h7m" or "some event in 1h7m in Room 204" +fn show_imminent(td: TimeDelta, summary: &str, location: Option) -> String { if let Some(loc) = location { - format!("{} in {} in {}", summary, format_time_delta(time_delta), loc) + format!("{} in {} in {}", summary, format_time_delta(td), loc) } else { - format!("{} in {}", summary, format_time_delta(time_delta)) + format!("{} in {}", summary, format_time_delta(td)) } } -fn parenthesize_location(location: Option) -> String { - location.map(|u| format!("({}) ", u)).unwrap_or("".to_string()) +// "Board meeting (Room 217) ending in 10m" +fn show_imminent_ending(td: TimeDelta, summary: &str, location: Option) -> String { + format!("{} {}ending in {}", summary, parenthesize_location(location), format_time_delta(td)) } -fn is_ending_imminent(time_delta: TimeDelta) -> bool { - time_delta <= TimeDelta::minutes(30) -} -fn show_imminent_ending(time_delta: TimeDelta, summary: &str, location: Option) -> String { - format!("{} {}ending in {}", summary, parenthesize_location(location), format_time_delta(time_delta)) -} +// "in Board meeting (Room 217) until 13:00" fn show_distant_ending(end: DateTime, summary: &str, location: Option) -> String { format!("in {} {}until {}", summary, parenthesize_location(location), end.format("%H:%M")) } fn main() { let vdir = args().nth(1).expect("usage: calstr [withloc]"); - let with_loc = args().nth(2) == Some("withloc".to_string()); let now = Utc::now(); let mut upcoming: Vec<(DateTime, DateTime, String, Option)> = Vec::new(); - for entry in WalkDir::new(&vdir) - .follow_links(true) - .into_iter() - .filter_map(|e| e.ok()) - { + for entry in WalkDir::new(&vdir).follow_links(true).into_iter().filter_map(|e| e.ok()) { if !entry.file_name().to_string_lossy().ends_with(".ics") { continue; } @@ -112,87 +107,77 @@ fn main() { Err(_) => continue, }; for component in cal.components { - if let CalendarComponent::Event(event) = component { - let Some(start) = event.get_start() else { - continue; - }; - let Some(end) = event.get_end() else { - continue; - }; - let dtstart_str = dpt_to_dtstart_string(&start); - let Some(dt) = to_utc(start) else { continue }; - let Some(dte) = to_utc(end) else { continue }; - - let summary = event.get_summary().unwrap_or("(no title)").to_string(); - let location = event.get_location().map(|u| u.to_string()); - let loc = if with_loc { location } else { None }; - - if let Some(rrule_str) = event.property_value("RRULE") { - let mut rrule_input = format!("{}\nRRULE:{}", dtstart_str, rrule_str); - if let Some(exdate_prop) = event.properties().get("EXDATE") { - rrule_input.push_str("\nEXDATE"); - for (key, param) in exdate_prop.params() { - rrule_input.push_str(&format!(";{}={}", key, param.value())); - } - rrule_input.push(':'); - rrule_input.push_str(exdate_prop.value()); + let CalendarComponent::Event(event) = component else { continue }; + let Some(start) = event.get_start() else { continue }; + let Some(end) = event.get_end() else { continue }; + let dtstart_str = dpt_to_dtstart_string(&start); + let Some(dt) = to_utc(start) else { continue }; + let Some(dte) = to_utc(end) else { continue }; + + let summary = event.get_summary().unwrap_or("(no title)").to_string(); + let loc = if with_loc { event.get_location().map(|u| u.to_string()) } else { None }; + + if let Some(rrule_str) = event.property_value("RRULE") { + // recurring event: build rrule input and expand occurrences + let mut rrule_input = format!("{}\nRRULE:{}", dtstart_str, rrule_str); + // include EXDATE so deleted single occurrences are skipped + if let Some(exdate_prop) = event.properties().get("EXDATE") { + rrule_input.push_str("\nEXDATE"); + for (key, param) in exdate_prop.params() { + rrule_input.push_str(&format!(";{}={}", key, param.value())); } - if let Ok(rrule_set) = rrule_input.parse::() { - let duration = dte - dt; - let search_from = (now - duration).with_timezone(&rrule::Tz::UTC); - let result = rrule_set.after(search_from).all(5); - for occ in result.dates { - let occ_utc: DateTime = occ.with_timezone(&Utc); - let occ_end = occ_utc + duration; - if occ_end > now { - upcoming.push((occ_utc, occ_end, summary.clone(), loc.clone())); - } + rrule_input.push(':'); + rrule_input.push_str(exdate_prop.value()); + } + if let Ok(rrule_set) = rrule_input.parse::() { + let duration = dte - dt; + // search from (now - duration) to also catch currently-ongoing occurrences + let search_from = (now - duration).with_timezone(&rrule::Tz::UTC); + for occ in rrule_set.after(search_from).all(5).dates { + let occ_start: DateTime = occ.with_timezone(&Utc); + let occ_end = occ_start + duration; + if occ_end > now { + upcoming.push((occ_start, occ_end, summary.clone(), loc.clone())); } } - } else { - if dte > now { - upcoming.push((dt, dte, summary, loc)); - } } + } else if dte > now { + upcoming.push((dt, dte, summary, loc)); } } } upcoming.sort_by_key(|(dt, _, _, _)| *dt); - - // write the message - // "nothing left today" if next event is past midnight or there are no events + if upcoming.is_empty() { println!("nothing left today"); return; } + let next = &upcoming[0]; let next_local = next.0.with_timezone(&Local); - - let now = Utc::now(); - - let mut midnight = Local::now().with_time(NaiveTime::from_hms_milli_opt(0, 0, 0, 0).unwrap()).unwrap(); - midnight += TimeDelta::days(1); - let time_until_next = next.0 - now; + let midnight = Local::now().with_time(NaiveTime::from_hms_milli_opt(0, 0, 0, 0).unwrap()).unwrap() + + TimeDelta::days(1); + + // nothing left today (with optional peek at tomorrow if soon) if next.0 > midnight && !is_imminent(time_until_next) { print!("nothing left today"); - // exception: if event is soon (<6h), include it if is_soon(time_until_next) { - print!(", {} tomorrow", show_distant_event(next_local, &next.2)) + print!(", {} tomorrow", show_distant_event(next_local, &next.2)); } println!(); return; } - // has event started? + // event is ongoing if time_until_next < TimeDelta::zero() { let time_until_end = next.1 - now; let end_local = next.1.with_timezone(&Local); - let second = upcoming.get(1); + // does the next event overlap with the current one? let overlap = second.filter(|(b_start, _, _, _)| *b_start < next.1); let a_fmt = if is_ending_imminent(time_until_end) { @@ -202,8 +187,8 @@ fn main() { }; if let Some((b_start, b_end, b_name, b_loc)) = overlap { - let time_until_b = *b_start - now; if *b_start < now { + // both ongoing — "and" let time_until_b_end = *b_end - now; let b_fmt = if is_ending_imminent(time_until_b_end) { show_imminent_ending(time_until_b_end, b_name, b_loc.clone()) @@ -211,40 +196,37 @@ fn main() { show_distant_ending(b_end.with_timezone(&Local), b_name, b_loc.clone()) }; println!("{}, and {}", a_fmt, b_fmt); - } else if is_imminent(time_until_b) { - println!("{}, with {}", a_fmt, show_imminent(time_until_b, b_name, b_loc.clone())); + } else if is_imminent(*b_start - now) { + // B starting soon — "with" + println!("{}, with {}", a_fmt, show_imminent(*b_start - now, b_name, b_loc.clone())); } else { println!("{}", a_fmt); } return; } + // no overlap: show upcoming event only if current is ending imminently if is_ending_imminent(time_until_end) { if let Some((b_start, _, b_name, b_loc)) = second { - let turnaround_time = *b_start - next.1; - if !is_soon(turnaround_time) { - println!("{}", a_fmt); + let turnaround = *b_start - next.1; + if is_soon(turnaround) { + if is_imminent(turnaround) { + println!("{}, then {}", a_fmt, show_imminent(turnaround, b_name, b_loc.clone())); + } else { + println!("{}, then {}", a_fmt, show_distant_event(b_start.with_timezone(&Local), b_name)); + } return; } - if is_imminent(turnaround_time) { - println!("{}, then {}", a_fmt, show_imminent(turnaround_time, b_name, b_loc.clone())); - } else { - println!("{}, then {}", a_fmt, show_distant_event(b_start.with_timezone(&Local), b_name)); - } - return; } } println!("{}", a_fmt); return; } - // next event is today or imminent (or both) - // is it imminent? + // upcoming event if is_imminent(time_until_next) { println!("{}", show_imminent(time_until_next, &next.2, next.3.clone())); return; } - - // today, show distant println!("{}", show_distant_event(next_local, &next.2)); }