agent_core/tui/keys/
exit.rs

1//! Exit handling for TUI applications.
2//!
3//! This module provides:
4//! - [`ExitHandler`] trait for agent cleanup on exit
5//! - [`ExitState`] enum for tracking exit confirmation state
6
7use std::time::Instant;
8
9/// Exit confirmation state for key handlers.
10///
11/// This tracks whether the user has initiated an exit sequence
12/// that requires confirmation (e.g., press Ctrl+D twice to quit).
13#[derive(Debug, Clone)]
14pub enum ExitState {
15    /// Normal operation, no exit pending.
16    Normal,
17    /// Awaiting exit confirmation within the timeout.
18    AwaitingConfirmation {
19        /// When the exit sequence was initiated.
20        since: Instant,
21        /// Timeout in seconds for confirmation.
22        timeout_secs: u64,
23    },
24}
25
26impl Default for ExitState {
27    fn default() -> Self {
28        Self::Normal
29    }
30}
31
32impl ExitState {
33    /// Create a new exit confirmation state.
34    pub fn awaiting_confirmation(timeout_secs: u64) -> Self {
35        Self::AwaitingConfirmation {
36            since: Instant::now(),
37            timeout_secs,
38        }
39    }
40
41    /// Check if the confirmation has expired.
42    pub fn is_expired(&self) -> bool {
43        match self {
44            Self::Normal => false,
45            Self::AwaitingConfirmation { since, timeout_secs } => {
46                since.elapsed().as_secs() >= *timeout_secs
47            }
48        }
49    }
50
51    /// Check if awaiting confirmation (and not expired).
52    pub fn is_awaiting(&self) -> bool {
53        match self {
54            Self::Normal => false,
55            Self::AwaitingConfirmation { since, timeout_secs } => {
56                since.elapsed().as_secs() < *timeout_secs
57            }
58        }
59    }
60
61    /// Reset to normal state.
62    pub fn reset(&mut self) {
63        *self = Self::Normal;
64    }
65}
66
67/// Optional hook for agent cleanup on exit.
68///
69/// Implement this trait to run cleanup code before the application exits.
70/// This is useful for saving session state, closing connections, or other
71/// cleanup tasks.
72///
73/// # Example
74///
75/// ```ignore
76/// struct SaveOnExitHandler {
77///     session_file: PathBuf,
78/// }
79///
80/// impl ExitHandler for SaveOnExitHandler {
81///     fn on_exit(&mut self) -> bool {
82///         // Save session state
83///         self.save_session();
84///         true // proceed with exit
85///     }
86/// }
87/// ```
88pub trait ExitHandler: Send + 'static {
89    /// Called when exit is confirmed.
90    ///
91    /// Return `true` to proceed with exit, or `false` to cancel the exit.
92    /// This allows handlers to veto the exit if needed (e.g., unsaved changes).
93    fn on_exit(&mut self) -> bool {
94        true
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use std::time::Duration;
102
103    #[test]
104    fn test_exit_state_default() {
105        let state = ExitState::default();
106        assert!(!state.is_awaiting());
107        assert!(!state.is_expired());
108    }
109
110    #[test]
111    fn test_exit_state_awaiting() {
112        let state = ExitState::awaiting_confirmation(2);
113        assert!(state.is_awaiting());
114        assert!(!state.is_expired());
115    }
116
117    #[test]
118    fn test_exit_state_expired() {
119        let state = ExitState::AwaitingConfirmation {
120            since: Instant::now() - Duration::from_secs(3),
121            timeout_secs: 2,
122        };
123        assert!(!state.is_awaiting());
124        assert!(state.is_expired());
125    }
126
127    #[test]
128    fn test_exit_state_reset() {
129        let mut state = ExitState::awaiting_confirmation(2);
130        assert!(state.is_awaiting());
131        state.reset();
132        assert!(!state.is_awaiting());
133    }
134}