1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5use std::collections::HashMap;
6
7use crate::buffer::AppMode;
8use crate::ui::input::actions::{Action, ActionContext, CursorPosition, NavigateAction};
9
10pub struct KeyMapper {
12 global_mappings: HashMap<(KeyCode, KeyModifiers), Action>,
14
15 mode_mappings: HashMap<AppMode, HashMap<(KeyCode, KeyModifiers), Action>>,
17
18 count_buffer: String,
20
21 vim_command_buffer: String,
23}
24
25impl KeyMapper {
26 pub fn new() -> Self {
27 let mut mapper = Self {
28 global_mappings: HashMap::new(),
29 mode_mappings: HashMap::new(),
30 count_buffer: String::new(),
31 vim_command_buffer: String::new(),
32 };
33
34 mapper.init_global_mappings();
35 mapper.init_mode_mappings();
36 mapper
37 }
38
39 fn init_global_mappings(&mut self) {
41 use KeyCode::*;
42 use KeyModifiers as Mod;
43
44 self.global_mappings
46 .insert((F(1), Mod::NONE), Action::ShowHelp);
47 self.global_mappings
48 .insert((F(3), Mod::NONE), Action::ShowPrettyQuery);
49 self.global_mappings
50 .insert((F(5), Mod::NONE), Action::ShowDebugInfo);
51 self.global_mappings
52 .insert((F(6), Mod::NONE), Action::ToggleRowNumbers);
53 self.global_mappings
54 .insert((F(7), Mod::NONE), Action::ToggleCompactMode);
55 self.global_mappings
56 .insert((F(8), Mod::NONE), Action::ToggleCaseInsensitive);
57 self.global_mappings
58 .insert((F(9), Mod::NONE), Action::KillLine);
59 self.global_mappings
60 .insert((F(10), Mod::NONE), Action::KillLineBackward);
61 self.global_mappings
62 .insert((F(12), Mod::NONE), Action::ToggleKeyIndicator);
63
64 self.global_mappings
66 .insert((Char('c'), Mod::CONTROL), Action::ForceQuit);
67 self.global_mappings
68 .insert((Char('C'), Mod::CONTROL), Action::ForceQuit);
69 }
70
71 fn init_mode_mappings(&mut self) {
73 self.init_results_mappings();
74 self.init_command_mappings();
75 }
77
78 fn init_results_mappings(&mut self) {
80 use crate::buffer::AppMode;
81 use KeyCode::*;
82 use KeyModifiers as Mod;
83
84 let mut mappings = HashMap::new();
85
86 mappings.insert((Up, Mod::NONE), Action::Navigate(NavigateAction::Up(1)));
88 mappings.insert((Down, Mod::NONE), Action::Navigate(NavigateAction::Down(1)));
89 mappings.insert((Left, Mod::NONE), Action::Navigate(NavigateAction::Left(1)));
90 mappings.insert(
91 (Right, Mod::NONE),
92 Action::Navigate(NavigateAction::Right(1)),
93 );
94
95 mappings.insert(
96 (PageUp, Mod::NONE),
97 Action::Navigate(NavigateAction::PageUp),
98 );
99 mappings.insert(
100 (PageDown, Mod::NONE),
101 Action::Navigate(NavigateAction::PageDown),
102 );
103
104 mappings.insert(
106 (Char('f'), Mod::CONTROL),
107 Action::Navigate(NavigateAction::PageDown),
108 );
109 mappings.insert(
110 (Char('b'), Mod::CONTROL),
111 Action::Navigate(NavigateAction::PageUp),
112 );
113
114 mappings.insert((Home, Mod::NONE), Action::Navigate(NavigateAction::Home));
115 mappings.insert((End, Mod::NONE), Action::Navigate(NavigateAction::End));
116
117 mappings.insert(
119 (Char('h'), Mod::NONE),
120 Action::Navigate(NavigateAction::Left(1)),
121 );
122 mappings.insert(
123 (Char('j'), Mod::NONE),
124 Action::Navigate(NavigateAction::Down(1)),
125 );
126 mappings.insert(
127 (Char('k'), Mod::NONE),
128 Action::Navigate(NavigateAction::Up(1)),
129 );
130 mappings.insert(
131 (Char('l'), Mod::NONE),
132 Action::Navigate(NavigateAction::Right(1)),
133 );
134
135 mappings.insert((Left, Mod::NONE), Action::Navigate(NavigateAction::Left(1)));
137 mappings.insert(
138 (Right, Mod::NONE),
139 Action::Navigate(NavigateAction::Right(1)),
140 );
141 mappings.insert((Down, Mod::NONE), Action::Navigate(NavigateAction::Down(1)));
142 mappings.insert((Up, Mod::NONE), Action::Navigate(NavigateAction::Up(1))); mappings.insert(
146 (PageUp, Mod::NONE),
147 Action::Navigate(NavigateAction::PageUp),
148 );
149 mappings.insert(
150 (PageDown, Mod::NONE),
151 Action::Navigate(NavigateAction::PageDown),
152 );
153
154 mappings.insert(
158 (Char('G'), Mod::SHIFT),
159 Action::Navigate(NavigateAction::End),
160 );
161
162 mappings.insert(
164 (Char('0'), Mod::NONE),
165 Action::Navigate(NavigateAction::FirstColumn),
166 );
167 mappings.insert(
168 (Char('^'), Mod::NONE),
169 Action::Navigate(NavigateAction::FirstColumn),
170 );
171 mappings.insert(
172 (Char('$'), Mod::NONE),
173 Action::Navigate(NavigateAction::LastColumn),
174 );
175
176 mappings.insert((Char('H'), Mod::SHIFT), Action::NavigateToViewportTop);
178 mappings.insert((Char('M'), Mod::SHIFT), Action::NavigateToViewportMiddle);
179 mappings.insert((Char('L'), Mod::SHIFT), Action::NavigateToViewportBottom);
180
181 mappings.insert((Esc, Mod::NONE), Action::ExitCurrentMode);
183 mappings.insert((Char('q'), Mod::NONE), Action::Quit);
184 mappings.insert((Char('c'), Mod::CONTROL), Action::Quit); mappings.insert((F(2), Mod::NONE), Action::SwitchMode(AppMode::Command));
188
189 mappings.insert(
191 (Char('i'), Mod::NONE),
192 Action::SwitchModeWithCursor(AppMode::Command, CursorPosition::Current),
193 );
194
195 mappings.insert(
197 (Char('a'), Mod::NONE),
198 Action::SwitchModeWithCursor(AppMode::Command, CursorPosition::End),
199 );
200
201 mappings.insert((Char('p'), Mod::NONE), Action::ToggleColumnPin);
203 mappings.insert((Char('-'), Mod::NONE), Action::HideColumn); mappings.insert(
205 (Char('H'), Mod::CONTROL | Mod::SHIFT),
206 Action::UnhideAllColumns,
207 );
208 mappings.insert((Char('+'), Mod::NONE), Action::UnhideAllColumns); mappings.insert((Char('='), Mod::NONE), Action::UnhideAllColumns); mappings.insert((Char('e'), Mod::NONE), Action::HideEmptyColumns);
212 mappings.insert((Char('E'), Mod::SHIFT), Action::HideEmptyColumns);
213 mappings.insert((Left, Mod::SHIFT), Action::MoveColumnLeft);
214 mappings.insert((Right, Mod::SHIFT), Action::MoveColumnRight);
215 mappings.insert((Char('<'), Mod::NONE), Action::MoveColumnLeft);
217 mappings.insert((Char('>'), Mod::NONE), Action::MoveColumnRight);
218 mappings.insert((Char('/'), Mod::NONE), Action::StartSearch);
220 mappings.insert((Char('\\'), Mod::NONE), Action::StartColumnSearch);
221 mappings.insert((Char('f'), Mod::NONE), Action::StartFilter);
222 mappings.insert((Char('F'), Mod::SHIFT), Action::StartFuzzyFilter);
223
224 mappings.insert((Char('s'), Mod::NONE), Action::Sort(None));
226
227 mappings.insert((Char('N'), Mod::NONE), Action::ToggleRowNumbers);
229 mappings.insert((Char('C'), Mod::NONE), Action::ToggleCompactMode);
230
231 mappings.insert((Char('x'), Mod::CONTROL), Action::ExportToCsv);
233 mappings.insert((Char('j'), Mod::CONTROL), Action::ExportToJson);
234
235 mappings.insert((Char('C'), Mod::SHIFT), Action::ClearFilter);
237
238 mappings.insert((Char(':'), Mod::NONE), Action::StartJumpToRow);
240
241 mappings.insert((Char('n'), Mod::NONE), Action::NextSearchMatch);
243 mappings.insert((Char('N'), Mod::SHIFT), Action::PreviousSearchMatch);
244
245 mappings.insert((Char('v'), Mod::NONE), Action::ToggleSelectionMode);
247
248 mappings.insert((Char('S'), Mod::SHIFT), Action::ShowColumnStatistics);
250
251 mappings.insert((Char('s'), Mod::ALT), Action::CycleColumnPacking);
253
254 mappings.insert((Char(' '), Mod::NONE), Action::ToggleViewportLock);
256 mappings.insert((Char('x'), Mod::NONE), Action::ToggleCursorLock);
257 mappings.insert((Char('X'), Mod::SHIFT), Action::ToggleCursorLock);
258 mappings.insert((Char(' '), Mod::CONTROL), Action::ToggleViewportLock);
259
260 mappings.insert((Char('?'), Mod::NONE), Action::ShowHelp); mappings.insert((Char('P'), Mod::SHIFT), Action::ClearAllPins);
266
267 mappings.insert((Char('r'), Mod::CONTROL), Action::StartHistorySearch);
269
270 self.mode_mappings.insert(AppMode::Results, mappings);
271 }
272
273 fn init_command_mappings(&mut self) {
275 use crate::buffer::AppMode;
276 use KeyCode::*;
277 use KeyModifiers as Mod;
278
279 let mut mappings = HashMap::new();
280
281 mappings.insert((Enter, Mod::NONE), Action::ExecuteQuery);
283
284 mappings.insert((F(2), Mod::NONE), Action::SwitchMode(AppMode::Results));
286
287 mappings.insert((Char('u'), Mod::CONTROL), Action::ClearLine);
289
290 mappings.insert((Char('z'), Mod::CONTROL), Action::Undo);
292 mappings.insert((Char('y'), Mod::CONTROL), Action::Redo);
293
294 mappings.insert((Left, Mod::NONE), Action::MoveCursorLeft);
296 mappings.insert((Right, Mod::NONE), Action::MoveCursorRight);
297 mappings.insert((Down, Mod::NONE), Action::SwitchMode(AppMode::Results)); mappings.insert((Home, Mod::NONE), Action::MoveCursorHome);
299 mappings.insert((End, Mod::NONE), Action::MoveCursorEnd);
300 mappings.insert((Char('a'), Mod::CONTROL), Action::MoveCursorHome);
301 mappings.insert((Char('e'), Mod::CONTROL), Action::MoveCursorEnd);
302 mappings.insert((Left, Mod::CONTROL), Action::MoveCursorWordLeft);
303 mappings.insert((Right, Mod::CONTROL), Action::MoveCursorWordRight);
304 mappings.insert((Char('b'), Mod::ALT), Action::MoveCursorWordLeft);
305 mappings.insert((Char('f'), Mod::ALT), Action::MoveCursorWordRight);
306
307 mappings.insert((Backspace, Mod::NONE), Action::Backspace);
309 mappings.insert((Delete, Mod::NONE), Action::Delete);
310 mappings.insert((Char('w'), Mod::CONTROL), Action::DeleteWordBackward);
311 mappings.insert((Char('d'), Mod::ALT), Action::DeleteWordForward);
312 mappings.insert((Char('k'), Mod::CONTROL), Action::KillLine);
313 mappings.insert((Char('v'), Mod::CONTROL), Action::Paste);
317
318 mappings.insert((Char('p'), Mod::CONTROL), Action::PreviousHistoryCommand);
320 mappings.insert((Char('n'), Mod::CONTROL), Action::NextHistoryCommand);
321 mappings.insert((Up, Mod::ALT), Action::PreviousHistoryCommand);
322 mappings.insert((Down, Mod::ALT), Action::NextHistoryCommand);
323
324 mappings.insert((Char('*'), Mod::CONTROL), Action::ExpandAsterisk);
326 mappings.insert((Char('*'), Mod::ALT), Action::ExpandAsteriskVisible);
327
328 self.mode_mappings.insert(AppMode::Command, mappings);
331 }
332
333 pub fn map_key(&mut self, key: KeyEvent, context: &ActionContext) -> Option<Action> {
335 if context.mode == AppMode::Results {
337 if let KeyCode::Char(c) = key.code {
338 if key.modifiers.is_empty() {
339 if !self.vim_command_buffer.is_empty() {
341 let command = format!("{}{}", self.vim_command_buffer, c);
343 let action = match command.as_str() {
344 "gg" => {
345 self.vim_command_buffer.clear();
347 Some(Action::Navigate(NavigateAction::Home))
348 }
349 _ => {
350 self.vim_command_buffer.clear();
352 None
353 }
354 };
355
356 if action.is_some() {
357 return action;
358 }
359 }
360
361 if c.is_ascii_digit() {
363 self.count_buffer.push(c);
364 return None; }
366
367 if c == 'g' {
371 let key_combo = (key.code, key.modifiers);
372 if let Some(mode_mappings) = self.mode_mappings.get(&context.mode) {
373 if mode_mappings.contains_key(&key_combo) {
374 tracing::debug!(
377 "Key '{}' has standalone mapping, not treating as vim command",
378 c
379 );
380 } else {
381 self.vim_command_buffer.push(c);
383 tracing::debug!("Starting vim command buffer with '{}'", c);
384 return None; }
386 }
387 }
388 }
389 }
390 }
391
392 let action = self.map_key_internal(key, context);
394
395 if !self.count_buffer.is_empty() {
397 if let Some(mut action) = action {
398 if let Ok(count) = self.count_buffer.parse::<usize>() {
399 action = self.apply_count_to_action(action, count);
400 }
401 self.count_buffer.clear();
402 return Some(action);
403 }
404 self.count_buffer.clear();
406 }
407
408 action
409 }
410
411 fn map_key_internal(&self, key: KeyEvent, context: &ActionContext) -> Option<Action> {
413 let key_combo = (key.code, key.modifiers);
414
415 if let Some(action) = self.global_mappings.get(&key_combo) {
417 return Some(action.clone());
418 }
419
420 if let Some(mode_mappings) = self.mode_mappings.get(&context.mode) {
422 if let Some(action) = mode_mappings.get(&key_combo) {
423 return Some(action.clone());
424 }
425 }
426
427 if context.mode == AppMode::Command {
429 if let KeyCode::Char(c) = key.code {
430 if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT {
431 return Some(Action::InsertChar(c));
433 }
434 }
435 }
436
437 None
439 }
440
441 fn apply_count_to_action(&self, action: Action, count: usize) -> Action {
443 match action {
444 Action::Navigate(NavigateAction::Up(_)) => Action::Navigate(NavigateAction::Up(count)),
445 Action::Navigate(NavigateAction::Down(_)) => {
446 Action::Navigate(NavigateAction::Down(count))
447 }
448 Action::Navigate(NavigateAction::Left(_)) => {
449 Action::Navigate(NavigateAction::Left(count))
450 }
451 Action::Navigate(NavigateAction::Right(_)) => {
452 Action::Navigate(NavigateAction::Right(count))
453 }
454 _ => action,
456 }
457 }
458
459 pub fn clear_pending(&mut self) {
461 self.count_buffer.clear();
462 self.vim_command_buffer.clear();
463 }
464
465 pub fn is_collecting_count(&self) -> bool {
467 !self.count_buffer.is_empty()
468 }
469
470 pub fn get_count_buffer(&self) -> &str {
472 &self.count_buffer
473 }
474}
475
476impl Default for KeyMapper {
477 fn default() -> Self {
478 Self::new()
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485 use crate::app_state_container::SelectionMode;
486
487 #[test]
488 fn test_basic_navigation_mapping() {
489 let mut mapper = KeyMapper::new();
490 let context = ActionContext {
491 mode: AppMode::Results,
492 selection_mode: SelectionMode::Row,
493 has_results: true,
494 has_filter: false,
495 has_search: false,
496 row_count: 100,
497 column_count: 10,
498 current_row: 0,
499 current_column: 0,
500 };
501
502 let key = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
504 let action = mapper.map_key(key, &context);
505 assert_eq!(action, Some(Action::Navigate(NavigateAction::Down(1))));
506
507 let key = KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE);
509 let action = mapper.map_key(key, &context);
510 assert_eq!(action, Some(Action::Navigate(NavigateAction::Down(1))));
511 }
512
513 #[test]
514 fn test_vim_count_motion() {
515 let mut mapper = KeyMapper::new();
516 let context = ActionContext {
517 mode: AppMode::Results,
518 selection_mode: SelectionMode::Row,
519 has_results: true,
520 has_filter: false,
521 has_search: false,
522 row_count: 100,
523 column_count: 10,
524 current_row: 0,
525 current_column: 0,
526 };
527
528 let key = KeyEvent::new(KeyCode::Char('5'), KeyModifiers::NONE);
530 let action = mapper.map_key(key, &context);
531 assert_eq!(action, None); assert_eq!(mapper.get_count_buffer(), "5");
533
534 let key = KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE);
536 let action = mapper.map_key(key, &context);
537 assert_eq!(action, Some(Action::Navigate(NavigateAction::Down(5))));
538 assert_eq!(mapper.get_count_buffer(), ""); }
540
541 #[test]
542 fn test_global_mapping_override() {
543 let mut mapper = KeyMapper::new();
544 let context = ActionContext {
545 mode: AppMode::Results,
546 selection_mode: SelectionMode::Row,
547 has_results: true,
548 has_filter: false,
549 has_search: false,
550 row_count: 100,
551 column_count: 10,
552 current_row: 0,
553 current_column: 0,
554 };
555
556 let key = KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE);
558 let action = mapper.map_key(key, &context);
559 assert_eq!(action, Some(Action::ShowHelp));
560 }
561
562 #[test]
563 fn test_command_mode_editing_actions() {
564 let mut mapper = KeyMapper::new();
565 let context = ActionContext {
566 mode: AppMode::Command,
567 selection_mode: SelectionMode::Row,
568 has_results: false,
569 has_filter: false,
570 has_search: false,
571 row_count: 0,
572 column_count: 0,
573 current_row: 0,
574 current_column: 0,
575 };
576
577 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
579 let action = mapper.map_key(key, &context);
580 assert_eq!(action, Some(Action::InsertChar('a')));
581
582 let key = KeyEvent::new(KeyCode::Char('A'), KeyModifiers::SHIFT);
584 let action = mapper.map_key(key, &context);
585 assert_eq!(action, Some(Action::InsertChar('A')));
586
587 let key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
589 let action = mapper.map_key(key, &context);
590 assert_eq!(action, Some(Action::Backspace));
591
592 let key = KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE);
594 let action = mapper.map_key(key, &context);
595 assert_eq!(action, Some(Action::Delete));
596
597 let key = KeyEvent::new(KeyCode::Left, KeyModifiers::NONE);
599 let action = mapper.map_key(key, &context);
600 assert_eq!(action, Some(Action::MoveCursorLeft));
601
602 let key = KeyEvent::new(KeyCode::Right, KeyModifiers::NONE);
604 let action = mapper.map_key(key, &context);
605 assert_eq!(action, Some(Action::MoveCursorRight));
606
607 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
609 let action = mapper.map_key(key, &context);
610 assert_eq!(action, Some(Action::MoveCursorHome));
611
612 let key = KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL);
614 let action = mapper.map_key(key, &context);
615 assert_eq!(action, Some(Action::MoveCursorEnd));
616
617 let key = KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL);
619 let action = mapper.map_key(key, &context);
620 assert_eq!(action, Some(Action::ClearLine));
621
622 let key = KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL);
624 let action = mapper.map_key(key, &context);
625 assert_eq!(action, Some(Action::DeleteWordBackward));
626
627 let key = KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL);
629 let action = mapper.map_key(key, &context);
630 assert_eq!(action, Some(Action::Undo));
631
632 let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
634 let action = mapper.map_key(key, &context);
635 assert_eq!(action, Some(Action::ExecuteQuery));
636 }
637
638 #[test]
639 fn test_vim_style_append_modes() {
640 let mut mapper = KeyMapper::new();
641 let context = ActionContext {
642 mode: AppMode::Results,
643 selection_mode: SelectionMode::Row,
644 has_results: true,
645 has_filter: false,
646 has_search: false,
647 row_count: 100,
648 column_count: 10,
649 current_row: 0,
650 current_column: 0,
651 };
652
653 let key = KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE);
655 let action = mapper.map_key(key, &context);
656 assert_eq!(
657 action,
658 Some(Action::SwitchModeWithCursor(
659 AppMode::Command,
660 CursorPosition::Current
661 ))
662 );
663
664 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
666 let action = mapper.map_key(key, &context);
667 assert_eq!(
668 action,
669 Some(Action::SwitchModeWithCursor(
670 AppMode::Command,
671 CursorPosition::End
672 ))
673 );
674
675 }
679
680 #[test]
681 fn test_sort_key_mapping() {
682 let mut mapper = KeyMapper::new();
683 let context = ActionContext {
684 mode: AppMode::Results,
685 selection_mode: SelectionMode::Row,
686 has_results: true,
687 has_filter: false,
688 has_search: false,
689 row_count: 100,
690 column_count: 10,
691 current_row: 0,
692 current_column: 0,
693 };
694
695 let key = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE);
697 let action = mapper.map_key(key, &context);
698 assert_eq!(action, Some(Action::Sort(None)));
699 }
700
701 #[test]
702 fn test_vim_go_to_top() {
703 let mut mapper = KeyMapper::new();
704 let context = ActionContext {
705 mode: AppMode::Results,
706 selection_mode: SelectionMode::Row,
707 has_results: true,
708 has_filter: false,
709 has_search: false,
710 row_count: 100,
711 column_count: 10,
712 current_row: 0,
713 current_column: 0,
714 };
715
716 let key_g1 = KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE);
718 let action_g1 = mapper.map_key(key_g1, &context);
719 assert_eq!(action_g1, None); let key_g2 = KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE);
722 let action_gg = mapper.map_key(key_g2, &context);
723 assert_eq!(action_gg, Some(Action::Navigate(NavigateAction::Home)));
724 }
725
726 #[test]
727 fn test_bug_reproduction_s_key_not_found() {
728 let mut mapper = KeyMapper::new();
730 let context = ActionContext {
731 mode: AppMode::Results,
732 selection_mode: SelectionMode::Row,
733 has_results: true,
734 has_filter: false,
735 has_search: false,
736 row_count: 100,
737 column_count: 10,
738 current_row: 0,
739 current_column: 0,
740 };
741
742 let key = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE);
745 let action = mapper.map_key(key, &context);
746
747 assert!(
749 action.is_some(),
750 "Bug reproduction: 's' key should map to an action, not return None"
751 );
752 assert_eq!(
753 action,
754 Some(Action::Sort(None)),
755 "Bug reproduction: 's' key should map to Sort action"
756 );
757 }
758}