limit_cli/tui/input/
handler.rs1use crate::error::CliError;
6use crossterm::event::{
7 KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
8};
9use std::time::Instant;
10
11#[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
35pub struct InputHandler {
37 last_esc_time: Option<Instant>,
39 cursor_blink_state: bool,
41 cursor_blink_timer: Instant,
43}
44
45impl InputHandler {
46 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 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 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 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 if key.code == KeyCode::Esc {
97 return self.handle_esc(is_busy, has_autocomplete);
98 }
99
100 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 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 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 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 #[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 #[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 #[inline]
207 pub fn cursor_blink_state(&self) -> bool {
208 self.cursor_blink_state
209 }
210
211 #[inline]
213 pub fn reset_esc_time(&mut self) {
214 self.last_esc_time = None;
215 }
216
217 #[inline]
219 pub fn last_esc_time(&self) -> Option<Instant> {
220 self.last_esc_time
221 }
222
223 #[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}