added 'days until' and UI improvements

This commit is contained in:
2026-08-12 10:38:29 -07:00
parent 6366cdd40d
commit 4a659f016f

View File

@@ -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<u64> = None;
let mut max_rain_id_tomorrow: Option<u64> = 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<u64> = 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<u64>| -> &'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<String> {
upcoming
}
fn render_calendar_and_birthdays(
fn get_countdowns() -> Vec<String> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
// 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<dyn std::error::Error>> {
tomorrow_summary: "Fetching...".to_string(),
};
let mut upcoming_birthdays: Vec<String>;
thread::sleep(Duration::from_millis(1000));
loop {
@@ -467,7 +546,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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!("===============================================================================================");