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}