From 3b519578c599084547a55eb2947748d33749f6ba Mon Sep 17 00:00:00 2001 From: michael Date: Mon, 27 Apr 2026 11:19:46 -0700 Subject: [PATCH] feat(gui): WatchdogState pure-logic helper for first-run modal Convert gui/src/firstrun.rs to a module dir with watchdog.rs, the pure-logic WatchdogState helper that the GTK widget code in Task 2 will poll. Three unit tests cover fresh/expired/poke transitions. Co-Authored-By: Claude Opus 4.7 (1M context) --- gui/src/{firstrun.rs => firstrun/mod.rs} | 6 ++- gui/src/firstrun/watchdog.rs | 55 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) rename gui/src/{firstrun.rs => firstrun/mod.rs} (73%) create mode 100644 gui/src/firstrun/watchdog.rs diff --git a/gui/src/firstrun.rs b/gui/src/firstrun/mod.rs similarity index 73% rename from gui/src/firstrun.rs rename to gui/src/firstrun/mod.rs index 970c361..6166ef3 100644 --- a/gui/src/firstrun.rs +++ b/gui/src/firstrun/mod.rs @@ -1,6 +1,10 @@ //! First-login modal. Phase 10 implements the body of `run`. -#![allow(dead_code)] // body lands in Phase 10. +#![allow(dead_code)] // body lands in Phase 10 Task 2. + +mod watchdog; +#[allow(unused_imports)] // wired into run() in Phase 10 Task 2. +pub(crate) use watchdog::WatchdogState; use crate::app_context::AppContext; diff --git a/gui/src/firstrun/watchdog.rs b/gui/src/firstrun/watchdog.rs new file mode 100644 index 0000000..de1231b --- /dev/null +++ b/gui/src/firstrun/watchdog.rs @@ -0,0 +1,55 @@ +//! Tracks the last-activity timestamp; the GTK timer queries this each tick +//! to decide whether 60s of idle has elapsed. + +use std::time::{Duration, Instant}; + +pub(crate) struct WatchdogState { + last_activity: Instant, + timeout: Duration, +} + +impl WatchdogState { + pub fn new(timeout: Duration) -> Self { + Self { + last_activity: Instant::now(), + timeout, + } + } + + pub fn poke(&mut self) { + self.last_activity = Instant::now(); + } + + /// True iff `timeout` has passed since the last poke. Once expired, + /// stays expired until the caller drops + recreates. + pub fn expired(&self) -> bool { + self.last_activity.elapsed() >= self.timeout + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fresh_state_is_not_expired() { + let w = WatchdogState::new(Duration::from_secs(60)); + assert!(!w.expired()); + } + + #[test] + fn zero_timeout_is_immediately_expired() { + let w = WatchdogState::new(Duration::from_millis(0)); + std::thread::sleep(Duration::from_millis(2)); + assert!(w.expired()); + } + + #[test] + fn poke_resets_the_clock() { + let mut w = WatchdogState::new(Duration::from_millis(100)); + std::thread::sleep(Duration::from_millis(150)); + assert!(w.expired()); + w.poke(); + assert!(!w.expired()); + } +}