rusticity_term/
keymap.rs

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}