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::CloseMenu),
94 KeyCode::Down => Some(Action::NextItem),
95 KeyCode::Up => Some(Action::PrevItem),
96 KeyCode::Enter => Some(Action::Select),
97 KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
98 Some(Action::DeleteWord)
99 }
100 KeyCode::Left if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordLeft),
101 KeyCode::Right if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordRight),
102 KeyCode::Char(c) => Some(Action::FilterInput(c)),
103 KeyCode::Backspace => Some(Action::FilterBackspace),
104 _ => None,
105 },
106 Mode::ColumnSelector => match key.code {
107 KeyCode::Esc => Some(Action::CloseColumnSelector),
108 KeyCode::Down => Some(Action::NextItem),
109 KeyCode::Up => Some(Action::PrevItem),
110 KeyCode::Char(' ') | KeyCode::Enter => Some(Action::ToggleColumn),
111 KeyCode::Tab => Some(Action::NextPreferences),
112 KeyCode::BackTab => Some(Action::PrevPreferences),
113 KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
114 Some(Action::PageDown)
115 }
116 KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
117 Some(Action::PageUp)
118 }
119 _ => None,
120 },
121 Mode::FilterInput => match key.code {
122 KeyCode::Esc => Some(Action::CloseMenu),
123 KeyCode::Enter => Some(Action::ApplyFilter),
124 KeyCode::BackTab => Some(Action::PrevFilterFocus),
125 KeyCode::Tab if key.modifiers.contains(KeyModifiers::SHIFT) => {
126 Some(Action::PrevFilterFocus)
127 }
128 KeyCode::Tab => Some(Action::NextFilterFocus),
129 KeyCode::Up => Some(Action::PrevItem),
130 KeyCode::Down => Some(Action::NextItem),
131 KeyCode::Left if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordLeft),
132 KeyCode::Right if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordRight),
133 KeyCode::Left => Some(Action::PageUp),
134 KeyCode::Right => Some(Action::PageDown),
135 KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
136 Some(Action::DeleteWord)
137 }
138 KeyCode::Char(c) => Some(Action::FilterInput(c)),
139 KeyCode::Backspace => Some(Action::FilterBackspace),
140 _ => None,
141 },
142 Mode::EventFilterInput => match key.code {
143 KeyCode::Esc => Some(Action::CloseMenu),
144 KeyCode::Enter => Some(Action::ApplyFilter),
145 KeyCode::BackTab => Some(Action::PrevFilterFocus),
146 KeyCode::Tab if key.modifiers.contains(KeyModifiers::SHIFT) => {
147 Some(Action::PrevFilterFocus)
148 }
149 KeyCode::Tab => Some(Action::NextFilterFocus),
150 KeyCode::Char(' ') => Some(Action::ToggleFilterCheckbox),
151 KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
152 Some(Action::DeleteWord)
153 }
154 KeyCode::Left if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordLeft),
155 KeyCode::Right if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordRight),
156 KeyCode::Char(c) if c != ' ' => Some(Action::FilterInput(c)),
157 KeyCode::Backspace => Some(Action::FilterBackspace),
158 _ => None,
159 },
160 Mode::InsightsInput => match key.code {
161 KeyCode::Esc => Some(Action::CloseMenu),
162 KeyCode::Enter => Some(Action::Select),
163 KeyCode::Tab => Some(Action::NextFilterFocus),
164 KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
165 Some(Action::Refresh)
166 }
167 KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
168 Some(Action::DeleteWord)
169 }
170 KeyCode::Left if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordLeft),
171 KeyCode::Right if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordRight),
172 KeyCode::Down => Some(Action::NextItem),
173 KeyCode::Up => Some(Action::PrevItem),
174 KeyCode::Char(' ') => Some(Action::ToggleFilterCheckbox),
175 KeyCode::Char(c) if c != ' ' => Some(Action::FilterInput(c)),
176 KeyCode::Backspace => Some(Action::FilterBackspace),
177 _ => None,
178 },
179 Mode::ErrorModal => match key.code {
180 KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
181 Some(Action::RetryLoad)
182 }
183 KeyCode::Char('y') => Some(Action::Yank),
184 KeyCode::Char('q') | KeyCode::Esc => Some(Action::Quit),
185 _ => None,
186 },
187 Mode::HelpModal => match key.code {
188 KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') | KeyCode::Char('?') => {
189 Some(Action::CloseMenu)
190 }
191 _ => None,
192 },
193 Mode::RegionPicker => match key.code {
194 KeyCode::Esc => Some(Action::CloseMenu),
195 KeyCode::Char('j') | KeyCode::Down => Some(Action::NextItem),
196 KeyCode::Char('k') | KeyCode::Up => Some(Action::PrevItem),
197 KeyCode::Enter => Some(Action::Select),
198 KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => Some(Action::Refresh),
199 KeyCode::Backspace => Some(Action::FilterBackspace),
200 KeyCode::Char(c) => Some(Action::FilterInput(c)),
201 _ => None,
202 },
203 Mode::CalendarPicker => match key.code {
204 KeyCode::Esc => Some(Action::CloseCalendar),
205 KeyCode::Left => Some(Action::CalendarPrevDay),
206 KeyCode::Down => Some(Action::CalendarNextWeek),
207 KeyCode::Up => Some(Action::CalendarPrevWeek),
208 KeyCode::Right => Some(Action::CalendarNextDay),
209 KeyCode::Char('n') | KeyCode::Tab => Some(Action::CalendarNextMonth),
210 KeyCode::Char('p') | KeyCode::BackTab => Some(Action::CalendarPrevMonth),
211 KeyCode::Enter => Some(Action::CalendarSelect),
212 _ => None,
213 },
214 Mode::TabPicker => match key.code {
215 KeyCode::Esc => Some(Action::CloseMenu),
216 KeyCode::Down => Some(Action::NextItem),
217 KeyCode::Up => Some(Action::PrevItem),
218 KeyCode::Enter => Some(Action::Select),
219 KeyCode::Backspace => Some(Action::FilterBackspace),
220 KeyCode::Char(c) => Some(Action::FilterInput(c)),
221 _ => None,
222 },
223 Mode::SessionPicker => match key.code {
224 KeyCode::Esc => Some(Action::CloseMenu),
225 KeyCode::Down => Some(Action::NextItem),
226 KeyCode::Up => Some(Action::PrevItem),
227 KeyCode::Enter => Some(Action::LoadSession),
228 KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => Some(Action::Refresh),
229 KeyCode::Backspace => Some(Action::FilterBackspace),
230 KeyCode::Char(c) => Some(Action::FilterInput(c)),
231 _ => None,
232 },
233 Mode::ProfilePicker => match key.code {
234 KeyCode::Esc => Some(Action::CloseMenu),
235 KeyCode::Down => Some(Action::NextItem),
236 KeyCode::Up => Some(Action::PrevItem),
237 KeyCode::Enter => Some(Action::Select),
238 KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => Some(Action::Refresh),
239 KeyCode::Backspace => Some(Action::FilterBackspace),
240 KeyCode::Char(c) => Some(Action::FilterInput(c)),
241 _ => None,
242 },
243 }
244}
245
246#[derive(Debug, Clone, PartialEq)]
247pub enum Action {
248 Quit,
249 CloseService,
250 NextItem,
251 PrevItem,
252 NextPane,
253 PrevPane,
254 CollapseRow,
255 ExpandRow,
256 Select,
257 OpenSpaceMenu,
258 CloseMenu,
259 OpenServicePicker,
260 OpenCloudWatch,
261 OpenCloudWatchSplit,
262 OpenCloudWatchAlarms,
263 FilterInput(char),
264 FilterBackspace,
265 DeleteWord,
266 WordLeft,
267 WordRight,
268 OpenColumnSelector,
269 ToggleColumn,
270 NextPreferences,
271 PrevPreferences,
272 CloseColumnSelector,
273 StartFilter,
274 StartEventFilter,
275 ApplyFilter,
276 ToggleExactMatch,
277 ToggleShowExpired,
278 GoBack,
279 NextFilterFocus,
280 PrevFilterFocus,
281 ToggleFilterCheckbox,
282 CycleSortColumn,
283 ToggleSortDirection,
284 ScrollUp,
285 ScrollDown,
286 PageUp,
287 PageDown,
288 Refresh,
289 RetryLoad,
290 Yank,
291 OpenInConsole,
292 OpenInBrowser,
293 ShowHelp,
294 OpenRegionPicker,
295 OpenCalendar,
296 CloseCalendar,
297 CalendarPrevDay,
298 CalendarNextDay,
299 CalendarPrevWeek,
300 CalendarNextWeek,
301 CalendarPrevMonth,
302 CalendarNextMonth,
303 CalendarSelect,
304 NextTab,
305 PrevTab,
306 NextDetailTab,
307 PrevDetailTab,
308 CloseTab,
309 OpenTabPicker,
310 OpenSessionPicker,
311 OpenProfilePicker,
312 LoadSession,
313 SaveSession,
314 CopyToClipboard,
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_space_o_opens_service_menu() {
323 let key = KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE);
324 let action = handle_key(key, Mode::SpaceMenu);
325 assert_eq!(action, Some(Action::OpenServicePicker));
326 }
327
328 #[test]
329 fn test_insights_input_accepts_chars() {
330 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
331 let action = handle_key(key, Mode::InsightsInput);
332 assert_eq!(action, Some(Action::FilterInput('a')));
333
334 let key2 = KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE);
335 let action2 = handle_key(key2, Mode::InsightsInput);
336 assert_eq!(action2, Some(Action::FilterInput('1')));
337 }
338
339 #[test]
340 fn test_insights_input_esc_closes() {
341 let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
342 let action = handle_key(key, Mode::InsightsInput);
343 assert_eq!(action, Some(Action::CloseMenu));
344 }
345
346 #[test]
347 fn test_service_menu_accepts_input() {
348 let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE);
349 let action = handle_key(key, Mode::ServicePicker);
350 assert_eq!(action, Some(Action::FilterInput('c')));
351 }
352
353 #[test]
354 fn test_service_menu_navigation() {
355 let key_down = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
356 let action_down = handle_key(key_down, Mode::ServicePicker);
357 assert_eq!(action_down, Some(Action::NextItem));
358
359 let key_up = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
360 let action_up = handle_key(key_up, Mode::ServicePicker);
361 assert_eq!(action_up, Some(Action::PrevItem));
362 }
363
364 #[test]
365 fn test_service_menu_backspace() {
366 let key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
367 let action = handle_key(key, Mode::ServicePicker);
368 assert_eq!(action, Some(Action::FilterBackspace));
369 }
370
371 #[test]
372 fn test_ctrl_shift_n_next_tab() {
373 let key = KeyEvent::new(
374 KeyCode::Char('N'),
375 KeyModifiers::CONTROL | KeyModifiers::SHIFT,
376 );
377 let action = handle_key(key, Mode::Normal);
378 assert_eq!(action, Some(Action::NextTab));
379 }
380
381 #[test]
382 fn test_ctrl_shift_p_prev_tab() {
383 let key = KeyEvent::new(
384 KeyCode::Char('P'),
385 KeyModifiers::CONTROL | KeyModifiers::SHIFT,
386 );
387 let action = handle_key(key, Mode::Normal);
388 assert_eq!(action, Some(Action::PrevTab));
389 }
390
391 #[test]
392 fn test_space_c_close_tab() {
393 let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE);
394 let action = handle_key(key, Mode::SpaceMenu);
395 assert_eq!(action, Some(Action::CloseService));
396 }
397
398 #[test]
399 fn test_space_b_window_picker() {
400 let key = KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE);
401 let action = handle_key(key, Mode::SpaceMenu);
402 assert_eq!(action, Some(Action::OpenTabPicker));
403 }
404
405 #[test]
406 fn test_window_picker_navigation() {
407 let key_down = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
408 let action = handle_key(key_down, Mode::TabPicker);
409 assert_eq!(action, Some(Action::NextItem));
410
411 let key_up = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
412 let action_up = handle_key(key_up, Mode::TabPicker);
413 assert_eq!(action_up, Some(Action::PrevItem));
414 }
415
416 #[test]
417 fn test_window_picker_select() {
418 let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
419 let action = handle_key(key, Mode::TabPicker);
420 assert_eq!(action, Some(Action::Select));
421 }
422
423 #[test]
424 fn test_space_opens_space_menu_in_normal_mode() {
425 let key = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE);
426 let action = handle_key(key, Mode::Normal);
427 assert_eq!(action, Some(Action::OpenSpaceMenu));
428 }
429
430 #[test]
431 fn test_space_menu_o_opens_service_menu() {
432 let key = KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE);
433 let action = handle_key(key, Mode::SpaceMenu);
434 assert_eq!(action, Some(Action::OpenServicePicker));
435 }
436
437 #[test]
438 fn test_ctrl_r_refreshes_profile_picker() {
439 let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL);
440 let action = handle_key(key, Mode::ProfilePicker);
441 assert_eq!(action, Some(Action::Refresh));
442 }
443
444 #[test]
445 fn test_ctrl_r_refreshes_region_picker() {
446 let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL);
447 let action = handle_key(key, Mode::RegionPicker);
448 assert_eq!(action, Some(Action::Refresh));
449 }
450
451 #[test]
452 fn test_ctrl_r_refreshes_session_picker() {
453 let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL);
454 let action = handle_key(key, Mode::SessionPicker);
455 assert_eq!(action, Some(Action::Refresh));
456 }
457
458 #[test]
459 fn test_p_opens_column_selector() {
460 let key = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE);
461 let action = handle_key(key, Mode::Normal);
462 assert_eq!(
463 action,
464 Some(Action::OpenColumnSelector),
465 "p should open column selector (preferences)"
466 );
467 }
468
469 #[test]
470 fn test_ctrl_p_copies_to_clipboard() {
471 let key = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL);
472 let action = handle_key(key, Mode::Normal);
473 assert_eq!(
474 action,
475 Some(Action::CopyToClipboard),
476 "Ctrl+P should copy screen to clipboard (print)"
477 );
478 }
479
480 #[test]
481 fn test_y_yanks_selected_item() {
482 let key = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE);
483 let action = handle_key(key, Mode::Normal);
484 assert_eq!(
485 action,
486 Some(Action::Yank),
487 "y should yank (copy) selected item"
488 );
489 }
490}