agent_core/tui/keys/
handler.rs1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
4
5use super::bindings::KeyBindings;
6use super::exit::ExitState;
7use super::types::{AppKeyAction, AppKeyResult, KeyContext};
8
9pub trait KeyHandler: Send + 'static {
26 fn handle_key(&mut self, key: KeyEvent, context: &KeyContext) -> AppKeyResult;
37
38 fn status_hint(&self) -> Option<String> {
43 None
44 }
45}
46
47pub struct DefaultKeyHandler {
57 bindings: KeyBindings,
58 exit_state: ExitState,
59}
60
61impl DefaultKeyHandler {
62 pub fn new(bindings: KeyBindings) -> Self {
64 Self {
65 bindings,
66 exit_state: ExitState::default(),
67 }
68 }
69
70 fn is_exit_key(&self, key: &KeyEvent) -> bool {
72 KeyBindings::matches_any(&self.bindings.enter_exit_mode, key)
73 }
74}
75
76impl Default for DefaultKeyHandler {
77 fn default() -> Self {
78 Self::new(KeyBindings::default())
79 }
80}
81
82impl KeyHandler for DefaultKeyHandler {
83 fn handle_key(&mut self, key: KeyEvent, context: &KeyContext) -> AppKeyResult {
84 if self.exit_state.is_expired() {
86 self.exit_state.reset();
87 }
88
89 if context.widget_blocking {
92 if KeyBindings::matches_any(&self.bindings.force_quit, &key) {
94 return AppKeyResult::Action(AppKeyAction::Quit);
95 }
96 return AppKeyResult::NotHandled;
98 }
99
100 if context.is_processing {
102 if KeyBindings::matches_any(&self.bindings.interrupt, &key) {
103 return AppKeyResult::Action(AppKeyAction::Interrupt);
104 }
105 if KeyBindings::matches_any(&self.bindings.force_quit, &key) {
106 return AppKeyResult::Action(AppKeyAction::Quit);
107 }
108 if self.is_exit_key(&key) {
110 if self.exit_state.is_awaiting() {
111 self.exit_state.reset();
112 return AppKeyResult::Action(AppKeyAction::RequestExit);
113 } else if context.input_empty {
114 self.exit_state = ExitState::awaiting_confirmation(
115 self.bindings.exit_timeout_secs,
116 );
117 return AppKeyResult::Handled;
118 }
119 }
120 return AppKeyResult::Handled;
122 }
123
124 if self.exit_state.is_awaiting() {
126 if self.is_exit_key(&key) {
127 self.exit_state.reset();
128 return AppKeyResult::Action(AppKeyAction::RequestExit);
129 }
130 self.exit_state.reset();
132 }
134
135 if KeyBindings::matches_any(&self.bindings.force_quit, &key) {
137 return AppKeyResult::Action(AppKeyAction::Quit);
138 }
139 if KeyBindings::matches_any(&self.bindings.quit, &key) && context.input_empty {
140 return AppKeyResult::Action(AppKeyAction::Quit);
141 }
142 if self.is_exit_key(&key) {
143 if context.input_empty {
144 self.exit_state = ExitState::awaiting_confirmation(
146 self.bindings.exit_timeout_secs,
147 );
148 return AppKeyResult::Handled;
149 }
150 return AppKeyResult::Action(AppKeyAction::DeleteCharAt);
152 }
153 if KeyBindings::matches_any(&self.bindings.submit, &key) {
154 return AppKeyResult::Action(AppKeyAction::Submit);
155 }
156 if KeyBindings::matches_any(&self.bindings.interrupt, &key) {
157 return AppKeyResult::Action(AppKeyAction::Interrupt);
158 }
159
160 if KeyBindings::matches_any(&self.bindings.move_up, &key) {
162 return AppKeyResult::Action(AppKeyAction::MoveUp);
163 }
164 if KeyBindings::matches_any(&self.bindings.move_down, &key) {
165 return AppKeyResult::Action(AppKeyAction::MoveDown);
166 }
167 if KeyBindings::matches_any(&self.bindings.move_left, &key) {
168 return AppKeyResult::Action(AppKeyAction::MoveLeft);
169 }
170 if KeyBindings::matches_any(&self.bindings.move_right, &key) {
171 return AppKeyResult::Action(AppKeyAction::MoveRight);
172 }
173 if KeyBindings::matches_any(&self.bindings.move_line_start, &key) {
174 return AppKeyResult::Action(AppKeyAction::MoveLineStart);
175 }
176 if KeyBindings::matches_any(&self.bindings.move_line_end, &key) {
177 return AppKeyResult::Action(AppKeyAction::MoveLineEnd);
178 }
179
180 if KeyBindings::matches_any(&self.bindings.delete_char_before, &key) {
182 return AppKeyResult::Action(AppKeyAction::DeleteCharBefore);
183 }
184 if KeyBindings::matches_any(&self.bindings.delete_char_at, &key) {
185 return AppKeyResult::Action(AppKeyAction::DeleteCharAt);
186 }
187 if KeyBindings::matches_any(&self.bindings.kill_line, &key) {
188 return AppKeyResult::Action(AppKeyAction::KillLine);
189 }
190 if KeyBindings::matches_any(&self.bindings.insert_newline, &key) {
191 return AppKeyResult::Action(AppKeyAction::InsertNewline);
192 }
193
194 if let KeyCode::Char(c) = key.code {
196 if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT {
197 return AppKeyResult::Action(AppKeyAction::InsertChar(c));
198 }
199 }
200
201 AppKeyResult::NotHandled
203 }
204
205 fn status_hint(&self) -> Option<String> {
206 if self.exit_state.is_awaiting() {
207 Some("Press again to exit".to_string())
208 } else {
209 None
210 }
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_default_handler_force_quit_in_modal() {
220 let mut handler = DefaultKeyHandler::default();
221 let context = KeyContext {
222 input_empty: true,
223 is_processing: false,
224 widget_blocking: true, };
226
227 let key = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL);
229 let result = handler.handle_key(key, &context);
230 assert_eq!(result, AppKeyResult::Action(AppKeyAction::Quit));
231
232 let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
234 let result = handler.handle_key(esc, &context);
235 assert_eq!(result, AppKeyResult::NotHandled);
236 }
237
238 #[test]
239 fn test_emacs_handler_processing_mode() {
240 let mut handler = DefaultKeyHandler::new(KeyBindings::emacs());
242 let context = KeyContext {
243 input_empty: true,
244 is_processing: true, widget_blocking: false,
246 };
247
248 let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
250 let result = handler.handle_key(esc, &context);
251 assert_eq!(result, AppKeyResult::Action(AppKeyAction::Interrupt));
252
253 let a = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
255 let result = handler.handle_key(a, &context);
256 assert_eq!(result, AppKeyResult::Handled);
257 }
258
259 #[test]
260 fn test_emacs_handler_exit_mode() {
261 let mut handler = DefaultKeyHandler::new(KeyBindings::emacs());
263 let context = KeyContext {
264 input_empty: true,
265 is_processing: false,
266 widget_blocking: false,
267 };
268
269 let ctrl_d = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL);
271 let result = handler.handle_key(ctrl_d, &context);
272 assert_eq!(result, AppKeyResult::Handled);
273
274 assert!(handler.status_hint().is_some());
276
277 let result = handler.handle_key(ctrl_d, &context);
279 assert_eq!(result, AppKeyResult::Action(AppKeyAction::RequestExit));
280
281 assert!(handler.status_hint().is_none());
283 }
284
285 #[test]
286 fn test_bare_minimum_handler_quit() {
287 let mut handler = DefaultKeyHandler::default(); let context = KeyContext {
289 input_empty: true,
290 is_processing: false,
291 widget_blocking: false,
292 };
293
294 let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
296 let result = handler.handle_key(esc, &context);
297 assert_eq!(result, AppKeyResult::Action(AppKeyAction::Quit));
298 }
299
300 #[test]
301 fn test_default_handler_char_input() {
302 let mut handler = DefaultKeyHandler::default();
303 let context = KeyContext {
304 input_empty: true,
305 is_processing: false,
306 widget_blocking: false,
307 };
308
309 let a = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
311 let result = handler.handle_key(a, &context);
312 assert_eq!(result, AppKeyResult::Action(AppKeyAction::InsertChar('a')));
313
314 let shift_a = KeyEvent::new(KeyCode::Char('A'), KeyModifiers::SHIFT);
316 let result = handler.handle_key(shift_a, &context);
317 assert_eq!(result, AppKeyResult::Action(AppKeyAction::InsertChar('A')));
318 }
319
320 #[test]
321 fn test_exit_mode_cancelled_by_other_key() {
322 let mut handler = DefaultKeyHandler::new(KeyBindings::emacs());
324 let context = KeyContext {
325 input_empty: true,
326 is_processing: false,
327 widget_blocking: false,
328 };
329
330 let ctrl_d = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL);
332 let result = handler.handle_key(ctrl_d, &context);
333 assert_eq!(result, AppKeyResult::Handled);
334 assert!(handler.status_hint().is_some());
335
336 let a = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
338 let result = handler.handle_key(a, &context);
339 assert_eq!(result, AppKeyResult::Action(AppKeyAction::InsertChar('a')));
340
341 assert!(handler.status_hint().is_none());
343 }
344}