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}