Skip to main content

limit_cli/tui/input/
handler.rs

1//! Input event handler for TUI
2//!
3//! Handles keyboard events, mouse events, and delegates to appropriate handlers.
4
5use crate::error::CliError;
6use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
7use std::time::Instant;
8
9/// Actions that can result from input handling
10#[derive(Debug, Clone, PartialEq)]
11pub enum InputAction {
12    None,
13    Submit(String),
14    Exit,
15    Cancel,
16    ScrollUp,
17    ScrollDown,
18    PageUp,
19    PageDown,
20    StartAutocomplete,
21    UpdateAutocomplete(char),
22    AutocompleteUp,
23    AutocompleteDown,
24    AutocompleteAccept,
25    AutocompleteCancel,
26    CopySelection,
27    Paste,
28    ShowHelp,
29    ClearChat,
30    HandleCommand(String),
31}
32
33/// Input handler for processing keyboard and mouse events
34pub struct InputHandler {
35    /// Last ESC press time for double-ESC detection
36    last_esc_time: Option<Instant>,
37    /// Cursor blink state
38    cursor_blink_state: bool,
39    /// Cursor blink timer
40    cursor_blink_timer: Instant,
41}
42
43impl InputHandler {
44    /// Create a new input handler
45    pub fn new() -> Self {
46        Self {
47            last_esc_time: None,
48            cursor_blink_state: true,
49            cursor_blink_timer: Instant::now(),
50        }
51    }
52
53    /// Handle a keyboard event and return the action to take
54    pub fn handle_key(
55        &mut self,
56        key: KeyEvent,
57        input_text: &str,
58        _cursor_pos: usize,
59        is_busy: bool,
60        has_autocomplete: bool,
61    ) -> Result<InputAction, CliError> {
62        tracing::trace!("handle_key: code={:?} mod={:?} kind={:?}", key.code, key.modifiers, key.kind);
63
64        if key.kind != KeyEventKind::Press {
65            return Ok(InputAction::None);
66        }
67
68        // Handle copy/paste shortcuts
69        if self.is_copy_paste_modifier(&key, 'c') {
70            return Ok(InputAction::CopySelection);
71        }
72
73        if self.is_copy_paste_modifier(&key, 'v') && !is_busy {
74            return Ok(InputAction::Paste);
75        }
76
77        // Handle autocomplete navigation
78        if has_autocomplete {
79            match key.code {
80                KeyCode::Up => return Ok(InputAction::AutocompleteUp),
81                KeyCode::Down => return Ok(InputAction::AutocompleteDown),
82                KeyCode::Enter | KeyCode::Tab => return Ok(InputAction::AutocompleteAccept),
83                KeyCode::Esc => return Ok(InputAction::AutocompleteCancel),
84                _ => {}
85            }
86        }
87
88        // Handle ESC
89        if key.code == KeyCode::Esc {
90            return self.handle_esc(is_busy, has_autocomplete);
91        }
92
93        // Handle scrolling
94        match key.code {
95            KeyCode::PageUp => return Ok(InputAction::PageUp),
96            KeyCode::PageDown => return Ok(InputAction::PageDown),
97            KeyCode::Up => return Ok(InputAction::ScrollUp),
98            KeyCode::Down => return Ok(InputAction::ScrollDown),
99            _ => {}
100        }
101
102        if is_busy {
103            return Ok(InputAction::None);
104        }
105
106        // Handle command/character input
107        match key.code {
108            KeyCode::Enter => {
109                let text = input_text.trim();
110                if text.is_empty() {
111                    Ok(InputAction::None)
112                } else if text.starts_with('/') {
113                    Ok(InputAction::HandleCommand(text.to_string()))
114                } else {
115                    Ok(InputAction::Submit(text.to_string()))
116                }
117            }
118            KeyCode::Char(c) if key.modifiers == KeyModifiers::NONE || key.modifiers == KeyModifiers::SHIFT => {
119                if c == '@' {
120                    Ok(InputAction::StartAutocomplete)
121                } else if has_autocomplete {
122                    Ok(InputAction::UpdateAutocomplete(c))
123                } else {
124                    Ok(InputAction::None)
125                }
126            }
127            _ => Ok(InputAction::None),
128        }
129    }
130
131    /// Handle ESC key (double-ESC to cancel when busy)
132    fn handle_esc(&mut self, is_busy: bool, has_autocomplete: bool) -> Result<InputAction, CliError> {
133        if has_autocomplete {
134            return Ok(InputAction::AutocompleteCancel);
135        }
136        
137        if is_busy {
138            let now = Instant::now();
139            let should_cancel = self.last_esc_time
140                .map(|last| now.duration_since(last) < std::time::Duration::from_millis(1000))
141                .unwrap_or(false);
142
143            self.last_esc_time = Some(now);
144
145            return Ok(if should_cancel { InputAction::Cancel } else { InputAction::None });
146        }
147        
148        Ok(InputAction::Exit)
149    }
150
151    /// Handle a mouse event
152    pub fn handle_mouse(&mut self, mouse: MouseEvent) -> Result<bool, CliError> {
153        Ok(matches!(
154            mouse.kind,
155            MouseEventKind::Down(MouseButton::Left)
156                | MouseEventKind::Drag(MouseButton::Left)
157                | MouseEventKind::Up(MouseButton::Left)
158                | MouseEventKind::ScrollUp
159                | MouseEventKind::ScrollDown
160        ))
161    }
162
163    /// Check if the current key event is a copy/paste shortcut
164    #[inline]
165    fn is_copy_paste_modifier(&self, key: &KeyEvent, char: char) -> bool {
166        #[cfg(target_os = "macos")]
167        {
168            let has_super = key.modifiers.contains(KeyModifiers::SUPER);
169            let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
170            key.code == KeyCode::Char(char) && (has_super || has_ctrl)
171        }
172        #[cfg(not(target_os = "macos"))]
173        {
174            key.code == KeyCode::Char(char) && key.modifiers.contains(KeyModifiers::CONTROL)
175        }
176    }
177
178    /// Tick cursor blink animation
179    #[inline]
180    pub fn tick_cursor_blink(&mut self) {
181        if self.cursor_blink_timer.elapsed().as_millis() > 500 {
182            self.cursor_blink_state = !self.cursor_blink_state;
183            self.cursor_blink_timer = Instant::now();
184        }
185    }
186
187    /// Get current cursor blink state
188    #[inline]
189    pub fn cursor_blink_state(&self) -> bool {
190        self.cursor_blink_state
191    }
192
193    /// Reset last ESC time
194    #[inline]
195    pub fn reset_esc_time(&mut self) {
196        self.last_esc_time = None;
197    }
198
199    /// Get last ESC time
200    #[inline]
201    pub fn last_esc_time(&self) -> Option<Instant> {
202        self.last_esc_time
203    }
204
205    /// Set last ESC time
206    #[inline]
207    pub fn set_last_esc_time(&mut self, time: Instant) {
208        self.last_esc_time = Some(time);
209    }
210}
211
212impl Default for InputHandler {
213    fn default() -> Self {
214        Self::new()
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_input_handler_creation() {
224        let handler = InputHandler::new();
225        assert!(handler.cursor_blink_state());
226    }
227
228    #[test]
229    fn test_input_handler_default() {
230        let handler = InputHandler::default();
231        assert!(handler.cursor_blink_state());
232    }
233
234    #[test]
235    fn test_cursor_blink() {
236        let mut handler = InputHandler::new();
237        let initial_state = handler.cursor_blink_state();
238
239        handler.tick_cursor_blink();
240        assert_eq!(handler.cursor_blink_state(), initial_state);
241    }
242}