From 4a659f016fc3e89f61fb310515be0d202a635fb8 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 12 Aug 2026 10:38:29 -0700 Subject: [PATCH] added 'days until' and UI improvements --- hypr/scripts/statmon/src/main.rs | 129 +++++++++++++++++++++++++------ 1 file changed, 104 insertions(+), 25 deletions(-) diff --git a/hypr/scripts/statmon/src/main.rs b/hypr/scripts/statmon/src/main.rs index c24d500..f2bc1bf 100644 --- a/hypr/scripts/statmon/src/main.rs +++ b/hypr/scripts/statmon/src/main.rs @@ -42,6 +42,12 @@ struct WeatherState { tomorrow_summary: String, } +struct Countdown { + label: String, + month: u32, + day: u32, +} + fn get_config_dir() -> PathBuf { if let Ok(home) = env::var("HOME") { PathBuf::from(home).join(".config/hypr/scripts/statmon") @@ -112,8 +118,9 @@ fn fetch_weather_and_forecast(api_key: &str) -> (String, String) { if let Some(list) = json["list"].as_array() { let mut max_pop_today = 0.0f64; let mut max_pop_tomorrow = 0.0f64; - let mut today_rain = false; - let mut tomorrow_rain = false; + + let mut max_rain_id_today: Option = None; + let mut max_rain_id_tomorrow: Option = None; let now = Local::now().date_naive(); let tomorrow = now + chrono::Days::new(1); @@ -122,31 +129,53 @@ fn fetch_weather_and_forecast(api_key: &str) -> (String, String) { if let Some(dt_txt) = item["dt_txt"].as_str() { if let Ok(date) = NaiveDate::parse_from_str(&dt_txt[..10], "%Y-%m-%d") { let pop = item["pop"].as_f64().unwrap_or(0.0); - let weather_main = item["weather"][0]["main"].as_str().unwrap_or(""); + + // Extract rain condition IDs in the 3xx (drizzle) and 5xx (rain) ranges + let weather_ids: Vec = item["weather"] + .as_array() + .map(|arr| { + arr.iter() + .filter_map(|w| w["id"].as_u64()) + .filter(|&id| (300..600).contains(&id)) + .collect() + }) + .unwrap_or_default(); + + let max_block_id = weather_ids.into_iter().max(); if date == now { if pop > max_pop_today { max_pop_today = pop; } - if weather_main.to_lowercase().contains("rain") || pop > 0.3 { - today_rain = true; + if let Some(id) = max_block_id { + max_rain_id_today = Some(max_rain_id_today.map_or(id, |m| m.max(id))); } } else if date == tomorrow { if pop > max_pop_tomorrow { max_pop_tomorrow = pop; } - if weather_main.to_lowercase().contains("rain") || pop > 0.3 { - tomorrow_rain = true; + if let Some(id) = max_block_id { + max_rain_id_tomorrow = Some(max_rain_id_tomorrow.map_or(id, |m| m.max(id))); } } } } } + // Helper closure to map max weather ID to human-readable status + let classify_rain = |max_id: Option| -> &'static str { + match max_id { + Some(502..=504 | 522 | 531) => "Heavy Rain", + Some(501 | 521) => "Moderate Rain", + Some(500 | 520 | 300..=321) => "Light Rain", + _ => "Clear/No Major Rain", + } + }; + today_str = format!( "{} (Rain Chance: {:.0}%)", - if today_rain { "Rain Expected" } else { "Clear/No Major Rain" }, + classify_rain(max_rain_id_today), max_pop_today * 100.0 ); tomorrow_str = format!( "{} (Rain Chance: {:.0}%)", - if tomorrow_rain { "Rain Expected" } else { "Clear/No Major Rain" }, + classify_rain(max_rain_id_tomorrow), max_pop_tomorrow * 100.0 ); } @@ -181,9 +210,46 @@ fn get_upcoming_birthdays() -> Vec { upcoming } -fn render_calendar_and_birthdays( +fn get_countdowns() -> Vec { + let today = Local::now().date_naive(); + let current_year = today.year(); + + let targets = vec![ + Countdown { label: "Avy's Birthday".to_string(), month: 4, day: 6 }, + Countdown { label: "4th of July".to_string(), month: 7, day: 4 }, + Countdown { label: "Cal's Birthday".to_string(), month: 9, day: 24 }, + Countdown { label: "Halloween".to_string(), month: 10, day: 31 }, + Countdown { label: "Christmas".to_string(), month: 12, day: 25 }, + ]; + + let mut results = Vec::new(); + + for t in targets { + let target_date = if let Some(d) = NaiveDate::from_ymd_opt(current_year, t.month, t.day) { + if d >= today { + d + } else { + NaiveDate::from_ymd_opt(current_year + 1, t.month, t.day).unwrap() + } + } else { + continue; + }; + + let diff = target_date.signed_duration_since(today).num_days(); + if diff == 0 { + results.push(format!("{:<14}: TODAY!", t.label)); + } else { + results.push(format!("{:<14}: {} days", t.label, diff)); + } + } + + results +} + +fn render_bottom_section( stdout: &mut std::io::Stdout, upcoming_birthdays: &[String], + countdowns: &[String], ) -> Result<(), Box> { let now = Local::now(); let year = now.year(); @@ -202,14 +268,15 @@ fn render_calendar_and_birthdays( let start_weekday = first_of_month.weekday().num_days_from_sunday(); - // Side-by-Side Headers + // 3-Column Headers execute!(stdout, SetForegroundColor(ACCENT))?; write!(stdout, " {:<27}", format!("CALENDAR ({} {})", now.format("%B"), year))?; - execute!(stdout, SetForegroundColor(ACCENT))?; - writeln!(stdout, "UPCOMING EVENTS")?; + write!(stdout, "{:<34}", "UPCOMING EVENTS")?; + writeln!(stdout, "DAYS UNTIL")?; execute!(stdout, SetForegroundColor(FG_MAIN))?; write!(stdout, " Su Mo Tu We Th Fr Sa ")?; + write!(stdout, "--------------------------------- ")?; writeln!(stdout, "----------------------------------")?; // Build Calendar Lines @@ -239,9 +306,13 @@ fn render_calendar_and_birthdays( } } - let total_rows = calendar_lines.len().max(upcoming_birthdays.len().max(1)); + let total_rows = calendar_lines + .len() + .max(upcoming_birthdays.len().max(1)) + .max(countdowns.len()); for i in 0..total_rows { + // Col 1: Calendar execute!(stdout, SetForegroundColor(FG_MAIN))?; if i < calendar_lines.len() { write!(stdout, " {} ", calendar_lines[i])?; @@ -249,14 +320,25 @@ fn render_calendar_and_birthdays( write!(stdout, " ")?; } - write!(stdout, " ")?; + write!(stdout, " ")?; + // Col 2: Upcoming Events if i < upcoming_birthdays.len() { execute!(stdout, SetForegroundColor(ACCENT))?; - writeln!(stdout, "• {}", upcoming_birthdays[i])?; + write!(stdout, "{:<32}", format!("• {}", upcoming_birthdays[i]))?; } else if i == 0 && upcoming_birthdays.is_empty() { execute!(stdout, SetForegroundColor(FG_MAIN))?; - writeln!(stdout, "No events in next 2 days")?; + write!(stdout, "{:<32}", "No events in next 2 days")?; + } else { + write!(stdout, " ")?; + } + + write!(stdout, " ")?; + + // Col 3: Days Until + if i < countdowns.len() { + execute!(stdout, SetForegroundColor(ACCENT))?; + writeln!(stdout, "• {}", countdowns[i])?; } else { writeln!(stdout)?; } @@ -422,7 +504,6 @@ fn print_line(stdout: &mut std::io::Stdout, label: &str, value: &str, val_color: } fn main() -> Result<(), Box> { - // Load .env from ~/.config/hypr/scripts/statmon/.env let env_path = get_config_dir().join(".env"); let _ = dotenvy::from_path(&env_path); @@ -453,8 +534,6 @@ fn main() -> Result<(), Box> { tomorrow_summary: "Fetching...".to_string(), }; - let mut upcoming_birthdays: Vec; - thread::sleep(Duration::from_millis(1000)); loop { @@ -467,7 +546,7 @@ fn main() -> Result<(), Box> { weather.aqi_desc = aqi_desc; weather.aqi_val = aqi_val; - if aqi_val >= 5 { + if aqi_val >= 4 { send_notification("Air Quality Warning", &format!("Air quality in 95662 is concerning: {}", weather.aqi_desc), "critical"); } last_aqi_fetch = Instant::now(); @@ -481,8 +560,8 @@ fn main() -> Result<(), Box> { last_weather_fetch = Instant::now(); } - // Always check birthdays on every refresh loop - upcoming_birthdays = get_upcoming_birthdays(); + let upcoming_birthdays = get_upcoming_birthdays(); + let countdowns = get_countdowns(); sys.refresh_all(); networks.refresh(); @@ -589,10 +668,10 @@ fn main() -> Result<(), Box> { print_line(&mut stdout, "Weather Today", &weather.today_summary, FG_MAIN)?; print_line(&mut stdout, "Weather Tomorrow", &weather.tomorrow_summary, FG_MAIN)?; - // Calendar & Birthdays Section + // Bottom Section (Calendar, Events, Days Until) execute!(stdout, SetForegroundColor(FG_MAIN))?; println!("-----------------------------------------------------------------------------------------------"); - render_calendar_and_birthdays(&mut stdout, &upcoming_birthdays)?; + render_bottom_section(&mut stdout, &upcoming_birthdays, &countdowns)?; execute!(stdout, SetForegroundColor(FG_MAIN))?; println!("===============================================================================================");