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            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    Select,
255    OpenSpaceMenu,
256    CloseMenu,
257    OpenServicePicker,
258    OpenCloudWatch,
259    OpenCloudWatchSplit,
260    OpenCloudWatchAlarms,
261    FilterInput(char),
262    FilterBackspace,
263    DeleteWord,
264    WordLeft,
265    WordRight,
266    OpenColumnSelector,
267    ToggleColumn,
268    NextPreferences,
269    PrevPreferences,
270    CloseColumnSelector,
271    StartFilter,
272    StartEventFilter,
273    ApplyFilter,
274    ToggleExactMatch,
275    ToggleShowExpired,
276    GoBack,
277    NextFilterFocus,
278    PrevFilterFocus,
279    ToggleFilterCheckbox,
280    CycleSortColumn,
281    ToggleSortDirection,
282    ScrollUp,
283    ScrollDown,
284    PageUp,
285    PageDown,
286    Refresh,
287    RetryLoad,
288    Yank,
289    OpenInConsole,
290    OpenInBrowser,
291    ShowHelp,
292    OpenRegionPicker,
293    OpenCalendar,
294    CloseCalendar,
295    CalendarPrevDay,
296    CalendarNextDay,
297    CalendarPrevWeek,
298    CalendarNextWeek,
299    CalendarPrevMonth,
300    CalendarNextMonth,
301    CalendarSelect,
302    NextTab,
303    PrevTab,
304    NextDetailTab,
305    PrevDetailTab,
306    CloseTab,
307    OpenTabPicker,
308    OpenSessionPicker,
309    OpenProfilePicker,
310    LoadSession,
311    SaveSession,
312    CopyToClipboard,
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn test_space_o_opens_service_menu() {
321        let key = KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE);
322        let action = handle_key(key, Mode::SpaceMenu);
323        assert_eq!(action, Some(Action::OpenServicePicker));
324    }
325
326    #[test]
327    fn test_insights_input_accepts_chars() {
328        let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
329        let action = handle_key(key, Mode::InsightsInput);
330        assert_eq!(action, Some(Action::FilterInput('a')));
331
332        let key2 = KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE);
333        let action2 = handle_key(key2, Mode::InsightsInput);
334        assert_eq!(action2, Some(Action::FilterInput('1')));
335    }
336
337    #[test]
338    fn test_insights_input_esc_closes() {
339        let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
340        let action = handle_key(key, Mode::InsightsInput);
341        assert_eq!(action, Some(Action::CloseMenu));
342    }
343
344    #[test]
345    fn test_service_menu_accepts_input() {
346        let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE);
347        let action = handle_key(key, Mode::ServicePicker);
348        assert_eq!(action, Some(Action::FilterInput('c')));
349    }
350
351    #[test]
352    fn test_service_menu_navigation() {
353        let key_down = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
354        let action_down = handle_key(key_down, Mode::ServicePicker);
355        assert_eq!(action_down, Some(Action::NextItem));
356
357        let key_up = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
358        let action_up = handle_key(key_up, Mode::ServicePicker);
359        assert_eq!(action_up, Some(Action::PrevItem));
360    }
361
362    #[test]
363    fn test_service_menu_backspace() {
364        let key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
365        let action = handle_key(key, Mode::ServicePicker);
366        assert_eq!(action, Some(Action::FilterBackspace));
367    }
368
369    #[test]
370    fn test_ctrl_shift_n_next_tab() {
371        let key = KeyEvent::new(
372            KeyCode::Char('N'),
373            KeyModifiers::CONTROL | KeyModifiers::SHIFT,
374        );
375        let action = handle_key(key, Mode::Normal);
376        assert_eq!(action, Some(Action::NextTab));
377    }
378
379    #[test]
380    fn test_ctrl_shift_p_prev_tab() {
381        let key = KeyEvent::new(
382            KeyCode::Char('P'),
383            KeyModifiers::CONTROL | KeyModifiers::SHIFT,
384        );
385        let action = handle_key(key, Mode::Normal);
386        assert_eq!(action, Some(Action::PrevTab));
387    }
388
389    #[test]
390    fn test_space_c_close_tab() {
391        let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE);
392        let action = handle_key(key, Mode::SpaceMenu);
393        assert_eq!(action, Some(Action::CloseService));
394    }
395
396    #[test]
397    fn test_space_b_window_picker() {
398        let key = KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE);
399        let action = handle_key(key, Mode::SpaceMenu);
400        assert_eq!(action, Some(Action::OpenTabPicker));
401    }
402
403    #[test]
404    fn test_window_picker_navigation() {
405        let key_down = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
406        let action = handle_key(key_down, Mode::TabPicker);
407        assert_eq!(action, Some(Action::NextItem));
408
409        let key_up = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
410        let action_up = handle_key(key_up, Mode::TabPicker);
411        assert_eq!(action_up, Some(Action::PrevItem));
412    }
413
414    #[test]
415    fn test_window_picker_select() {
416        let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
417        let action = handle_key(key, Mode::TabPicker);
418        assert_eq!(action, Some(Action::Select));
419    }
420
421    #[test]
422    fn test_space_opens_space_menu_in_normal_mode() {
423        let key = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE);
424        let action = handle_key(key, Mode::Normal);
425        assert_eq!(action, Some(Action::OpenSpaceMenu));
426    }
427
428    #[test]
429    fn test_space_menu_o_opens_service_menu() {
430        let key = KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE);
431        let action = handle_key(key, Mode::SpaceMenu);
432        assert_eq!(action, Some(Action::OpenServicePicker));
433    }
434
435    #[test]
436    fn test_ctrl_r_refreshes_profile_picker() {
437        let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL);
438        let action = handle_key(key, Mode::ProfilePicker);
439        assert_eq!(action, Some(Action::Refresh));
440    }
441
442    #[test]
443    fn test_ctrl_r_refreshes_region_picker() {
444        let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL);
445        let action = handle_key(key, Mode::RegionPicker);
446        assert_eq!(action, Some(Action::Refresh));
447    }
448
449    #[test]
450    fn test_ctrl_r_refreshes_session_picker() {
451        let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL);
452        let action = handle_key(key, Mode::SessionPicker);
453        assert_eq!(action, Some(Action::Refresh));
454    }
455}