Skip to main content

rpi_tui/
countdown_timer.rs

1//! Deadline-based countdown timer.
2//!
3//! Adapted from `packages/coding-agent/src/modes/interactive/.../countdown-timer.ts`.
4//! The TS original spins a `setInterval`; the Rust port is **deadline-based**:
5//! it stores an [`Instant`] and the host re-renders on the existing render tick
6//! (the 120ms task that re-renders while a run is in flight already drives
7//! visible updates). The owner reads [`remaining_secs`] each render.
8//!
9//! `Instant::now()` is allowed here — the `Date.now()`/`Instant::now()` ban in
10//! this codebase applies only to Workflow scripts, not to normal Rust library
11//! code.
12
13use std::time::{Duration, Instant};
14
15/// A simple countdown bounded by an absolute deadline.
16///
17/// No thread, no timer: compute remaining time on demand.
18pub struct CountdownTimer {
19    deadline: Instant,
20}
21
22impl CountdownTimer {
23    /// Create a timer that expires after `duration`.
24    pub fn new(duration: Duration) -> Self {
25        Self {
26            deadline: Instant::now() + duration,
27        }
28    }
29
30    /// Seconds remaining until expiry (clamped at 0).
31    pub fn remaining_secs(&self) -> u32 {
32        let now = Instant::now();
33        if now >= self.deadline {
34            0
35        } else {
36            let remaining = self.deadline - now;
37            remaining
38                .as_secs()
39                .saturating_add(u64::from(remaining.subsec_nanos() > 0))
40                .min(u32::MAX as u64) as u32
41        }
42    }
43
44    /// Whether the deadline has passed.
45    pub fn expired(&self) -> bool {
46        Instant::now() >= self.deadline
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn test_new_timer_not_expired() {
56        let t = CountdownTimer::new(Duration::from_secs(10));
57        assert!(!t.expired());
58        assert!(t.remaining_secs() <= 10);
59    }
60
61    #[test]
62    fn test_expired_timer() {
63        let t = CountdownTimer::new(Duration::from_millis(0));
64        // Sleep is unnecessary for a zero-duration timer; deadline is now/now+0.
65        let r = t.remaining_secs();
66        // For a 0ms timer remaining is 0 (clamped) — but allow 1s of slack on
67        // slow CI by asserting <= 1.
68        assert!(r <= 1, "expected <= 1, got {r}");
69    }
70}