@@ 3,6 3,15 @@
version = 4
[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 42,6 51,7 @@ dependencies = [
"chrono",
"chrono-tz",
"icalendar",
+ "rrule",
"walkdir",
]
@@ 332,6 342,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "rrule"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "720acfb4980b9d8a6a430f6d7a11933e701ebbeba5eee39cc9d8c5f932aaff74"
+dependencies = [
+ "chrono",
+ "chrono-tz",
+ "log",
+ "regex",
+ "thiserror",
+]
+
+[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 418,6 470,26 @@ dependencies = [
]
[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 1,9 1,9 @@
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;
fn to_utc(dpt: DatePerhapsTime) -> Option<DateTime<Utc>> {
@@ 31,6 31,21 @@ fn to_utc(dpt: DatePerhapsTime) -> Option<DateTime<Utc>> {
}
}
+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::WithTimezone { date_time, tzid } => {
+ format!("DTSTART;TZID={}:{}", tzid, date_time.format("%Y%m%dT%H%M%S"))
+ }
+ },
+ DatePerhapsTime::Date(date) => format!("DTSTART;VALUE=DATE:{}", date.format("%Y%m%d")),
+ }
+}
+
fn is_imminent(time_delta: TimeDelta) -> bool {
time_delta <= TimeDelta::hours(2)
}
@@ 104,19 119,47 @@ fn main() {
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 };
- 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 }));
+
+ 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());
+ }
+ if let Ok(rrule_set) = rrule_input.parse::<RRuleSet>() {
+ 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<Utc> = occ.with_timezone(&Utc);
+ let occ_end = occ_utc + duration;
+ if occ_end > now {
+ upcoming.push((occ_utc, occ_end, summary.clone(), loc.clone()));
+ }
+ }
+ }
+ } 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() {
@@ 147,33 190,52 @@ fn main() {
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;
+ let second = upcoming.get(1);
+
+ let overlap = second.filter(|(b_start, _, _, _)| *b_start < next.1);
+
+ let a_fmt = if is_ending_imminent(time_until_end) {
+ show_imminent_ending(time_until_end, &next.2, next.3.clone())
+ } else {
+ show_distant_ending(end_local, &next.2, next.3.clone())
+ };
+
+ if let Some((b_start, b_end, b_name, b_loc)) = overlap {
+ let time_until_b = *b_start - now;
+ if *b_start < now {
+ 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())
+ } else {
+ 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 {
+ println!("{}", a_fmt);
+ }
+ return;
+ }
+
+ 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!();
+ println!("{}", a_fmt);
return;
}
-
if is_imminent(turnaround_time) {
- println!(", then {}", show_imminent(turnaround_time, following, following_loc.clone()));
- return;
+ println!("{}, then {}", a_fmt, show_imminent(turnaround_time, b_name, b_loc.clone()));
} else {
- println!(", then {}", show_distant_event(following_start.with_timezone(&Local), following));
- return;
+ println!("{}, then {}", a_fmt, show_distant_event(b_start.with_timezone(&Local), b_name));
}
- } 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;
}
+ println!("{}", a_fmt);
+ return;
}
// next event is today or imminent (or both)