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