limit_cli/tui/input/
handler.rs1use crate::error::CliError;
6use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
7use std::time::Instant;
8
9#[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
33pub struct InputHandler {
35 last_esc_time: Option<Instant>,
37 cursor_blink_state: bool,
39 cursor_blink_timer: Instant,
41}
42
43impl InputHandler {
44 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 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 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 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 if key.code == KeyCode::Esc {
90 return self.handle_esc(is_busy, has_autocomplete);
91 }
92
93 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 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 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 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 #[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 #[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 #[inline]
189 pub fn cursor_blink_state(&self) -> bool {
190 self.cursor_blink_state
191 }
192
193 #[inline]
195 pub fn reset_esc_time(&mut self) {
196 self.last_esc_time = None;
197 }
198
199 #[inline]
201 pub fn last_esc_time(&self) -> Option<Instant> {
202 self.last_esc_time
203 }
204
205 #[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}