This commit is contained in:
Mikhail Kilin
2026-06-21 16:22:06 +03:00
parent 698c953c55
commit 8cedd606f5
58 changed files with 4333 additions and 146 deletions

View File

@@ -0,0 +1,160 @@
use std::{
collections::BTreeSet,
process::{Command, Stdio},
thread::sleep,
time::{Duration, Instant},
};
use chrono::{Datelike, Local, NaiveDate, Timelike};
use serde_json::json;
const COMMAND_TIMEOUT: Duration = Duration::from_secs(5);
const MONTH_NAMES: [&str; 13] = [
"",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
fn run_khal(args: &[&str]) -> Option<String> {
let mut child = Command::new("khal")
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.ok()?;
let deadline = Instant::now() + COMMAND_TIMEOUT;
loop {
match child.try_wait() {
Ok(Some(_status)) => {
let output = child.wait_with_output().ok()?;
return Some(String::from_utf8_lossy(&output.stdout).trim().to_owned());
}
Ok(None) if Instant::now() >= deadline => {
let _ = child.kill();
let _ = child.wait();
return None;
}
Ok(None) => sleep(Duration::from_millis(25)),
Err(_) => return None,
}
}
}
fn days_in_month(year: i32, month: u32) -> u32 {
let (next_year, next_month) = if month == 12 {
(year + 1, 1)
} else {
(year, month + 1)
};
NaiveDate::from_ymd_opt(next_year, next_month, 1)
.and_then(|date| date.pred_opt())
.map_or(31, |date| date.day())
}
fn get_event_days(year: i32, month: u32) -> BTreeSet<u32> {
let Some(first) = NaiveDate::from_ymd_opt(year, month, 1) else {
return BTreeSet::new();
};
let Some(last) = NaiveDate::from_ymd_opt(year, month, days_in_month(year, month)) else {
return BTreeSet::new();
};
let start = first.format("%d.%m.%Y").to_string();
let end = last.format("%d.%m.%Y").to_string();
let Some(stdout) = run_khal(&[
"list",
&start,
&end,
"--format",
"{start-date}",
"--day-format",
"",
]) else {
return BTreeSet::new();
};
stdout
.lines()
.filter_map(|line| NaiveDate::parse_from_str(line.trim(), "%d.%m.%Y").ok())
.filter(|date| date.month() == month)
.map(|date| date.day())
.collect()
}
fn build_calendar(year: i32, month: u32, event_days: &BTreeSet<u32>, today: NaiveDate) -> String {
let Some(first) = NaiveDate::from_ymd_opt(year, month, 1) else {
return String::new();
};
let month_name = MONTH_NAMES
.get(month as usize)
.copied()
.unwrap_or("Unknown");
let mut lines = vec![
format!(r##"<span color="#1e66f5"><b>{month_name} {year}</b></span>"##),
r##"<span color="#8839ef"><b>Mo Tu We Th Fr Sa Su</b></span>"##.to_owned(),
];
let mut week = vec![" ".to_owned(); (first.weekday().number_from_monday() - 1) as usize];
for day in 1..=days_in_month(year, month) {
let day_string = if day == today.day() && month == today.month() && year == today.year() {
format!(r##"<span color="#d20f39"><b><u>{day:2}</u></b></span>"##)
} else if event_days.contains(&day) {
format!(r##"<span color="#40a02b"><b>{day:2}</b></span>"##)
} else {
format!(r##"<span color="#4c4f69">{day:2}</span>"##)
};
week.push(day_string);
if week.len() == 7 {
lines.push(week.join(" "));
week.clear();
}
}
if !week.is_empty() {
week.resize(7, " ".to_owned());
lines.push(week.join(" "));
}
lines.join("\n")
}
fn get_upcoming_events() -> String {
run_khal(&["list", "today", "7d"]).unwrap_or_default()
}
fn main() {
let now = Local::now();
let today = now.date_naive();
let event_days = get_event_days(today.year(), today.month());
let calendar = build_calendar(today.year(), today.month(), &event_days, today);
let events = get_upcoming_events();
let tooltip = if events.is_empty() {
calendar
} else {
format!(r##"{calendar}\n\n<span color="#1e66f5"><b>Upcoming</b></span>\n{events}"##)
};
println!(
"{}",
json!({
"text": format!("{:02}:{:02}", now.hour(), now.minute()),
"tooltip": tooltip,
"alt": today.format("%Y-%m-%d").to_string(),
})
);
}