1use std::collections::HashMap;
10
11use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum KeyAction {
17 NavigateUp,
19 NavigateDown,
21 PageUp,
23 PageDown,
25 GoToTop,
27 GoToBottom,
29 FocusNextPane,
31 FocusPrevPane,
33 Switch,
35 Filter,
37 ClearFilter,
39 New,
41 Remove,
43 PrCheckout,
45 Checkout,
47 Sync,
49 OpenEditor,
51 Refresh,
53 SortCycle,
55 SortReverse,
57 Help,
59 Quit,
61 ToggleSidebar,
63 ResizeSidebarGrow,
65 ResizeSidebarShrink,
67}
68
69impl KeyAction {
70 pub const ALL: [KeyAction; 25] = [
72 KeyAction::NavigateUp,
73 KeyAction::NavigateDown,
74 KeyAction::PageUp,
75 KeyAction::PageDown,
76 KeyAction::GoToTop,
77 KeyAction::GoToBottom,
78 KeyAction::FocusNextPane,
79 KeyAction::FocusPrevPane,
80 KeyAction::Switch,
81 KeyAction::Filter,
82 KeyAction::ClearFilter,
83 KeyAction::New,
84 KeyAction::Remove,
85 KeyAction::PrCheckout,
86 KeyAction::Checkout,
87 KeyAction::Sync,
88 KeyAction::OpenEditor,
89 KeyAction::Refresh,
90 KeyAction::SortCycle,
91 KeyAction::SortReverse,
92 KeyAction::Help,
93 KeyAction::Quit,
94 KeyAction::ToggleSidebar,
95 KeyAction::ResizeSidebarGrow,
96 KeyAction::ResizeSidebarShrink,
97 ];
98
99 pub fn name(self) -> &'static str {
101 match self {
102 KeyAction::NavigateUp => "navigate-up",
103 KeyAction::NavigateDown => "navigate-down",
104 KeyAction::PageUp => "page-up",
105 KeyAction::PageDown => "page-down",
106 KeyAction::GoToTop => "go-to-top",
107 KeyAction::GoToBottom => "go-to-bottom",
108 KeyAction::FocusNextPane => "focus-next-pane",
109 KeyAction::FocusPrevPane => "focus-prev-pane",
110 KeyAction::Switch => "switch",
111 KeyAction::Filter => "filter",
112 KeyAction::ClearFilter => "clear-filter",
113 KeyAction::New => "new",
114 KeyAction::Remove => "remove",
115 KeyAction::PrCheckout => "pr-checkout",
116 KeyAction::Checkout => "checkout",
117 KeyAction::Sync => "sync",
118 KeyAction::OpenEditor => "open-editor",
119 KeyAction::Refresh => "refresh",
120 KeyAction::SortCycle => "sort-cycle",
121 KeyAction::SortReverse => "sort-reverse",
122 KeyAction::Help => "help",
123 KeyAction::Quit => "quit",
124 KeyAction::ToggleSidebar => "toggle-sidebar",
125 KeyAction::ResizeSidebarGrow => "resize-sidebar-grow",
126 KeyAction::ResizeSidebarShrink => "resize-sidebar-shrink",
127 }
128 }
129
130 pub fn parse(name: &str) -> Option<KeyAction> {
132 KeyAction::ALL.into_iter().find(|a| a.name() == name)
133 }
134
135 pub fn label(self) -> &'static str {
141 match self {
142 KeyAction::NavigateUp => "navigate up",
143 KeyAction::NavigateDown => "navigate down",
144 KeyAction::PageUp => "page up",
145 KeyAction::PageDown => "page down",
146 KeyAction::GoToTop => "go to top",
147 KeyAction::GoToBottom => "go to bottom",
148 KeyAction::FocusNextPane => "next pane",
149 KeyAction::FocusPrevPane => "prev pane",
150 KeyAction::Switch => "switch",
151 KeyAction::Filter => "filter",
152 KeyAction::ClearFilter => "clear / back",
153 KeyAction::New => "new",
154 KeyAction::Remove => "remove",
155 KeyAction::PrCheckout => "pr picker",
156 KeyAction::Checkout => "checkout",
157 KeyAction::Sync => "sync",
158 KeyAction::OpenEditor => "open in editor",
159 KeyAction::Refresh => "refresh",
160 KeyAction::SortCycle => "sort cycle",
161 KeyAction::SortReverse => "sort reverse",
162 KeyAction::Help => "help",
163 KeyAction::Quit => "quit",
164 KeyAction::ToggleSidebar => "toggle sidebar",
165 KeyAction::ResizeSidebarGrow => "grow sidebar",
166 KeyAction::ResizeSidebarShrink => "shrink sidebar",
167 }
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
173pub struct KeyChord {
174 pub code: KeyCode,
176 pub mods: KeyModifiers,
178}
179
180impl KeyChord {
181 pub fn key(code: KeyCode) -> KeyChord {
183 KeyChord {
184 code,
185 mods: KeyModifiers::empty(),
186 }
187 }
188
189 pub fn ctrl(c: char) -> KeyChord {
191 KeyChord {
192 code: KeyCode::Char(c),
193 mods: KeyModifiers::CONTROL,
194 }
195 }
196
197 pub fn normalized(code: KeyCode, mods: KeyModifiers) -> KeyChord {
199 let mut code = code;
200 let mut mods = mods;
201 if code == KeyCode::BackTab {
203 code = KeyCode::Tab;
204 mods |= KeyModifiers::SHIFT;
205 }
206 if matches!(code, KeyCode::Char(_)) {
208 mods.remove(KeyModifiers::SHIFT);
209 }
210 mods &= KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT;
212 KeyChord { code, mods }
213 }
214
215 pub fn from_event(ev: KeyEvent) -> KeyChord {
217 KeyChord::normalized(ev.code, ev.modifiers)
218 }
219
220 pub fn parse(s: &str) -> Option<KeyChord> {
223 let s = s.trim();
224 if s.is_empty() {
225 return None;
226 }
227 if s == "+" {
229 return Some(KeyChord::key(KeyCode::Char('+')));
230 }
231 if s == "-" {
232 return Some(KeyChord::key(KeyCode::Char('-')));
233 }
234 let parts: Vec<&str> = s.split('+').collect();
235 let (key_tok, mod_toks) = parts.split_last()?;
236 if key_tok.is_empty() {
237 return None;
238 }
239 let mut mods = KeyModifiers::empty();
240 for m in mod_toks {
241 mods |= parse_modifier(m)?;
242 }
243 let code = parse_keycode(key_tok)?;
244 Some(KeyChord::normalized(code, mods))
245 }
246
247 pub fn render(&self) -> String {
249 let mut s = String::new();
250 if self.mods.contains(KeyModifiers::CONTROL) {
251 s.push_str("ctrl+");
252 }
253 if self.mods.contains(KeyModifiers::ALT) {
254 s.push_str("alt+");
255 }
256 if self.mods.contains(KeyModifiers::SHIFT) {
257 s.push_str("shift+");
258 }
259 s.push_str(&keycode_name(self.code));
260 s
261 }
262
263 pub fn display(&self) -> String {
268 let ctrl = self.mods.contains(KeyModifiers::CONTROL);
269 let mut s = String::new();
270 if ctrl {
271 s.push_str("Ctrl-");
272 }
273 if self.mods.contains(KeyModifiers::ALT) {
274 s.push_str("Alt-");
275 }
276 if self.mods.contains(KeyModifiers::SHIFT) {
277 s.push_str("Shift+");
278 }
279 match self.code {
281 KeyCode::Char(c) if ctrl => s.push(c.to_ascii_uppercase()),
282 code => s.push_str(&keycode_display(code)),
283 }
284 s
285 }
286}
287
288fn parse_modifier(token: &str) -> Option<KeyModifiers> {
290 Some(match token.to_ascii_lowercase().as_str() {
291 "ctrl" | "control" => KeyModifiers::CONTROL,
292 "alt" | "option" => KeyModifiers::ALT,
293 "shift" => KeyModifiers::SHIFT,
294 _ => return None,
295 })
296}
297
298fn parse_keycode(token: &str) -> Option<KeyCode> {
300 let lower = token.to_ascii_lowercase();
301 Some(match lower.as_str() {
302 "up" => KeyCode::Up,
303 "down" => KeyCode::Down,
304 "left" => KeyCode::Left,
305 "right" => KeyCode::Right,
306 "home" => KeyCode::Home,
307 "end" => KeyCode::End,
308 "pageup" | "pgup" => KeyCode::PageUp,
309 "pagedown" | "pgdn" | "pgdown" => KeyCode::PageDown,
310 "enter" | "return" => KeyCode::Enter,
311 "esc" | "escape" => KeyCode::Esc,
312 "tab" => KeyCode::Tab,
313 "backtab" => KeyCode::BackTab,
314 "space" => KeyCode::Char(' '),
315 "backspace" => KeyCode::Backspace,
316 "delete" | "del" => KeyCode::Delete,
317 "insert" | "ins" => KeyCode::Insert,
318 _ => {
319 if let Some(n) = lower.strip_prefix('f').and_then(|d| d.parse::<u8>().ok())
320 && (1..=12).contains(&n)
321 {
322 return Some(KeyCode::F(n));
323 }
324 let mut chars = token.chars();
325 let c = chars.next()?;
326 if chars.next().is_some() {
327 return None;
328 }
329 KeyCode::Char(c)
330 }
331 })
332}
333
334fn keycode_name(code: KeyCode) -> String {
336 match code {
337 KeyCode::Up => "up".into(),
338 KeyCode::Down => "down".into(),
339 KeyCode::Left => "left".into(),
340 KeyCode::Right => "right".into(),
341 KeyCode::Home => "home".into(),
342 KeyCode::End => "end".into(),
343 KeyCode::PageUp => "pageup".into(),
344 KeyCode::PageDown => "pagedown".into(),
345 KeyCode::Enter => "enter".into(),
346 KeyCode::Esc => "esc".into(),
347 KeyCode::Tab => "tab".into(),
348 KeyCode::BackTab => "backtab".into(),
349 KeyCode::Backspace => "backspace".into(),
350 KeyCode::Delete => "delete".into(),
351 KeyCode::Insert => "insert".into(),
352 KeyCode::Char(' ') => "space".into(),
353 KeyCode::Char(c) => c.to_string(),
354 KeyCode::F(n) => format!("f{n}"),
355 other => format!("{other:?}").to_ascii_lowercase(),
356 }
357}
358
359fn keycode_display(code: KeyCode) -> String {
362 match code {
363 KeyCode::Up => "↑".into(),
364 KeyCode::Down => "↓".into(),
365 KeyCode::Left => "←".into(),
366 KeyCode::Right => "→".into(),
367 KeyCode::Home => "Home".into(),
368 KeyCode::End => "End".into(),
369 KeyCode::PageUp => "PgUp".into(),
370 KeyCode::PageDown => "PgDn".into(),
371 KeyCode::Enter => "Enter".into(),
372 KeyCode::Esc => "Esc".into(),
373 KeyCode::Tab => "Tab".into(),
374 KeyCode::BackTab => "Shift+Tab".into(),
375 KeyCode::Backspace => "Backspace".into(),
376 KeyCode::Delete => "Del".into(),
377 KeyCode::Insert => "Ins".into(),
378 KeyCode::Char(' ') => "Space".into(),
379 KeyCode::Char(c) => c.to_string(),
380 KeyCode::F(n) => format!("F{n}"),
381 other => format!("{other:?}"),
382 }
383}
384
385#[derive(Debug, Clone)]
388pub struct Keymap {
389 bindings: HashMap<KeyChord, KeyAction>,
390}
391
392impl Keymap {
393 pub fn defaults() -> Keymap {
395 let pairs: Vec<(KeyAction, KeyChord)> = vec![
396 (KeyAction::NavigateUp, KeyChord::key(KeyCode::Up)),
397 (KeyAction::NavigateUp, KeyChord::key(KeyCode::Char('k'))),
398 (KeyAction::NavigateDown, KeyChord::key(KeyCode::Down)),
399 (KeyAction::NavigateDown, KeyChord::key(KeyCode::Char('j'))),
400 (KeyAction::PageUp, KeyChord::key(KeyCode::PageUp)),
401 (KeyAction::PageUp, KeyChord::ctrl('u')),
402 (KeyAction::PageDown, KeyChord::key(KeyCode::PageDown)),
403 (KeyAction::PageDown, KeyChord::ctrl('d')),
404 (KeyAction::GoToTop, KeyChord::key(KeyCode::Char('g'))),
405 (KeyAction::GoToTop, KeyChord::key(KeyCode::Home)),
406 (KeyAction::GoToBottom, KeyChord::key(KeyCode::Char('G'))),
407 (KeyAction::GoToBottom, KeyChord::key(KeyCode::End)),
408 (KeyAction::FocusNextPane, KeyChord::key(KeyCode::Tab)),
409 (
410 KeyAction::FocusPrevPane,
411 KeyChord::normalized(KeyCode::Tab, KeyModifiers::SHIFT),
412 ),
413 (KeyAction::Switch, KeyChord::key(KeyCode::Enter)),
414 (KeyAction::Filter, KeyChord::key(KeyCode::Char('/'))),
415 (KeyAction::ClearFilter, KeyChord::key(KeyCode::Esc)),
416 (KeyAction::New, KeyChord::key(KeyCode::Char('n'))),
417 (KeyAction::Remove, KeyChord::key(KeyCode::Char('d'))),
418 (KeyAction::PrCheckout, KeyChord::key(KeyCode::Char('p'))),
419 (KeyAction::Checkout, KeyChord::key(KeyCode::Char('c'))),
420 (KeyAction::Sync, KeyChord::key(KeyCode::Char('y'))),
421 (KeyAction::OpenEditor, KeyChord::key(KeyCode::Char('o'))),
422 (KeyAction::Refresh, KeyChord::key(KeyCode::Char('r'))),
423 (KeyAction::SortCycle, KeyChord::key(KeyCode::Char('s'))),
424 (KeyAction::SortReverse, KeyChord::key(KeyCode::Char('S'))),
425 (KeyAction::Help, KeyChord::key(KeyCode::Char('?'))),
426 (KeyAction::Quit, KeyChord::key(KeyCode::Char('q'))),
427 (KeyAction::ToggleSidebar, KeyChord::key(KeyCode::Char('\\'))),
428 (
429 KeyAction::ResizeSidebarGrow,
430 KeyChord::key(KeyCode::Char('+')),
431 ),
432 (
433 KeyAction::ResizeSidebarShrink,
434 KeyChord::key(KeyCode::Char('-')),
435 ),
436 ];
437 let mut bindings = HashMap::with_capacity(pairs.len());
438 for (action, chord) in pairs {
439 bindings.insert(chord, action);
440 }
441 Keymap { bindings }
442 }
443
444 pub fn action_for(&self, chord: KeyChord) -> Option<KeyAction> {
446 self.bindings.get(&chord).copied()
447 }
448
449 pub fn rebind(&mut self, action: KeyAction, chord: KeyChord) {
452 self.bindings.retain(|_, a| *a != action);
453 self.bindings.insert(chord, action);
454 }
455
456 pub fn chords_for(&self, action: KeyAction) -> Vec<KeyChord> {
458 self.bindings
459 .iter()
460 .filter(|(_, a)| **a == action)
461 .map(|(c, _)| *c)
462 .collect()
463 }
464
465 pub fn display_for(&self, action: KeyAction) -> Option<String> {
471 let mut chords = self.chords_for(action);
472 if chords.is_empty() {
473 return None;
474 }
475 chords.sort_by_key(chord_sort_key);
476 Some(
477 chords
478 .iter()
479 .map(KeyChord::display)
480 .collect::<Vec<_>>()
481 .join("/"),
482 )
483 }
484}
485
486fn chord_sort_key(chord: &KeyChord) -> (u8, String) {
490 let bucket = u8::from(matches!(chord.code, KeyCode::Char(_)));
491 (bucket, chord.render())
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497
498 #[test]
499 fn action_names_round_trip_and_are_unique() {
500 assert_eq!(KeyAction::ALL.len(), 25);
501 let mut names = std::collections::HashSet::new();
502 for action in KeyAction::ALL {
503 assert_eq!(KeyAction::parse(action.name()), Some(action));
504 assert!(
505 names.insert(action.name()),
506 "duplicate name {}",
507 action.name()
508 );
509 }
510 assert_eq!(KeyAction::parse("not-an-action"), None);
511 }
512
513 #[test]
514 fn parse_modifiers_and_keys() {
515 assert_eq!(KeyChord::parse("ctrl+u"), Some(KeyChord::ctrl('u')));
516 assert_eq!(
517 KeyChord::parse("alt+enter"),
518 Some(KeyChord::normalized(KeyCode::Enter, KeyModifiers::ALT))
519 );
520 assert_eq!(KeyChord::parse("f5"), Some(KeyChord::key(KeyCode::F(5))));
521 assert_eq!(
522 KeyChord::parse("g"),
523 Some(KeyChord::key(KeyCode::Char('g')))
524 );
525 assert_eq!(
526 KeyChord::parse("G"),
527 Some(KeyChord::key(KeyCode::Char('G')))
528 );
529 assert_eq!(
530 KeyChord::parse("?"),
531 Some(KeyChord::key(KeyCode::Char('?')))
532 );
533 assert_eq!(
534 KeyChord::parse("+"),
535 Some(KeyChord::key(KeyCode::Char('+')))
536 );
537 assert_eq!(
538 KeyChord::parse("-"),
539 Some(KeyChord::key(KeyCode::Char('-')))
540 );
541 assert_eq!(
542 KeyChord::parse("space"),
543 Some(KeyChord::key(KeyCode::Char(' ')))
544 );
545 assert_eq!(
546 KeyChord::parse("PgUp"),
547 Some(KeyChord::key(KeyCode::PageUp))
548 );
549 }
550
551 #[test]
552 fn parse_normalizes_shift_tab() {
553 let want = KeyChord::normalized(KeyCode::Tab, KeyModifiers::SHIFT);
554 assert_eq!(KeyChord::parse("shift+tab"), Some(want));
555 assert_eq!(KeyChord::parse("backtab"), Some(want));
556 assert_eq!(want.code, KeyCode::Tab);
557 assert!(want.mods.contains(KeyModifiers::SHIFT));
558 }
559
560 #[test]
561 fn parse_rejects_malformed() {
562 assert_eq!(KeyChord::parse(""), None);
563 assert_eq!(KeyChord::parse("ctrl+"), None);
564 assert_eq!(KeyChord::parse("nope+x"), None);
565 assert_eq!(KeyChord::parse("f99"), None);
566 assert_eq!(KeyChord::parse("abc"), None);
567 }
568
569 #[test]
570 fn from_event_normalizes() {
571 let backtab = KeyEvent::new(KeyCode::BackTab, KeyModifiers::empty());
572 assert_eq!(
573 KeyChord::from_event(backtab),
574 KeyChord::normalized(KeyCode::Tab, KeyModifiers::SHIFT)
575 );
576 let shifted_g = KeyEvent::new(KeyCode::Char('G'), KeyModifiers::SHIFT);
577 assert_eq!(
578 KeyChord::from_event(shifted_g),
579 KeyChord::key(KeyCode::Char('G'))
580 );
581 let ctrl_u = KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL);
582 assert_eq!(KeyChord::from_event(ctrl_u), KeyChord::ctrl('u'));
583 }
584
585 #[test]
586 fn render_round_trips() {
587 for s in [
588 "ctrl+u",
589 "alt+enter",
590 "shift+tab",
591 "f5",
592 "g",
593 "G",
594 "esc",
595 "space",
596 ] {
597 let chord = KeyChord::parse(s).unwrap();
598 assert_eq!(
599 KeyChord::parse(&chord.render()),
600 Some(chord),
601 "round-trip {s}"
602 );
603 }
604 }
605
606 #[test]
607 fn all_named_keycodes_round_trip() {
608 for code in [
609 KeyCode::Up,
610 KeyCode::Down,
611 KeyCode::Left,
612 KeyCode::Right,
613 KeyCode::Home,
614 KeyCode::End,
615 KeyCode::PageUp,
616 KeyCode::PageDown,
617 KeyCode::Enter,
618 KeyCode::Esc,
619 KeyCode::Tab,
620 KeyCode::Backspace,
621 KeyCode::Delete,
622 KeyCode::Insert,
623 KeyCode::Char(' '),
624 KeyCode::Char('x'),
625 KeyCode::F(12),
626 ] {
627 let chord = KeyChord::key(code);
628 let rendered = chord.render();
629 assert_eq!(
630 KeyChord::parse(&rendered),
631 Some(chord),
632 "round-trip {rendered}"
633 );
634 }
635 }
636
637 #[test]
638 fn defaults_cover_the_spec_table() {
639 let m = Keymap::defaults();
640 assert_eq!(
641 m.action_for(KeyChord::key(KeyCode::Up)),
642 Some(KeyAction::NavigateUp)
643 );
644 assert_eq!(
645 m.action_for(KeyChord::key(KeyCode::Char('k'))),
646 Some(KeyAction::NavigateUp)
647 );
648 assert_eq!(m.action_for(KeyChord::ctrl('u')), Some(KeyAction::PageUp));
649 assert_eq!(
650 m.action_for(KeyChord::key(KeyCode::Enter)),
651 Some(KeyAction::Switch)
652 );
653 assert_eq!(
654 m.action_for(KeyChord::normalized(KeyCode::Tab, KeyModifiers::SHIFT)),
655 Some(KeyAction::FocusPrevPane)
656 );
657 assert_eq!(
658 m.action_for(KeyChord::key(KeyCode::Char('?'))),
659 Some(KeyAction::Help)
660 );
661 assert_eq!(
662 m.action_for(KeyChord::key(KeyCode::Char('S'))),
663 Some(KeyAction::SortReverse)
664 );
665 assert_eq!(
666 m.action_for(KeyChord::key(KeyCode::Char('c'))),
667 Some(KeyAction::Checkout)
668 );
669 assert_eq!(
670 m.action_for(KeyChord::key(KeyCode::Char('y'))),
671 Some(KeyAction::Sync)
672 );
673 assert_eq!(m.action_for(KeyChord::key(KeyCode::Char('z'))), None);
674 }
675
676 #[test]
677 fn every_action_has_a_label() {
678 for action in KeyAction::ALL {
679 assert!(!action.label().is_empty(), "missing label for {action:?}");
680 }
681 }
682
683 #[test]
684 fn chord_display_is_terminal_pretty() {
685 assert_eq!(KeyChord::key(KeyCode::Up).display(), "↑");
686 assert_eq!(KeyChord::key(KeyCode::Down).display(), "↓");
687 assert_eq!(KeyChord::key(KeyCode::Enter).display(), "Enter");
688 assert_eq!(KeyChord::key(KeyCode::Esc).display(), "Esc");
689 assert_eq!(KeyChord::key(KeyCode::Char('k')).display(), "k");
690 assert_eq!(KeyChord::key(KeyCode::Char('?')).display(), "?");
691 assert_eq!(KeyChord::ctrl('s').display(), "Ctrl-S");
692 assert_eq!(
693 KeyChord::normalized(KeyCode::Tab, KeyModifiers::SHIFT).display(),
694 "Shift+Tab"
695 );
696 }
697
698 #[test]
699 fn display_for_is_sorted_and_deterministic() {
700 let m = Keymap::defaults();
701 assert_eq!(m.display_for(KeyAction::NavigateUp).as_deref(), Some("↑/k"));
703 assert_eq!(m.display_for(KeyAction::Switch).as_deref(), Some("Enter"));
704 assert_eq!(m.display_for(KeyAction::SortCycle).as_deref(), Some("s"));
705 assert_eq!(m.display_for(KeyAction::Checkout).as_deref(), Some("c"));
706 assert_eq!(
708 m.display_for(KeyAction::NavigateUp),
709 m.display_for(KeyAction::NavigateUp)
710 );
711 }
712
713 #[test]
714 fn display_for_follows_rebind_and_is_none_when_unbound() {
715 let mut m = Keymap::defaults();
716 m.rebind(KeyAction::Checkout, KeyChord::key(KeyCode::Char('x')));
717 assert_eq!(m.display_for(KeyAction::Checkout).as_deref(), Some("x"));
718 m.rebind(KeyAction::Quit, KeyChord::key(KeyCode::Char('x')));
720 assert_eq!(m.display_for(KeyAction::Checkout), None);
721 }
722
723 #[test]
724 fn rebind_replaces_all_chords_for_action() {
725 let mut m = Keymap::defaults();
726 m.rebind(KeyAction::NavigateUp, KeyChord::key(KeyCode::Char('w')));
727 assert_eq!(
728 m.action_for(KeyChord::key(KeyCode::Char('w'))),
729 Some(KeyAction::NavigateUp)
730 );
731 assert_eq!(m.action_for(KeyChord::key(KeyCode::Char('k'))), None);
733 assert_eq!(m.action_for(KeyChord::key(KeyCode::Up)), None);
734 assert_eq!(m.chords_for(KeyAction::NavigateUp).len(), 1);
735 }
736}