From 468699e31649322ad6adc7819900e63ad1c2d3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asger=20Geel=20Weirs=C3=B8e?= Date: Wed, 6 May 2026 09:55:11 +0200 Subject: [PATCH] feat: add rate-limited update check with desktop notification Co-Authored-By: Claude Sonnet 4.6 --- crates/ahfail-gtklock/src/handler.rs | 4 ++ crates/ahfail-ui/src/update.rs | 88 +++++++++++++++++++++++++- crates/ahfail-ui/tests/update_tests.rs | 14 ++++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 crates/ahfail-ui/tests/update_tests.rs diff --git a/crates/ahfail-gtklock/src/handler.rs b/crates/ahfail-gtklock/src/handler.rs index 5aadc81..15f3947 100644 --- a/crates/ahfail-gtklock/src/handler.rs +++ b/crates/ahfail-gtklock/src/handler.rs @@ -86,6 +86,10 @@ impl WindowHandler { data.ready_players.push(new_player); } }); + + std::thread::spawn(|| { + ahfail_ui::update::check_for_update(ahfail_ui::VERSION); + }); }); unsafe { diff --git a/crates/ahfail-ui/src/update.rs b/crates/ahfail-ui/src/update.rs index 0d2384e..93310b8 100644 --- a/crates/ahfail-ui/src/update.rs +++ b/crates/ahfail-ui/src/update.rs @@ -1 +1,87 @@ -pub fn check_for_update(_current_version: &str) {} +use std::path::PathBuf; +use std::process::Command; +use std::time::SystemTime; +use std::fs; + +const GITEA_API: &str = + "https://gitea.weircon.dk/api/v1/repos/agw/gtk-ahfail/releases/latest"; +const UPDATE_NOTIFY_MSG: &str = + "Update available — visit https://gitea.weircon.dk/agw/gtk-ahfail/releases"; +const CHECK_INTERVAL_SECS: u64 = 86_400; + +pub fn is_newer(latest: &str, current: &str) -> bool { + let parse = |s: &str| -> Option<(u32, u32, u32)> { + let s = s.trim_start_matches('v'); + let p: Vec<&str> = s.splitn(3, '.').collect(); + if p.len() != 3 { return None; } + Some((p[0].parse().ok()?, p[1].parse().ok()?, p[2].parse().ok()?)) + }; + match (parse(latest), parse(current)) { + (Some(l), Some(c)) => l > c, + _ => false, + } +} + +fn cache_file() -> PathBuf { + let dir = std::env::var("HOME") + .map(|h| PathBuf::from(h).join(".cache/ahfail")) + .unwrap_or_else(|_| PathBuf::from("/tmp/ahfail-cache")); + let _ = fs::create_dir_all(&dir); + dir.join("last_update_check") +} + +fn rate_limited() -> bool { + let path = cache_file(); + if let Ok(meta) = fs::metadata(&path) { + if let Ok(modified) = meta.modified() { + let age = SystemTime::now() + .duration_since(modified) + .unwrap_or_default(); + return age.as_secs() < CHECK_INTERVAL_SECS; + } + } + false +} + +fn touch_cache() { + let _ = fs::write(cache_file(), b""); +} + +fn send_notification() { + #[cfg(target_os = "linux")] + let _ = Command::new("notify-send") + .args(["ahfail", UPDATE_NOTIFY_MSG]) + .spawn(); + + #[cfg(target_os = "macos")] + let _ = Command::new("osascript") + .args(["-e", &format!( + "display notification \"{}\" with title \"ahfail\"", + UPDATE_NOTIFY_MSG + )]) + .spawn(); +} + +/// Run in a background thread. Checks Gitea for a newer release and sends a +/// desktop notification if one exists. Rate-limited to once per 24 hours. +/// Fails silently on any error. +pub fn check_for_update(current_version: &str) { + if rate_limited() { return; } + touch_cache(); + + let Ok(resp) = ureq::get(GITEA_API).call() else { return }; + let Ok(body) = resp.into_string() else { return }; + + if let Some(tag) = extract_tag_name(&body) { + if is_newer(&tag, current_version) { + send_notification(); + } + } +} + +fn extract_tag_name(json: &str) -> Option { + let key = "\"tag_name\":\""; + let start = json.find(key)? + key.len(); + let end = json[start..].find('"')? + start; + Some(json[start..end].to_string()) +} diff --git a/crates/ahfail-ui/tests/update_tests.rs b/crates/ahfail-ui/tests/update_tests.rs new file mode 100644 index 0000000..7d5d624 --- /dev/null +++ b/crates/ahfail-ui/tests/update_tests.rs @@ -0,0 +1,14 @@ +use ahfail_ui::update::is_newer; + +#[test] +fn newer_version_detected() { + assert!(is_newer("v0.2.0", "v0.1.0")); + assert!(!is_newer("v0.1.0", "v0.1.0")); + assert!(!is_newer("v0.1.0", "v0.2.0")); + assert!(!is_newer("garbage", "v0.1.0")); +} + +#[test] +fn strips_v_prefix() { + assert!(is_newer("0.2.0", "0.1.0")); +}