1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2
3#[derive(Debug, Clone, Copy, PartialEq)]
4pub enum Mode {
5 Normal,
6 SpaceMenu,
7 ServicePicker,
8 ColumnSelector,
9 FilterInput,
10 EventFilterInput,
11 InsightsInput,
12 ErrorModal,
13 HelpModal,
14 RegionPicker,
15 ProfilePicker,
16 CalendarPicker,
17 TabPicker,
18 SessionPicker,
19}
20
21pub fn handle_key(key: KeyEvent, mode: Mode) -> Option<Action> {
22 match mode {
23 Mode::Normal => match key.code {
24 KeyCode::Char('q') => Some(Action::Quit),
25 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
26 Some(Action::Quit)
27 }
28 KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
29 Some(Action::CloseService)
30 }
31 KeyCode::Char('o') if key.modifiers.contains(KeyModifiers::CONTROL) => {
32 Some(Action::OpenInConsole)
33 }
34 KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => Some(Action::Refresh),
35 KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
36 Some(Action::PageUp)
37 }
38 KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
39 Some(Action::PageDown)
40 }
41 KeyCode::Esc => Some(Action::GoBack),
42 KeyCode::Char('i') => Some(Action::StartFilter),
43 KeyCode::Char('c') => Some(Action::OpenCalendar),
44 KeyCode::Down => Some(Action::NextItem),
45 KeyCode::Up => Some(Action::PrevItem),
46 KeyCode::Right => Some(Action::ExpandRow),
47 KeyCode::Left => Some(Action::CollapseRow),
48 KeyCode::Tab => Some(Action::NextDetailTab),
49 KeyCode::BackTab => Some(Action::PrevDetailTab),
50 KeyCode::Enter => Some(Action::Select),
51 KeyCode::Char(' ') => Some(Action::OpenSpaceMenu),
52 KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => {
53 Some(Action::CopyToClipboard)
54 }
55 KeyCode::Char('p') => Some(Action::OpenColumnSelector),
56 KeyCode::Char('e') => Some(Action::ToggleExactMatch),
57 KeyCode::Char('x') => Some(Action::ToggleShowExpired),
58 KeyCode::Char('s') => Some(Action::CycleSortColumn),
59 KeyCode::Char('o') => Some(Action::ToggleSortDirection),
60 KeyCode::Char('y') => Some(Action::Yank),
61 KeyCode::Char('[') => Some(Action::PrevTab),
62 KeyCode::Char(']') => Some(Action::NextTab),
63 KeyCode::Char('?') => Some(Action::ShowHelp),
64 KeyCode::Char('N')
65 if key.modifiers.contains(KeyModifiers::CONTROL)
66 && key.modifiers.contains(KeyModifiers::SHIFT) =>
67 {
68 Some(Action::NextTab)
69 }
70 KeyCode::Char('P')
71 if key.modifiers.contains(KeyModifiers::CONTROL)
72 && key.modifiers.contains(KeyModifiers::SHIFT) =>
73 {
74 Some(Action::PrevTab)
75 }
76 KeyCode::Char(c) if c.is_ascii_digit() => Some(Action::FilterInput(c)),
77 KeyCode::Char('P') => Some(Action::ApplyFilter),
78 _ => None,
79 },
80 Mode::SpaceMenu => match key.code {
81 KeyCode::Esc => Some(Action::CloseMenu),
82 KeyCode::Char('o') => Some(Action::OpenServicePicker),
83 KeyCode::Char('r') => Some(Action::OpenRegionPicker),
84 KeyCode::Char('p') => Some(Action::OpenProfilePicker),
85 KeyCode::Char('a') => Some(Action::OpenCloudWatchAlarms),
86 KeyCode::Char('c') => Some(Action::CloseService),
87 KeyCode::Char('b') | KeyCode::Char('t') => Some(Action::OpenTabPicker),
88 KeyCode::Char('s') => Some(Action::OpenSessionPicker),
89 KeyCode::Char('h') => Some(Action::ShowHelp),
90 _ => None,
91 },
92 Mode::ServicePicker => match key.code {
93 KeyCode::Esc => Some(Action::ExitFilterMode),
94 KeyCode::Char('i') if key.modifiers.is_empty() => Some(Action::EnterFilterMode),
95 KeyCode::Down => Some(Action::NextItem),
96 KeyCode::Up => Some(Action::PrevItem),
97 KeyCode::Enter => Some(Action::Select),
98 KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
99 Some(Action::DeleteWord)
100 }
101 KeyCode::Left if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordLeft),
102 KeyCode::Right if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordRight),
103 KeyCode::Char(c) if c != 'i' => Some(Action::FilterInput(c)),
104 KeyCode::Backspace => Some(Action::FilterBackspace),
105 _ => None,
106 },
107 Mode::ColumnSelector => match key.code {
108 KeyCode::Esc => Some(Action::CloseColumnSelector),
109 KeyCode::Down => Some(Action::NextItem),
110 KeyCode::Up => Some(Action::PrevItem),
111 KeyCode::Char(' ') | KeyCode::Enter => Some(Action::ToggleColumn),
112 KeyCode::Tab => Some(Action::NextPreferences),
113 KeyCode::BackTab => Some(Action::PrevPreferences),
114 KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
115 Some(Action::PageDown)
116 }
117 KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
118 Some(Action::PageUp)
119 }
120 _ => None,
121 },
122 Mode::FilterInput => match key.code {
123 KeyCode::Esc => Some(Action::CloseMenu),
124 KeyCode::Enter => Some(Action::ApplyFilter),
125 KeyCode::BackTab => Some(Action::PrevFilterFocus),
126 KeyCode::Tab if key.modifiers.contains(KeyModifiers::SHIFT) => {
127 Some(Action::PrevFilterFocus)
128 }
129 KeyCode::Tab => Some(Action::NextFilterFocus),
130 KeyCode::Up => Some(Action::PrevItem),
131 KeyCode::Down => Some(Action::NextItem),
132 KeyCode::Left if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordLeft),
133 KeyCode::Right if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordRight),
134 KeyCode::Left => Some(Action::PageUp),
135 KeyCode::Right => Some(Action::PageDown),
136 KeyCode::Char(' ') => Some(Action::ToggleFilterCheckbox),
137 KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
138 Some(Action::DeleteWord)
139 }
140 KeyCode::Char(c) if c != ' ' => Some(Action::FilterInput(c)),
141 KeyCode::Backspace => Some(Action::FilterBackspace),
142 _ => None,
143 },
144 Mode::EventFilterInput => match key.code {
145 KeyCode::Esc => Some(Action::CloseMenu),
146 KeyCode::Enter => Some(Action::ApplyFilter),
147 KeyCode::BackTab => Some(Action::PrevFilterFocus),
148 KeyCode::Tab if key.modifiers.contains(KeyModifiers::SHIFT) => {
149 Some(Action::PrevFilterFocus)
150 }
151 KeyCode::Tab => Some(Action::NextFilterFocus),
152 KeyCode::Char(' ') => Some(Action::ToggleFilterCheckbox),
153 KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
154 Some(Action::DeleteWord)
155 }
156 KeyCode::Left if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordLeft),
157 KeyCode::Right if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordRight),
158 KeyCode::Char(c) if c != ' ' => Some(Action::FilterInput(c)),
159 KeyCode::Backspace => Some(Action::FilterBackspace),
160 _ => None,
161 },
162 Mode::InsightsInput => match key.code {
163 KeyCode::Esc => Some(Action::CloseMenu),
164 KeyCode::Enter => Some(Action::Select),
165 KeyCode::Tab => Some(Action::NextFilterFocus),
166 KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
167 Some(Action::Refresh)
168 }
169 KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
170 Some(Action::DeleteWord)
171 }
172 KeyCode::Left if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordLeft),
173 KeyCode::Right if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordRight),
174 KeyCode::Down => Some(Action::NextItem),
175 KeyCode::Up => Some(Action::PrevItem),
176 KeyCode::Char(' ') => Some(Action::ToggleFilterCheckbox),
177 KeyCode::Char(c) if c != ' ' => Some(Action::FilterInput(c)),
178 KeyCode::Backspace => Some(Action::FilterBackspace),
179 _ => None,
180 },
181 Mode::ErrorModal => match key.code {
182 KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
183 Some(Action::RetryLoad)
184 }
185 KeyCode::Char('y') => Some(Action::Yank),
186 KeyCode::Char('q') | KeyCode::Esc => Some(Action::Quit),
187 _ => None,
188 },
189 Mode::HelpModal => match key.code {
190 KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') | KeyCode::Char('?') => {
191 Some(Action::CloseMenu)
192 }
193 _ => None,
194 },
195 Mode::RegionPicker => match key.code {
196 KeyCode::Esc => Some(Action::ExitFilterMode),
197 KeyCode::Char('i') => Some(Action::EnterFilterMode),
198 KeyCode::Char('j') | KeyCode::Down => Some(Action::NextItem),
199 KeyCode::Char('k') | KeyCode::Up => Some(Action::PrevItem),
200 KeyCode::Enter => Some(Action::Select),
201 KeyCode::Char('l') if key.modifiers.contains(KeyModifiers::CONTROL) => {
202 Some(Action::MeasureLatency)
203 }
204 KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => Some(Action::Refresh),
205 KeyCode::Backspace => Some(Action::FilterBackspace),
206 KeyCode::Char(c) => Some(Action::FilterInput(c)),
207 _ => None,
208 },
209 Mode::CalendarPicker => match key.code {
210 KeyCode::Esc => Some(Action::CloseCalendar),
211 KeyCode::Left => Some(Action::CalendarPrevDay),
212 KeyCode::Down => Some(Action::CalendarNextWeek),
213 KeyCode::Up => Some(Action::CalendarPrevWeek),
214 KeyCode::Right => Some(Action::CalendarNextDay),
215 KeyCode::Char('n') | KeyCode::Tab => Some(Action::CalendarNextMonth),
216 KeyCode::Char('p') | KeyCode::BackTab => Some(Action::CalendarPrevMonth),
217 KeyCode::Enter => Some(Action::CalendarSelect),
218 _ => None,
219 },
220 Mode::TabPicker => match key.code {
221 KeyCode::Esc => Some(Action::CloseMenu),
222 KeyCode::Down => Some(Action::NextItem),
223 KeyCode::Up => Some(Action::PrevItem),
224 KeyCode::Enter => Some(Action::Select),
225 KeyCode::Backspace => Some(Action::FilterBackspace),
226 KeyCode::Char(c) => Some(Action::FilterInput(c)),
227 _ => None,
228 },
229 Mode::SessionPicker => match key.code {
230 KeyCode::Esc => Some(Action::ExitFilterMode),
231 KeyCode::Char('i') => Some(Action::EnterFilterMode),
232 KeyCode::Down => Some(Action::NextItem),
233 KeyCode::Up => Some(Action::PrevItem),
234 KeyCode::Enter => Some(Action::LoadSession),
235 KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => Some(Action::Refresh),
236 KeyCode::Backspace => Some(Action::FilterBackspace),
237 KeyCode::Char(c) => Some(Action::FilterInput(c)),
238 _ => None,
239 },
240 Mode::ProfilePicker => match key.code {
241 KeyCode::Esc => Some(Action::ExitFilterMode),
242 KeyCode::Char('i') => Some(Action::EnterFilterMode),
243 KeyCode::Down => Some(Action::NextItem),
244 KeyCode::Up => Some(Action::PrevItem),
245 KeyCode::Enter => Some(Action::Select),
246 KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => Some(Action::Refresh),
247 KeyCode::Backspace => Some(Action::FilterBackspace),
248 KeyCode::Char(c) => Some(Action::FilterInput(c)),
249 _ => None,
250 },
251 }
252}
253
254#[derive(Debug, Clone, PartialEq)]
255pub enum Action {
256 Quit,
257 CloseService,
258 NextItem,
259 PrevItem,
260 NextPane,
261 PrevPane,
262 CollapseRow,
263 ExpandRow,
264 Select,
265 OpenSpaceMenu,
266 CloseMenu,
267 OpenServicePicker,
268 OpenCloudWatch,
269 OpenCloudWatchSplit,
270 OpenCloudWatchAlarms,
271 FilterInput(char),
272 FilterBackspace,
273 DeleteWord,
274 WordLeft,
275 WordRight,
276 OpenColumnSelector,
277 ToggleColumn,
278 NextPreferences,
279 PrevPreferences,
280 CloseColumnSelector,
281 StartFilter,
282 StartEventFilter,
283 ApplyFilter,
284 ToggleExactMatch,
285 ToggleShowExpired,
286 GoBack,
287 NextFilterFocus,
288 PrevFilterFocus,
289 ToggleFilterCheckbox,
290 CycleSortColumn,
291 ToggleSortDirection,
292 ScrollUp,
293 ScrollDown,
294 PageUp,
295 PageDown,
296 Refresh,
297 MeasureLatency,
298 RetryLoad,
299 Yank,
300 OpenInConsole,
301 OpenInBrowser,
302 ShowHelp,
303 OpenRegionPicker,
304 OpenCalendar,
305 CloseCalendar,
306 CalendarPrevDay,
307 CalendarNextDay,
308 CalendarPrevWeek,
309 CalendarNextWeek,
310 CalendarPrevMonth,
311 CalendarNextMonth,
312 CalendarSelect,
313 NextTab,
314 PrevTab,
315 NextDetailTab,
316 PrevDetailTab,
317 CloseTab,
318 OpenTabPicker,
319 OpenSessionPicker,
320 OpenProfilePicker,
321 LoadSession,
322 SaveSession,
323 CopyToClipboard,
324 EnterFilterMode,
325 ExitFilterMode,
326 Noop,
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_space_o_opens_service_menu() {
335 let key = KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE);
336 let action = handle_key(key, Mode::SpaceMenu);
337 assert_eq!(action, Some(Action::OpenServicePicker));
338 }
339
340 #[test]
341 fn test_insights_input_accepts_chars() {
342 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
343 let action = handle_key(key, Mode::InsightsInput);
344 assert_eq!(action, Some(Action::FilterInput('a')));
345
346 let key2 = KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE);
347 let action2 = handle_key(key2, Mode::InsightsInput);
348 assert_eq!(action2, Some(Action::FilterInput('1')));
349 }
350
351 #[test]
352 fn test_insights_input_esc_closes() {
353 let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
354 let action = handle_key(key, Mode::InsightsInput);
355 assert_eq!(action, Some(Action::CloseMenu));
356 }
357
358 #[test]
359 fn test_service_menu_accepts_input() {
360 let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE);
361 let action = handle_key(key, Mode::ServicePicker);
362 assert_eq!(action, Some(Action::FilterInput('c')));
363 }
364
365 #[test]
366 fn test_service_menu_navigation() {
367 let key_down = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
368 let action_down = handle_key(key_down, Mode::ServicePicker);
369 assert_eq!(action_down, Some(Action::NextItem));
370
371 let key_up = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
372 let action_up = handle_key(key_up, Mode::ServicePicker);
373 assert_eq!(action_up, Some(Action::PrevItem));
374 }
375
376 #[test]
377 fn test_service_menu_backspace() {
378 let key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
379 let action = handle_key(key, Mode::ServicePicker);
380 assert_eq!(action, Some(Action::FilterBackspace));
381 }
382
383 #[test]
384 fn test_ctrl_shift_n_next_tab() {
385 let key = KeyEvent::new(
386 KeyCode::Char('N'),
387 KeyModifiers::CONTROL | KeyModifiers::SHIFT,
388 );
389 let action = handle_key(key, Mode::Normal);
390 assert_eq!(action, Some(Action::NextTab));
391 }
392
393 #[test]
394 fn test_ctrl_shift_p_prev_tab() {
395 let key = KeyEvent::new(
396 KeyCode::Char('P'),
397 KeyModifiers::CONTROL | KeyModifiers::SHIFT,
398 );
399 let action = handle_key(key, Mode::Normal);
400 assert_eq!(action, Some(Action::PrevTab));
401 }
402
403 #[test]
404 fn test_space_c_close_tab() {
405 let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE);
406 let action = handle_key(key, Mode::SpaceMenu);
407 assert_eq!(action, Some(Action::CloseService));
408 }
409
410 #[test]
411 fn test_space_b_window_picker() {
412 let key = KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE);
413 let action = handle_key(key, Mode::SpaceMenu);
414 assert_eq!(action, Some(Action::OpenTabPicker));
415 }
416
417 #[test]
418 fn test_window_picker_navigation() {
419 let key_down = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
420 let action = handle_key(key_down, Mode::TabPicker);
421 assert_eq!(action, Some(Action::NextItem));
422
423 let key_up = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
424 let action_up = handle_key(key_up, Mode::TabPicker);
425 assert_eq!(action_up, Some(Action::PrevItem));
426 }
427
428 #[test]
429 fn test_window_picker_select() {
430 let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
431 let action = handle_key(key, Mode::TabPicker);
432 assert_eq!(action, Some(Action::Select));
433 }
434
435 #[test]
436 fn test_space_opens_space_menu_in_normal_mode() {
437 let key = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE);
438 let action = handle_key(key, Mode::Normal);
439 assert_eq!(action, Some(Action::OpenSpaceMenu));
440 }
441
442 #[test]
443 fn test_space_menu_o_opens_service_menu() {
444 let key = KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE);
445 let action = handle_key(key, Mode::SpaceMenu);
446 assert_eq!(action, Some(Action::OpenServicePicker));
447 }
448
449 #[test]
450 fn test_ctrl_r_refreshes_profile_picker() {
451 let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL);
452 let action = handle_key(key, Mode::ProfilePicker);
453 assert_eq!(action, Some(Action::Refresh));
454 }
455
456 #[test]
457 fn test_ctrl_r_refreshes_region_picker() {
458 let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL);
459 let action = handle_key(key, Mode::RegionPicker);
460 assert_eq!(action, Some(Action::Refresh));
461 }
462
463 #[test]
464 fn test_ctrl_r_refreshes_session_picker() {
465 let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL);
466 let action = handle_key(key, Mode::SessionPicker);
467 assert_eq!(action, Some(Action::Refresh));
468 }
469
470 #[test]
471 fn test_p_opens_column_selector() {
472 let key = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE);
473 let action = handle_key(key, Mode::Normal);
474 assert_eq!(
475 action,
476 Some(Action::OpenColumnSelector),
477 "p should open column selector (preferences)"
478 );
479 }
480
481 #[test]
482 fn test_ctrl_p_copies_to_clipboard() {
483 let key = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL);
484 let action = handle_key(key, Mode::Normal);
485 assert_eq!(
486 action,
487 Some(Action::CopyToClipboard),
488 "Ctrl+P should copy screen to clipboard (print)"
489 );
490 }
491
492 #[test]
493 fn test_y_yanks_selected_item() {
494 let key = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE);
495 let action = handle_key(key, Mode::Normal);
496 assert_eq!(
497 action,
498 Some(Action::Yank),
499 "y should yank (copy) selected item"
500 );
501 }
502
503 #[test]
504 fn test_space_toggles_checkbox_in_filter_input() {
505 let key = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE);
506 let action = handle_key(key, Mode::FilterInput);
507 assert_eq!(
508 action,
509 Some(Action::ToggleFilterCheckbox),
510 "Space should toggle checkbox in FilterInput mode"
511 );
512 }
513
514 #[test]
515 fn test_space_not_added_to_filter_text() {
516 let key = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE);
518 let action = handle_key(key, Mode::FilterInput);
519 assert_ne!(
520 action,
521 Some(Action::FilterInput(' ')),
522 "Space should not be added to filter text"
523 );
524 }
525}