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 QuitConfirm,
20}
21
22pub fn handle_key(key: KeyEvent, mode: Mode) -> Option<Action> {
23 match mode {
24 Mode::Normal => match key.code {
25 KeyCode::Char('q') => Some(Action::Quit),
26 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
27 Some(Action::ConfirmQuit)
28 }
29 KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
30 Some(Action::CloseService)
31 }
32 KeyCode::Char('o') if key.modifiers.contains(KeyModifiers::CONTROL) => {
33 Some(Action::OpenInConsole)
34 }
35 KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => Some(Action::Refresh),
36 KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
37 Some(Action::PageUp)
38 }
39 KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
40 Some(Action::PageDown)
41 }
42 KeyCode::Esc => Some(Action::GoBack),
43 KeyCode::Char('i') => Some(Action::StartFilter),
44 KeyCode::Char('c') => Some(Action::OpenCalendar),
45 KeyCode::Down => Some(Action::NextItem),
46 KeyCode::Up => Some(Action::PrevItem),
47 KeyCode::Right => Some(Action::ExpandRow),
48 KeyCode::Left => Some(Action::CollapseRow),
49 KeyCode::Tab => Some(Action::NextDetailTab),
50 KeyCode::BackTab => Some(Action::PrevDetailTab),
51 KeyCode::Enter => Some(Action::Select),
52 KeyCode::Char(' ') => Some(Action::OpenSpaceMenu),
53 KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => {
54 Some(Action::CopyToClipboard)
55 }
56 KeyCode::Char('p') => Some(Action::OpenColumnSelector),
57 KeyCode::Char('e') => Some(Action::ToggleExactMatch),
58 KeyCode::Char('x') => Some(Action::ToggleShowExpired),
59 KeyCode::Char('s') => Some(Action::CycleSortColumn),
60 KeyCode::Char('o') => Some(Action::ToggleSortDirection),
61 KeyCode::Char('y') => Some(Action::Yank),
62 KeyCode::Char('[') => Some(Action::PrevTab),
63 KeyCode::Char(']') => Some(Action::NextTab),
64 KeyCode::Char('?') => Some(Action::ShowHelp),
65 KeyCode::Char('N')
66 if key.modifiers.contains(KeyModifiers::CONTROL)
67 && key.modifiers.contains(KeyModifiers::SHIFT) =>
68 {
69 Some(Action::NextTab)
70 }
71 KeyCode::Char('P')
72 if key.modifiers.contains(KeyModifiers::CONTROL)
73 && key.modifiers.contains(KeyModifiers::SHIFT) =>
74 {
75 Some(Action::PrevTab)
76 }
77 KeyCode::Char(c) if c.is_ascii_digit() => Some(Action::FilterInput(c)),
78 KeyCode::Char('P') => Some(Action::ApplyFilter),
79 _ => None,
80 },
81 Mode::SpaceMenu => match key.code {
82 KeyCode::Esc => Some(Action::CloseMenu),
83 KeyCode::Char('o') => Some(Action::OpenServicePicker),
84 KeyCode::Char('r') => Some(Action::OpenRegionPicker),
85 KeyCode::Char('p') => Some(Action::OpenProfilePicker),
86 KeyCode::Char('a') => Some(Action::OpenCloudWatchAlarms),
87 KeyCode::Char('c') => Some(Action::CloseService),
88 KeyCode::Char('b') | KeyCode::Char('t') => Some(Action::OpenTabPicker),
89 KeyCode::Char('s') => Some(Action::OpenSessionPicker),
90 KeyCode::Char('h') => Some(Action::ShowHelp),
91 _ => None,
92 },
93 Mode::ServicePicker => match key.code {
94 KeyCode::Esc => Some(Action::ExitFilterMode),
95 KeyCode::Down => Some(Action::NextItem),
96 KeyCode::Up => Some(Action::PrevItem),
97 KeyCode::Enter => Some(Action::Select),
98 KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
99 Some(Action::DeleteWord)
100 }
101 KeyCode::Left if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordLeft),
102 KeyCode::Right if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordRight),
103 KeyCode::Char(c) => Some(Action::FilterInput(c)),
104 KeyCode::Backspace => Some(Action::FilterBackspace),
105 _ => None,
106 },
107 Mode::ColumnSelector => match key.code {
108 KeyCode::Esc => Some(Action::CloseColumnSelector),
109 KeyCode::Down => Some(Action::NextItem),
110 KeyCode::Up => Some(Action::PrevItem),
111 KeyCode::Char(' ') | KeyCode::Enter => Some(Action::ToggleColumn),
112 KeyCode::Tab => Some(Action::NextPreferences),
113 KeyCode::BackTab => Some(Action::PrevPreferences),
114 KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
115 Some(Action::PageDown)
116 }
117 KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
118 Some(Action::PageUp)
119 }
120 _ => None,
121 },
122 Mode::FilterInput => match key.code {
123 KeyCode::Esc => Some(Action::CloseMenu),
124 KeyCode::Enter => Some(Action::ApplyFilter),
125 KeyCode::BackTab => Some(Action::PrevFilterFocus),
126 KeyCode::Tab if key.modifiers.contains(KeyModifiers::SHIFT) => {
127 Some(Action::PrevFilterFocus)
128 }
129 KeyCode::Tab => Some(Action::NextFilterFocus),
130 KeyCode::Up => Some(Action::PrevItem),
131 KeyCode::Down => Some(Action::NextItem),
132 KeyCode::Left if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordLeft),
133 KeyCode::Right if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordRight),
134 KeyCode::Left => Some(Action::PageUp),
135 KeyCode::Right => Some(Action::PageDown),
136 KeyCode::Char(' ') => Some(Action::ToggleFilterCheckbox),
137 KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
138 Some(Action::DeleteWord)
139 }
140 KeyCode::Char(c) if c != ' ' => Some(Action::FilterInput(c)),
141 KeyCode::Backspace => Some(Action::FilterBackspace),
142 _ => None,
143 },
144 Mode::EventFilterInput => match key.code {
145 KeyCode::Esc => Some(Action::CloseMenu),
146 KeyCode::Enter => Some(Action::ApplyFilter),
147 KeyCode::BackTab => Some(Action::PrevFilterFocus),
148 KeyCode::Tab if key.modifiers.contains(KeyModifiers::SHIFT) => {
149 Some(Action::PrevFilterFocus)
150 }
151 KeyCode::Tab => Some(Action::NextFilterFocus),
152 KeyCode::Char(' ') => Some(Action::ToggleFilterCheckbox),
153 KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
154 Some(Action::DeleteWord)
155 }
156 KeyCode::Left if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordLeft),
157 KeyCode::Right if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordRight),
158 KeyCode::Char(c) if c != ' ' => Some(Action::FilterInput(c)),
159 KeyCode::Backspace => Some(Action::FilterBackspace),
160 _ => None,
161 },
162 Mode::InsightsInput => match key.code {
163 KeyCode::Esc => Some(Action::CloseMenu),
164 KeyCode::Enter => Some(Action::Select),
165 KeyCode::Tab => Some(Action::NextFilterFocus),
166 KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
167 Some(Action::Refresh)
168 }
169 KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
170 Some(Action::DeleteWord)
171 }
172 KeyCode::Left if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordLeft),
173 KeyCode::Right if key.modifiers.contains(KeyModifiers::ALT) => Some(Action::WordRight),
174 KeyCode::Down => Some(Action::NextItem),
175 KeyCode::Up => Some(Action::PrevItem),
176 KeyCode::Char(' ') => Some(Action::ToggleFilterCheckbox),
177 KeyCode::Char(c) if c != ' ' => Some(Action::FilterInput(c)),
178 KeyCode::Backspace => Some(Action::FilterBackspace),
179 _ => None,
180 },
181 Mode::ErrorModal => match key.code {
182 KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
183 Some(Action::RetryLoad)
184 }
185 KeyCode::Char('y') => Some(Action::Yank),
186 KeyCode::Char('q') | KeyCode::Esc => Some(Action::Quit),
187 _ => None,
188 },
189 Mode::HelpModal => match key.code {
190 KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') | KeyCode::Char('?') => {
191 Some(Action::CloseMenu)
192 }
193 _ => None,
194 },
195 Mode::QuitConfirm => match key.code {
196 KeyCode::Char('y') | KeyCode::Enter => Some(Action::ConfirmQuit),
197 KeyCode::Char('n') | KeyCode::Esc | KeyCode::Char('q') => Some(Action::CancelQuit),
198 _ => None,
199 },
200 Mode::RegionPicker => match key.code {
201 KeyCode::Esc => Some(Action::ExitFilterMode),
202 KeyCode::Char('i') => Some(Action::EnterFilterMode),
203 KeyCode::Char('j') | KeyCode::Down => Some(Action::NextItem),
204 KeyCode::Char('k') | KeyCode::Up => Some(Action::PrevItem),
205 KeyCode::Enter => Some(Action::Select),
206 KeyCode::Char('l') if key.modifiers.contains(KeyModifiers::CONTROL) => {
207 Some(Action::MeasureLatency)
208 }
209 KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => Some(Action::Refresh),
210 KeyCode::Backspace => Some(Action::FilterBackspace),
211 KeyCode::Char(c) => Some(Action::FilterInput(c)),
212 _ => None,
213 },
214 Mode::CalendarPicker => match key.code {
215 KeyCode::Esc => Some(Action::CloseCalendar),
216 KeyCode::Left => Some(Action::CalendarPrevDay),
217 KeyCode::Down => Some(Action::CalendarNextWeek),
218 KeyCode::Up => Some(Action::CalendarPrevWeek),
219 KeyCode::Right => Some(Action::CalendarNextDay),
220 KeyCode::Char('n') | KeyCode::Tab => Some(Action::CalendarNextMonth),
221 KeyCode::Char('p') | KeyCode::BackTab => Some(Action::CalendarPrevMonth),
222 KeyCode::Enter => Some(Action::CalendarSelect),
223 _ => None,
224 },
225 Mode::TabPicker => match key.code {
226 KeyCode::Esc => Some(Action::CloseMenu),
227 KeyCode::Down => Some(Action::NextItem),
228 KeyCode::Up => Some(Action::PrevItem),
229 KeyCode::Enter => Some(Action::Select),
230 KeyCode::Backspace => Some(Action::FilterBackspace),
231 KeyCode::Char(c) => Some(Action::FilterInput(c)),
232 _ => None,
233 },
234 Mode::SessionPicker => match key.code {
235 KeyCode::Esc => Some(Action::ExitFilterMode),
236 KeyCode::Char('i') => Some(Action::EnterFilterMode),
237 KeyCode::Down => Some(Action::NextItem),
238 KeyCode::Up => Some(Action::PrevItem),
239 KeyCode::Enter => Some(Action::LoadSession),
240 KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => Some(Action::Refresh),
241 KeyCode::Backspace => Some(Action::FilterBackspace),
242 KeyCode::Char(c) => Some(Action::FilterInput(c)),
243 _ => None,
244 },
245 Mode::ProfilePicker => match key.code {
246 KeyCode::Esc => Some(Action::ExitFilterMode),
247 KeyCode::Char('i') => Some(Action::EnterFilterMode),
248 KeyCode::Down => Some(Action::NextItem),
249 KeyCode::Up => Some(Action::PrevItem),
250 KeyCode::Enter => Some(Action::Select),
251 KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => Some(Action::Refresh),
252 KeyCode::Backspace => Some(Action::FilterBackspace),
253 KeyCode::Char(c) => Some(Action::FilterInput(c)),
254 _ => None,
255 },
256 }
257}
258
259#[derive(Debug, Clone, PartialEq)]
260pub enum Action {
261 Quit,
262 ConfirmQuit,
263 CancelQuit,
264 CloseService,
265 NextItem,
266 PrevItem,
267 NextPane,
268 PrevPane,
269 CollapseRow,
270 ExpandRow,
271 Select,
272 OpenSpaceMenu,
273 CloseMenu,
274 OpenServicePicker,
275 OpenCloudWatch,
276 OpenCloudWatchSplit,
277 OpenCloudWatchAlarms,
278 FilterInput(char),
279 FilterBackspace,
280 DeleteWord,
281 WordLeft,
282 WordRight,
283 OpenColumnSelector,
284 ToggleColumn,
285 NextPreferences,
286 PrevPreferences,
287 CloseColumnSelector,
288 StartFilter,
289 StartEventFilter,
290 ApplyFilter,
291 ToggleExactMatch,
292 ToggleShowExpired,
293 GoBack,
294 NextFilterFocus,
295 PrevFilterFocus,
296 ToggleFilterCheckbox,
297 CycleSortColumn,
298 ToggleSortDirection,
299 ScrollUp,
300 ScrollDown,
301 PageUp,
302 PageDown,
303 Refresh,
304 MeasureLatency,
305 RetryLoad,
306 Yank,
307 OpenInConsole,
308 OpenInBrowser,
309 ShowHelp,
310 OpenRegionPicker,
311 OpenCalendar,
312 CloseCalendar,
313 CalendarPrevDay,
314 CalendarNextDay,
315 CalendarPrevWeek,
316 CalendarNextWeek,
317 CalendarPrevMonth,
318 CalendarNextMonth,
319 CalendarSelect,
320 NextTab,
321 PrevTab,
322 NextDetailTab,
323 PrevDetailTab,
324 CloseTab,
325 OpenTabPicker,
326 OpenSessionPicker,
327 OpenProfilePicker,
328 LoadSession,
329 SaveSession,
330 CopyToClipboard,
331 EnterFilterMode,
332 ExitFilterMode,
333 Noop,
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
341 fn test_space_o_opens_service_menu() {
342 let key = KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE);
343 let action = handle_key(key, Mode::SpaceMenu);
344 assert_eq!(action, Some(Action::OpenServicePicker));
345 }
346
347 #[test]
348 fn test_insights_input_accepts_chars() {
349 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
350 let action = handle_key(key, Mode::InsightsInput);
351 assert_eq!(action, Some(Action::FilterInput('a')));
352
353 let key2 = KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE);
354 let action2 = handle_key(key2, Mode::InsightsInput);
355 assert_eq!(action2, Some(Action::FilterInput('1')));
356 }
357
358 #[test]
359 fn test_insights_input_esc_closes() {
360 let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
361 let action = handle_key(key, Mode::InsightsInput);
362 assert_eq!(action, Some(Action::CloseMenu));
363 }
364
365 #[test]
366 fn test_service_menu_accepts_input() {
367 let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE);
368 let action = handle_key(key, Mode::ServicePicker);
369 assert_eq!(action, Some(Action::FilterInput('c')));
370 }
371
372 #[test]
373 fn test_service_menu_navigation() {
374 let key_down = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
375 let action_down = handle_key(key_down, Mode::ServicePicker);
376 assert_eq!(action_down, Some(Action::NextItem));
377
378 let key_up = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
379 let action_up = handle_key(key_up, Mode::ServicePicker);
380 assert_eq!(action_up, Some(Action::PrevItem));
381 }
382
383 #[test]
384 fn test_service_menu_backspace() {
385 let key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
386 let action = handle_key(key, Mode::ServicePicker);
387 assert_eq!(action, Some(Action::FilterBackspace));
388 }
389
390 #[test]
391 fn test_ctrl_shift_n_next_tab() {
392 let key = KeyEvent::new(
393 KeyCode::Char('N'),
394 KeyModifiers::CONTROL | KeyModifiers::SHIFT,
395 );
396 let action = handle_key(key, Mode::Normal);
397 assert_eq!(action, Some(Action::NextTab));
398 }
399
400 #[test]
401 fn test_ctrl_shift_p_prev_tab() {
402 let key = KeyEvent::new(
403 KeyCode::Char('P'),
404 KeyModifiers::CONTROL | KeyModifiers::SHIFT,
405 );
406 let action = handle_key(key, Mode::Normal);
407 assert_eq!(action, Some(Action::PrevTab));
408 }
409
410 #[test]
411 fn test_space_c_close_tab() {
412 let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE);
413 let action = handle_key(key, Mode::SpaceMenu);
414 assert_eq!(action, Some(Action::CloseService));
415 }
416
417 #[test]
418 fn test_space_b_window_picker() {
419 let key = KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE);
420 let action = handle_key(key, Mode::SpaceMenu);
421 assert_eq!(action, Some(Action::OpenTabPicker));
422 }
423
424 #[test]
425 fn test_window_picker_navigation() {
426 let key_down = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
427 let action = handle_key(key_down, Mode::TabPicker);
428 assert_eq!(action, Some(Action::NextItem));
429
430 let key_up = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
431 let action_up = handle_key(key_up, Mode::TabPicker);
432 assert_eq!(action_up, Some(Action::PrevItem));
433 }
434
435 #[test]
436 fn test_window_picker_select() {
437 let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
438 let action = handle_key(key, Mode::TabPicker);
439 assert_eq!(action, Some(Action::Select));
440 }
441
442 #[test]
443 fn test_space_opens_space_menu_in_normal_mode() {
444 let key = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE);
445 let action = handle_key(key, Mode::Normal);
446 assert_eq!(action, Some(Action::OpenSpaceMenu));
447 }
448
449 #[test]
450 fn test_space_menu_o_opens_service_menu() {
451 let key = KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE);
452 let action = handle_key(key, Mode::SpaceMenu);
453 assert_eq!(action, Some(Action::OpenServicePicker));
454 }
455
456 #[test]
457 fn test_ctrl_r_refreshes_profile_picker() {
458 let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL);
459 let action = handle_key(key, Mode::ProfilePicker);
460 assert_eq!(action, Some(Action::Refresh));
461 }
462
463 #[test]
464 fn test_ctrl_r_refreshes_region_picker() {
465 let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL);
466 let action = handle_key(key, Mode::RegionPicker);
467 assert_eq!(action, Some(Action::Refresh));
468 }
469
470 #[test]
471 fn test_ctrl_r_refreshes_session_picker() {
472 let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL);
473 let action = handle_key(key, Mode::SessionPicker);
474 assert_eq!(action, Some(Action::Refresh));
475 }
476
477 #[test]
478 fn test_p_opens_column_selector() {
479 let key = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE);
480 let action = handle_key(key, Mode::Normal);
481 assert_eq!(
482 action,
483 Some(Action::OpenColumnSelector),
484 "p should open column selector (preferences)"
485 );
486 }
487
488 #[test]
489 fn test_ctrl_p_copies_to_clipboard() {
490 let key = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL);
491 let action = handle_key(key, Mode::Normal);
492 assert_eq!(
493 action,
494 Some(Action::CopyToClipboard),
495 "Ctrl+P should copy screen to clipboard (print)"
496 );
497 }
498
499 #[test]
500 fn test_y_yanks_selected_item() {
501 let key = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE);
502 let action = handle_key(key, Mode::Normal);
503 assert_eq!(
504 action,
505 Some(Action::Yank),
506 "y should yank (copy) selected item"
507 );
508 }
509
510 #[test]
511 fn test_space_toggles_checkbox_in_filter_input() {
512 let key = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE);
513 let action = handle_key(key, Mode::FilterInput);
514 assert_eq!(
515 action,
516 Some(Action::ToggleFilterCheckbox),
517 "Space should toggle checkbox in FilterInput mode"
518 );
519 }
520
521 #[test]
522 fn test_space_not_added_to_filter_text() {
523 let key = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE);
525 let action = handle_key(key, Mode::FilterInput);
526 assert_ne!(
527 action,
528 Some(Action::FilterInput(' ')),
529 "Space should not be added to filter text"
530 );
531 }
532
533 #[test]
534 fn test_q_in_normal_mode_shows_quit_confirmation() {
535 let key = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE);
536 let action = handle_key(key, Mode::Normal);
537 assert_eq!(
538 action,
539 Some(Action::Quit),
540 "q in Normal mode must trigger Quit (shows confirmation dialog)"
541 );
542 }
543
544 #[test]
545 fn test_ctrl_c_quits_immediately_without_confirmation() {
546 let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
547 let action = handle_key(key, Mode::Normal);
548 assert_eq!(
549 action,
550 Some(Action::ConfirmQuit),
551 "Ctrl+C must quit immediately (ConfirmQuit, not Quit)"
552 );
553 }
554
555 #[test]
556 fn test_q_in_service_picker_shows_quit_confirmation() {
557 let key = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE);
561 let action = handle_key(key, Mode::ServicePicker);
562 assert_eq!(
563 action,
564 Some(Action::FilterInput('q')),
565 "q in ServicePicker must dispatch FilterInput so it can type 'sqs' in filter"
566 );
567 }
568
569 #[test]
570 fn test_q_in_service_picker_not_filtering_triggers_quit() {
571 use crate::app::App;
573 let mut app = App::new_without_client("default".to_string(), None);
574 app.mode = Mode::ServicePicker;
575 app.service_picker.filter_active = false; app.handle_action(Action::FilterInput('q'));
578
579 assert_eq!(
580 app.mode,
581 Mode::QuitConfirm,
582 "q when not filtering in ServicePicker must show quit confirmation"
583 );
584 }
585
586 #[test]
587 fn test_q_in_service_picker_while_filtering_types_q() {
588 use crate::app::App;
590 let mut app = App::new_without_client("default".to_string(), None);
591 app.mode = Mode::ServicePicker;
592 app.service_picker.filter_active = true; app.handle_action(Action::FilterInput('q'));
595
596 assert_eq!(
597 app.mode,
598 Mode::ServicePicker,
599 "q while filtering must NOT quit"
600 );
601 assert_eq!(
602 app.service_picker.filter, "q",
603 "q while filtering must type 'q' into filter"
604 );
605 }
606
607 #[test]
608 fn test_i_in_service_picker_dispatches_filter_input() {
609 let key = KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE);
610 let action = handle_key(key, Mode::ServicePicker);
611 assert_eq!(
612 action,
613 Some(Action::FilterInput('i')),
614 "i in ServicePicker must dispatch FilterInput('i')"
615 );
616 }
617
618 #[test]
619 fn test_i_in_service_picker_not_filtering_activates_filter() {
620 use crate::app::App;
621 let mut app = App::new_without_client("default".to_string(), None);
622 app.mode = Mode::ServicePicker;
623 app.service_picker.filter_active = false;
624
625 app.handle_action(Action::FilterInput('i'));
626
627 assert!(app.service_picker.filter_active);
628 assert_eq!(
629 app.service_picker.filter, "",
630 "i activates filter but must not type itself"
631 );
632 }
633
634 #[test]
635 fn test_i_in_service_picker_while_filtering_types_i() {
636 use crate::app::App;
637 let mut app = App::new_without_client("default".to_string(), None);
638 app.mode = Mode::ServicePicker;
639 app.service_picker.filter_active = true;
640 app.service_picker.filter = "sqs".to_string();
641
642 app.handle_action(Action::FilterInput('i'));
643
644 assert_eq!(
645 app.service_picker.filter, "sqsi",
646 "i while filtering must type 'i' into filter"
647 );
648 assert_eq!(
649 app.mode,
650 Mode::ServicePicker,
651 "mode must stay ServicePicker"
652 );
653 }
654
655 #[test]
656 fn test_service_filter_types_all_chars_in_sqs() {
657 use crate::app::App;
658 let mut app = App::new_without_client("default".to_string(), None);
659 app.mode = Mode::ServicePicker;
660 app.service_picker.filter_active = true;
661
662 for c in ['s', 'q', 's'] {
663 app.handle_action(Action::FilterInput(c));
664 }
665
666 assert_eq!(app.service_picker.filter, "sqs");
667 assert_eq!(app.mode, Mode::ServicePicker);
668 }
669
670 #[test]
671 fn test_any_char_auto_activates_filter_in_service_picker() {
672 use crate::app::App;
674 let mut app = App::new_without_client("default".to_string(), None);
675 app.mode = Mode::ServicePicker;
676 app.service_picker.filter_active = false;
677
678 app.handle_action(Action::FilterInput('s'));
679
680 assert!(app.service_picker.filter_active);
681 assert_eq!(app.service_picker.filter, "s");
682 }
683
684 #[test]
685 fn test_quit_confirm_mode_y_confirms() {
686 let key = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE);
687 let action = handle_key(key, Mode::QuitConfirm);
688 assert_eq!(action, Some(Action::ConfirmQuit));
689 }
690
691 #[test]
692 fn test_quit_confirm_mode_enter_confirms() {
693 let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
694 let action = handle_key(key, Mode::QuitConfirm);
695 assert_eq!(action, Some(Action::ConfirmQuit));
696 }
697
698 #[test]
699 fn test_quit_confirm_mode_n_cancels() {
700 let key = KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE);
701 let action = handle_key(key, Mode::QuitConfirm);
702 assert_eq!(action, Some(Action::CancelQuit));
703 }
704
705 #[test]
706 fn test_quit_confirm_mode_esc_cancels() {
707 let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
708 let action = handle_key(key, Mode::QuitConfirm);
709 assert_eq!(action, Some(Action::CancelQuit));
710 }
711
712 #[test]
713 fn test_quit_shows_confirmation_dialog_not_immediate_quit() {
714 use crate::app::{App, Service};
715 let mut app = App::new_without_client("default".to_string(), None);
716 app.current_service = Service::CloudWatchLogGroups;
717 app.service_selected = true;
718 app.mode = Mode::Normal;
719
720 app.handle_action(Action::Quit);
721
722 assert_eq!(
723 app.mode,
724 Mode::QuitConfirm,
725 "q must show confirmation dialog"
726 );
727 assert!(
728 app.running,
729 "app must still be running after q (before confirmation)"
730 );
731 }
732
733 #[test]
734 fn test_confirm_quit_actually_quits() {
735 use crate::app::App;
736 let mut app = App::new_without_client("default".to_string(), None);
737 app.mode = Mode::QuitConfirm;
738
739 app.handle_action(Action::ConfirmQuit);
740
741 assert!(!app.running, "ConfirmQuit must set running=false");
742 }
743
744 #[test]
745 fn test_cancel_quit_returns_to_normal() {
746 use crate::app::App;
747 let mut app = App::new_without_client("default".to_string(), None);
748 app.mode = Mode::QuitConfirm;
749
750 app.handle_action(Action::CancelQuit);
751
752 assert_eq!(
753 app.mode,
754 Mode::Normal,
755 "CancelQuit must return to Normal mode"
756 );
757 assert!(app.running, "app must still be running after cancel");
758 }
759}