Skip to main content

ai_usagebar/
countdown.rs

1//! Human-readable countdown between two instants.
2//!
3//! Mirrors claudebar's `countdown()` shell function (claudebar:252-268):
4//!   - missing / unparseable reset → `"—"`
5//!   - reset already in the past → `"now"`
6//!   - ≥1 day remaining → `"{d}d {h}h"`
7//!   - otherwise → `"{h}h {mm}m"` (zero-padded minutes)
8
9use chrono::{DateTime, Utc};
10
11/// Format `reset - now` as a short human string.
12///
13/// Same buckets as claudebar; `None` for `reset` returns `"—"`, matching the
14/// shell behavior where `[[ -z "$ts" ]]` short-circuits.
15pub fn format(reset: Option<DateTime<Utc>>, now: DateTime<Utc>) -> String {
16    let Some(reset) = reset else {
17        return "—".to_string();
18    };
19
20    let diff = reset.signed_duration_since(now);
21    let secs = diff.num_seconds();
22    if secs <= 0 {
23        return "now".to_string();
24    }
25
26    let days = secs / 86_400;
27    let hours = (secs % 86_400) / 3_600;
28    let mins = (secs % 3_600) / 60;
29
30    if days > 0 {
31        format!("{days}d {hours}h")
32    } else {
33        format!("{hours}h {mins:02}m")
34    }
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40    use chrono::TimeZone;
41
42    fn at(year: i32, month: u32, day: u32, h: u32, m: u32) -> DateTime<Utc> {
43        Utc.with_ymd_and_hms(year, month, day, h, m, 0).unwrap()
44    }
45
46    #[test]
47    fn missing_reset_renders_em_dash() {
48        let now = at(2026, 5, 23, 12, 0);
49        assert_eq!(format(None, now), "—");
50    }
51
52    #[test]
53    fn past_reset_renders_now() {
54        let now = at(2026, 5, 23, 12, 0);
55        let reset = at(2026, 5, 23, 11, 0);
56        assert_eq!(format(Some(reset), now), "now");
57    }
58
59    #[test]
60    fn exact_zero_renders_now() {
61        // Bash uses `<= 0`, so a zero diff is "now".
62        let t = at(2026, 5, 23, 12, 0);
63        assert_eq!(format(Some(t), t), "now");
64    }
65
66    #[test]
67    fn hours_minutes_zero_padded() {
68        let now = at(2026, 5, 23, 12, 0);
69        let reset = at(2026, 5, 23, 13, 5); // 1h 5m
70        assert_eq!(format(Some(reset), now), "1h 05m");
71    }
72
73    #[test]
74    fn hours_minutes_no_days_under_one_day() {
75        let now = at(2026, 5, 23, 12, 0);
76        let reset = at(2026, 5, 24, 11, 59); // 23h 59m
77        assert_eq!(format(Some(reset), now), "23h 59m");
78    }
79
80    #[test]
81    fn one_day_one_hour() {
82        let now = at(2026, 5, 23, 12, 0);
83        let reset = at(2026, 5, 24, 13, 30); // 1d 1h (minutes dropped)
84        assert_eq!(format(Some(reset), now), "1d 1h");
85    }
86
87    #[test]
88    fn multiple_days_drops_minutes() {
89        let now = at(2026, 5, 23, 12, 0);
90        let reset = at(2026, 5, 27, 13, 45); // 4d 1h
91        assert_eq!(format(Some(reset), now), "4d 1h");
92    }
93
94    #[test]
95    fn one_second_remaining_renders_zero_hours() {
96        // Mirrors claudebar: anything > 0 but < 1 min → "0h 00m"
97        let now = at(2026, 5, 23, 12, 0);
98        let reset = now + chrono::Duration::seconds(1);
99        assert_eq!(format(Some(reset), now), "0h 00m");
100    }
101}