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