agent_tui/tui/
quit_guard.rs1use std::time::{Duration, Instant};
12
13pub const WINDOW: Duration = Duration::from_secs(3);
15
16pub const NOTICE: &str =
18 "turn still running in the daemon \u{2014} press Esc to abort it, or Ctrl+C again within 3 s to detach (session keeps running)";
19
20pub const NOTICE_LINE: &str =
22 "turn still running in the daemon \u{2014} Ctrl+C again within 3 s to detach (session keeps running)";
23
24#[derive(Debug, Clone)]
26pub struct QuitGuard {
27 first_press: Option<Instant>,
29}
30
31impl QuitGuard {
32 pub fn new() -> Self {
33 Self { first_press: None }
34 }
35
36 pub fn press(&mut self, now: Instant) -> bool {
41 if let Some(first) = self.first_press {
42 if now.duration_since(first) <= WINDOW {
43 self.first_press = None;
44 return true;
45 }
46 }
47 self.first_press = Some(now);
49 false
50 }
51
52 pub fn reset(&mut self) {
54 self.first_press = None;
55 }
56}
57
58impl Default for QuitGuard {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 fn fake_now(offset: Duration) -> Instant {
70 use std::sync::OnceLock;
71 static ANCHOR: OnceLock<Instant> = OnceLock::new();
72 *ANCHOR.get_or_init(Instant::now) + offset
73 }
74
75 #[test]
76 fn first_press_returns_false() {
77 let mut g = QuitGuard::new();
78 assert!(!g.press(fake_now(Duration::from_secs(0))));
79 }
80
81 #[test]
82 fn second_press_within_window_returns_true() {
83 let mut g = QuitGuard::new();
84 assert!(!g.press(fake_now(Duration::from_secs(0))));
85 assert!(g.press(fake_now(Duration::from_secs(2))));
86 }
87
88 #[test]
89 fn second_press_after_window_returns_false() {
90 let mut g = QuitGuard::new();
91 assert!(!g.press(fake_now(Duration::from_secs(0))));
92 assert!(!g.press(fake_now(Duration::from_secs(4))));
93 }
94
95 #[test]
96 fn third_press_within_new_window_returns_true() {
97 let mut g = QuitGuard::new();
98 assert!(!g.press(fake_now(Duration::from_secs(0))));
99 assert!(!g.press(fake_now(Duration::from_secs(4))));
101 assert!(g.press(fake_now(Duration::from_secs(5))));
103 }
104
105 #[test]
106 fn reset_clears_state() {
107 let mut g = QuitGuard::new();
108 assert!(!g.press(fake_now(Duration::from_secs(0))));
109 g.reset();
110 assert!(!g.press(fake_now(Duration::from_secs(1))));
111 }
112
113 #[test]
114 fn exact_boundary_is_within_window() {
115 let mut g = QuitGuard::new();
116 assert!(!g.press(fake_now(Duration::from_secs(0))));
117 assert!(g.press(fake_now(Duration::from_secs(3))));
118 }
119
120 #[test]
121 fn just_past_boundary_is_outside_window() {
122 let mut g = QuitGuard::new();
123 assert!(!g.press(fake_now(Duration::from_secs(0))));
124 assert!(!g.press(fake_now(Duration::from_millis(3001))));
125 }
126}