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 walkdir::WalkDir;
fn to_utc(dpt: DatePerhapsTime) -> Option<DateTime<Utc>> {
match dpt {
DatePerhapsTime::DateTime(cdt) => match cdt {
CalendarDateTime::Utc(dt) => Some(dt),
CalendarDateTime::Floating(naive) => Local
.from_local_datetime(&naive)
.earliest()
.map(|dt| dt.with_timezone(&Utc)),
CalendarDateTime::WithTimezone { date_time, tzid } => {
let tz = tzid.parse::<Tz>().ok()?;
tz.from_local_datetime(&date_time)
.earliest()
.map(|dt| dt.with_timezone(&Utc))
}
},
DatePerhapsTime::Date(date) => {
let naive = date.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap());
Local
.from_local_datetime(&naive)
.earliest()
.map(|dt| dt.with_timezone(&Utc))
}
}
}
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<Local>, summary: &str) -> String {
format!("{} at {}", summary, time.format("%H:%M"))
}
fn format_time_delta(td: TimeDelta) -> String {
if td.num_hours() == 0 {
format!("{}m", td.num_minutes())
} else {
format!("{}h{}m", td.num_hours(), td.num_minutes() % 60)
}
}
fn show_imminent(time_delta: TimeDelta, summary: &str, location: Option<String>) -> String {
if let Some(loc) = location {
format!("{} in {} in {}", summary, format_time_delta(time_delta), loc)
} else {
format!("{} in {}", summary, format_time_delta(time_delta))
}
}
fn parenthesize_location(location: Option<String>) -> String {
location.map(|u| format!("({}) ", u)).unwrap_or("".to_string())
}
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>) -> String {
format!("{} {}ending in {}", summary, parenthesize_location(location), format_time_delta(time_delta))
}
fn show_distant_ending(end: DateTime<Local>, summary: &str, location: Option<String>) -> String {
format!("in {} {}until {}", summary, parenthesize_location(location), end.format("%H:%M"))
}
fn main() {
let vdir = args().nth(1).expect("usage: calstr <vdir> [withloc]");
let with_loc = args().nth(2) == Some("withloc".to_string());
let now = Utc::now();
let mut upcoming: Vec<(DateTime<Utc>, DateTime<Utc>, String, Option<String>)> = Vec::new();
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;
}
let content = match std::fs::read_to_string(entry.path()) {
Ok(c) => c,
Err(_) => continue,
};
let cal = match Calendar::from_str(&content) {
Ok(c) => c,
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 Some(dt) = to_utc(start) else { continue };
let Some(dte) = to_utc(end) else { continue };
if dte > now {
let summary = event.get_summary().unwrap_or("(no title)").to_string();
let location = event.get_location().map(|u| u.to_string());
upcoming.push((dt, dte, summary, if with_loc { location } else { None }));
}
}
}
}
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;
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))
}
println!();
return;
}
// has event started?
if time_until_next < TimeDelta::zero() {
let time_until_end = next.1 - now;
let end_local = next.1.with_timezone(&Local);
// is end imminent
if is_ending_imminent(time_until_end) {
let followup = upcoming.get(1);
if let Some((following_start, _, following, following_loc)) = followup {
print!("{}", show_imminent_ending(time_until_end, &next.2, next.3.clone()));
let turnaround_time = *following_start - next.1;
if !is_soon(turnaround_time) {
println!();
return;
}
if is_imminent(turnaround_time) {
println!(", then {}", show_imminent(turnaround_time, following, following_loc.clone()));
return;
} else {
println!(", then {}", show_distant_event(following_start.with_timezone(&Local), following));
return;
}
} else {
println!("{}", show_imminent_ending(time_until_end, &next.2, next.3.clone()));
return;
}
} else {
println!("{}", show_distant_ending(end_local, &next.2, next.3.clone()));
return;
}
}
// next event is today or imminent (or both)
// is it imminent?
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));
}