use crate::db::Database; use std::process::Command; /// Send a desktop notification via notify-send (Linux). /// Returns silently if notify-send is not available. pub fn send_notification(title: &str, body: &str, urgency: &str) { let _ = Command::new("notify-send") .arg("--urgency") .arg(urgency) .arg("--app-name=Outlay") .arg(title) .arg(body) .spawn(); } /// Check all budgets for the given month and send notifications /// for any thresholds crossed that haven't been notified yet. /// Only sends if budget_notifications setting is enabled. pub fn check_and_send_budget_notifications(db: &Database, month: &str) { let enabled = db.get_setting("budget_notifications") .ok().flatten().map(|s| s == "1").unwrap_or(false); if !enabled { return; } let budgets = match db.list_budgets_for_month(month) { Ok(b) => b, Err(_) => return, }; for budget in &budgets { let cat_name = db.get_category(budget.category_id) .map(|c| c.name) .unwrap_or_else(|_| "Unknown".to_string()); let thresholds = match db.check_budget_thresholds(budget.category_id, month) { Ok(t) => t, Err(_) => continue, }; for threshold in &thresholds { let (title, urgency) = match threshold { 100 => ( format!("Budget exceeded: {}", cat_name), "critical", ), _ => ( format!("Budget {}% used: {}", threshold, cat_name), "normal", ), }; let progress = db.get_budget_progress(budget.category_id, month) .ok().flatten(); let body = if let Some((budget_amt, spent, pct)) = progress { format!("{:.2} of {:.2} spent ({:.0}%)", spent, budget_amt, pct) } else { String::new() }; send_notification(&title, &body, urgency); let _ = db.record_notification(budget.category_id, month, *threshold); } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_send_notification_does_not_panic() { // Should not panic even if notify-send is not installed send_notification("Test", "Body", "normal"); } }