1use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use std::fmt;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum KeyAction {
10 Quit,
12 Shell,
14 SelectLeft,
16 SelectRight,
18 SelectUp,
20 SelectDown,
22 MoveLeft,
24 MoveRight,
26 ReorderUp,
28 ReorderDown,
30 ToggleExpand,
32 Add,
34 DependencyAdd,
36 DependencyRemove,
38 Parent,
40 Split,
42 Edit,
44 Reload,
46 Maximize,
48 Search,
50 RegexSearch,
52 Details,
54 Help,
56 ClearFilter,
58 ConfirmQuit,
60 CancelQuit,
62 PopupClose,
64 PopupScrollUp,
66 PopupScrollDown,
68 PopupSelectUp,
70 PopupSelectDown,
72 PopupSelectLeft,
74 PopupSelectRight,
76}
77
78impl KeyAction {
79 pub const ALL: &'static [Self] = &[
81 Self::Quit,
82 Self::Shell,
83 Self::SelectLeft,
84 Self::SelectRight,
85 Self::SelectUp,
86 Self::SelectDown,
87 Self::MoveLeft,
88 Self::MoveRight,
89 Self::ReorderUp,
90 Self::ReorderDown,
91 Self::ToggleExpand,
92 Self::Add,
93 Self::DependencyAdd,
94 Self::DependencyRemove,
95 Self::Parent,
96 Self::Split,
97 Self::Edit,
98 Self::Reload,
99 Self::Maximize,
100 Self::Search,
101 Self::RegexSearch,
102 Self::Details,
103 Self::Help,
104 Self::ClearFilter,
105 Self::ConfirmQuit,
106 Self::CancelQuit,
107 Self::PopupClose,
108 Self::PopupScrollUp,
109 Self::PopupScrollDown,
110 Self::PopupSelectUp,
111 Self::PopupSelectDown,
112 Self::PopupSelectLeft,
113 Self::PopupSelectRight,
114 ];
115
116 #[must_use]
118 pub const fn name(self) -> &'static str {
119 match self {
120 Self::Quit => "quit",
121 Self::Shell => "shell",
122 Self::SelectLeft => "select_left",
123 Self::SelectRight => "select_right",
124 Self::SelectUp => "select_up",
125 Self::SelectDown => "select_down",
126 Self::MoveLeft => "move_left",
127 Self::MoveRight => "move_right",
128 Self::ReorderUp => "reorder_up",
129 Self::ReorderDown => "reorder_down",
130 Self::ToggleExpand => "toggle_expand",
131 Self::Add => "add",
132 Self::DependencyAdd => "dependency_add",
133 Self::DependencyRemove => "dependency_remove",
134 Self::Parent => "parent",
135 Self::Split => "split",
136 Self::Edit => "edit",
137 Self::Reload => "reload",
138 Self::Maximize => "maximize",
139 Self::Search => "search",
140 Self::RegexSearch => "regex_search",
141 Self::Details => "details",
142 Self::Help => "help",
143 Self::ClearFilter => "clear_filter",
144 Self::ConfirmQuit => "confirm_quit",
145 Self::CancelQuit => "cancel_quit",
146 Self::PopupClose => "popup_close",
147 Self::PopupScrollUp => "popup_scroll_up",
148 Self::PopupScrollDown => "popup_scroll_down",
149 Self::PopupSelectUp => "popup_select_up",
150 Self::PopupSelectDown => "popup_select_down",
151 Self::PopupSelectLeft => "popup_select_left",
152 Self::PopupSelectRight => "popup_select_right",
153 }
154 }
155
156 fn from_name(name: &str) -> Option<Self> {
157 Self::ALL
158 .iter()
159 .copied()
160 .find(|action| action.name() == name)
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
166pub enum KeyCode {
167 Char(char),
169 Enter,
171 Esc,
173 Tab,
175 Backspace,
177 Delete,
179 Insert,
181 Left,
183 Right,
185 Up,
187 Down,
189 Home,
191 End,
193 PageUp,
195 PageDown,
197 F(u8),
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
203pub struct Modifiers(u8);
204
205impl Modifiers {
206 pub const NONE: Self = Self(0);
208 pub const SHIFT: Self = Self(1 << 0);
210 pub const CONTROL: Self = Self(1 << 1);
212 pub const ALT: Self = Self(1 << 2);
214 pub const SUPER: Self = Self(1 << 3);
216 pub const HYPER: Self = Self(1 << 4);
218 pub const META: Self = Self(1 << 5);
220
221 #[must_use]
223 pub const fn contains(self, other: Self) -> bool {
224 self.0 & other.0 == other.0
225 }
226
227 const fn insert(self, other: Self) -> Self {
228 Self(self.0 | other.0)
229 }
230}
231
232impl std::ops::BitOr for Modifiers {
233 type Output = Self;
234
235 fn bitor(self, rhs: Self) -> Self::Output {
236 self.insert(rhs)
237 }
238}
239
240impl std::ops::BitOrAssign for Modifiers {
241 fn bitor_assign(&mut self, rhs: Self) {
242 *self = self.insert(rhs);
243 }
244}
245
246#[derive(Debug, Clone)]
248pub struct KeyStroke {
249 code: KeyCode,
250 modifiers: Modifiers,
251 display: String,
253}
254
255impl PartialEq for KeyStroke {
256 fn eq(&self, other: &Self) -> bool {
257 self.code == other.code && self.modifiers == other.modifiers
258 }
259}
260
261impl Eq for KeyStroke {}
262
263impl KeyStroke {
264 pub fn parse(input: &str) -> Result<Self, KeySpecError> {
266 let input = input.trim();
267 if input.is_empty() {
268 return Err(KeySpecError::new(input, "the key name must not be empty"));
269 }
270
271 let parts: Vec<&str> = input.split('+').collect();
272 if parts.iter().any(|part| part.trim().is_empty()) {
273 return Err(KeySpecError::new(
274 input,
275 "use `Plus` for the plus key and separate modifiers with `+`",
276 ));
277 }
278
279 let key_name = parts.last().copied().unwrap_or(input).trim();
280 let code = parse_key_code(key_name).map_err(|reason| KeySpecError::new(input, reason))?;
281 let mut modifiers = Modifiers::NONE;
282 for modifier_name in &parts[..parts.len().saturating_sub(1)] {
283 let modifier_name = modifier_name.trim();
284 let modifier = parse_modifier(modifier_name).ok_or_else(|| {
285 KeySpecError::new(
286 input,
287 format!(
288 "unknown modifier {modifier_name:?}; use Ctrl, Alt, Shift, Cmd, Meta, or Hyper"
289 ),
290 )
291 })?;
292 modifiers |= modifier;
293 }
294
295 if modifiers.contains(Modifiers::SHIFT) && matches!(code, KeyCode::Char(_)) {
296 return Err(KeySpecError::new(
297 input,
298 "write a Shift-modified printable key as its resulting character (use `A` instead of `Shift+a`, or `<` instead of `Shift+,`)",
299 ));
300 }
301
302 let display = display_key_stroke(code, modifiers);
303
304 Ok(Self {
305 code,
306 modifiers,
307 display,
308 })
309 }
310
311 #[must_use]
313 pub fn display(&self) -> &str {
314 &self.display
315 }
316
317 #[must_use]
320 pub fn matches(&self, code: KeyCode, modifiers: Modifiers) -> bool {
321 (self.code == code && self.modifiers == modifiers)
322 || control_letter_matches(self.code, self.modifiers, code, modifiers)
323 || control_character_terminal_alias_matches(self.code, self.modifiers, code, modifiers)
324 || question_mark_terminal_alias_matches(self.code, self.modifiers, code, modifiers)
325 }
326}
327
328fn control_letter_matches(
329 configured_code: KeyCode,
330 configured_modifiers: Modifiers,
331 actual_code: KeyCode,
332 actual_modifiers: Modifiers,
333) -> bool {
334 if configured_modifiers != actual_modifiers
335 || !configured_modifiers.contains(Modifiers::CONTROL)
336 {
337 return false;
338 }
339
340 match (configured_code, actual_code) {
341 (KeyCode::Char(configured), KeyCode::Char(actual)) => {
342 configured.is_ascii_alphabetic() && configured.eq_ignore_ascii_case(&actual)
343 }
344 _ => false,
345 }
346}
347
348fn control_character_terminal_alias_matches(
349 configured_code: KeyCode,
350 configured_modifiers: Modifiers,
351 actual_code: KeyCode,
352 actual_modifiers: Modifiers,
353) -> bool {
354 let KeyCode::Char(configured) = configured_code else {
355 return false;
356 };
357 if !configured_modifiers.contains(Modifiers::CONTROL) {
358 return false;
359 }
360
361 let actual_code_matches = match configured {
362 '@' => actual_code == KeyCode::Char(' '),
363 '[' => actual_code == KeyCode::Esc,
364 '\\' => actual_code == KeyCode::Char('4'),
365 ']' => actual_code == KeyCode::Char('5'),
366 '^' => actual_code == KeyCode::Char('6'),
367 '_' => actual_code == KeyCode::Char('7'),
368 _ => false,
369 };
370 if !actual_code_matches {
371 return false;
372 }
373
374 configured_modifiers == actual_modifiers
375 || (actual_code == KeyCode::Esc
376 && configured_modifiers == (actual_modifiers | Modifiers::CONTROL))
377}
378
379fn question_mark_terminal_alias_matches(
380 configured_code: KeyCode,
381 configured_modifiers: Modifiers,
382 actual_code: KeyCode,
383 actual_modifiers: Modifiers,
384) -> bool {
385 if configured_code != KeyCode::Char('?') || configured_modifiers == Modifiers::NONE {
386 return false;
387 }
388
389 if actual_code == KeyCode::Char('7') {
395 return configured_modifiers == actual_modifiers;
396 }
397
398 actual_code == KeyCode::Backspace
399 && (configured_modifiers == actual_modifiers
400 || (configured_modifiers.contains(Modifiers::CONTROL)
401 && configured_modifiers == (actual_modifiers | Modifiers::CONTROL)))
402}
403
404#[derive(Debug, Clone, PartialEq, Eq)]
406pub struct KeySpecError {
407 input: String,
408 reason: String,
409}
410
411impl KeySpecError {
412 fn new(input: &str, reason: impl Into<String>) -> Self {
413 Self {
414 input: input.to_string(),
415 reason: reason.into(),
416 }
417 }
418}
419
420impl fmt::Display for KeySpecError {
421 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422 write!(
423 f,
424 "invalid key {:?}: {}; supported key names include Esc, Enter, Space, arrows, and single characters",
425 self.input, self.reason
426 )
427 }
428}
429
430impl std::error::Error for KeySpecError {}
431
432#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
437#[serde(transparent)]
438pub struct KeyBindings(BTreeMap<String, Vec<String>>);
439
440impl Default for KeyBindings {
441 fn default() -> Self {
442 let mut bindings = BTreeMap::new();
443 for action in KeyAction::ALL {
444 bindings.insert(
445 action.name().to_string(),
446 default_keys(*action)
447 .iter()
448 .map(|key| (*key).to_string())
449 .collect(),
450 );
451 }
452 Self(bindings)
453 }
454}
455
456impl KeyBindings {
457 pub fn keys(&self, action: KeyAction) -> &[String] {
459 self.0.get(action.name()).map(Vec::as_slice).unwrap_or(&[])
460 }
461
462 pub fn set(&mut self, action: KeyAction, keys: Vec<String>) {
464 self.0.insert(action.name().to_string(), keys);
465 }
466
467 #[must_use]
469 pub fn with_defaults(mut self) -> Self {
470 for action in KeyAction::ALL {
471 self.0.entry(action.name().to_string()).or_insert_with(|| {
472 default_keys(*action)
473 .iter()
474 .map(|key| (*key).to_string())
475 .collect()
476 });
477 }
478 self
479 }
480
481 pub fn validate(&self) -> Result<(), KeyBindingError> {
483 for (name, keys) in &self.0 {
484 let Some(action) = KeyAction::from_name(name) else {
485 return Err(KeyBindingError::UnknownAction {
486 action: name.clone(),
487 });
488 };
489 if keys.is_empty() {
490 return Err(KeyBindingError::Empty {
491 action: action.name().to_string(),
492 });
493 }
494 for (index, key) in keys.iter().enumerate() {
495 KeyStroke::parse(key).map_err(|source| KeyBindingError::InvalidKey {
496 action: action.name().to_string(),
497 index,
498 source,
499 })?;
500 }
501 }
502 Ok(())
503 }
504}
505
506#[derive(Debug, Clone, PartialEq, Eq)]
508pub enum KeyBindingError {
509 UnknownAction { action: String },
511 Empty { action: String },
513 InvalidKey {
515 action: String,
516 index: usize,
517 source: KeySpecError,
518 },
519}
520
521impl fmt::Display for KeyBindingError {
522 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
523 match self {
524 Self::UnknownAction { action } => write!(
525 f,
526 "unknown operation {action:?}; use one of: {}",
527 KeyAction::ALL
528 .iter()
529 .map(|action| action.name())
530 .collect::<Vec<_>>()
531 .join(", ")
532 ),
533 Self::Empty { action } => write!(
534 f,
535 "operation {action:?} must have at least one key (remove the override to use its default)"
536 ),
537 Self::InvalidKey {
538 action,
539 index,
540 source,
541 } => write!(f, "operation {action:?} key #{index}: {source}"),
542 }
543 }
544}
545
546impl std::error::Error for KeyBindingError {}
547
548fn default_keys(action: KeyAction) -> &'static [&'static str] {
549 match action {
550 KeyAction::Quit => &["q", "Esc"],
551 KeyAction::Shell => &["Q"],
552 KeyAction::SelectLeft => &["h", "Left"],
553 KeyAction::SelectRight => &["l", "Right"],
554 KeyAction::SelectUp => &["k", "Up"],
555 KeyAction::SelectDown => &["j", "Down"],
556 KeyAction::MoveLeft => &["H", "Shift+Left"],
557 KeyAction::MoveRight => &["L", "Shift+Right"],
558 KeyAction::ReorderUp => &["K", "Shift+Up"],
559 KeyAction::ReorderDown => &["J", "Shift+Down"],
560 KeyAction::ToggleExpand => &["Space", "Enter"],
561 KeyAction::Add => &["a"],
562 KeyAction::DependencyAdd => &["d"],
563 KeyAction::DependencyRemove => &["D"],
564 KeyAction::Parent => &["p"],
565 KeyAction::Split => &["s"],
566 KeyAction::Edit => &["e"],
567 KeyAction::Reload => &["r"],
568 KeyAction::Maximize => &["m"],
569 KeyAction::Search => &["/"],
570 KeyAction::RegexSearch => &["Ctrl+?"],
571 KeyAction::Details => &["v"],
572 KeyAction::Help => &["?"],
573 KeyAction::ClearFilter => &["Esc"],
574 KeyAction::ConfirmQuit => &["y", "Y", "q", "Q", "Enter", "Esc"],
575 KeyAction::CancelQuit => &["n", "N"],
576 KeyAction::PopupClose => &["v", "Esc", "q"],
577 KeyAction::PopupScrollUp => &["k", "Up"],
578 KeyAction::PopupScrollDown => &["j", "Down"],
579 KeyAction::PopupSelectUp => &["K", "Shift+Up"],
580 KeyAction::PopupSelectDown => &["J", "Shift+Down"],
581 KeyAction::PopupSelectLeft => &["H", "Shift+Left"],
582 KeyAction::PopupSelectRight => &["L", "Shift+Right"],
583 }
584}
585
586fn parse_modifier(input: &str) -> Option<Modifiers> {
587 let modifier = match input.to_ascii_lowercase().as_str() {
588 "shift" => Modifiers::SHIFT,
589 "ctrl" | "control" => Modifiers::CONTROL,
590 "alt" | "option" => Modifiers::ALT,
591 "cmd" | "command" | "super" | "windows" | "win" => Modifiers::SUPER,
592 "meta" => Modifiers::META,
593 "hyper" => Modifiers::HYPER,
594 _ => return None,
595 };
596 Some(modifier)
597}
598
599fn parse_key_code(input: &str) -> Result<KeyCode, String> {
600 let normalized = input.to_ascii_lowercase();
601 let code = match normalized.as_str() {
602 "esc" | "escape" => KeyCode::Esc,
603 "enter" | "return" => KeyCode::Enter,
604 "space" => KeyCode::Char(' '),
605 "tab" => KeyCode::Tab,
606 "backspace" | "back" => KeyCode::Backspace,
607 "delete" | "del" => KeyCode::Delete,
608 "insert" | "ins" => KeyCode::Insert,
609 "left" => KeyCode::Left,
610 "right" => KeyCode::Right,
611 "up" => KeyCode::Up,
612 "down" => KeyCode::Down,
613 "home" => KeyCode::Home,
614 "end" => KeyCode::End,
615 "pageup" | "page_up" | "pgup" => KeyCode::PageUp,
616 "pagedown" | "page_down" | "pgdn" => KeyCode::PageDown,
617 "plus" => KeyCode::Char('+'),
618 _ if normalized.starts_with('f') && normalized[1..].parse::<u8>().is_ok() => {
619 let number = normalized[1..]
620 .parse::<u8>()
621 .map_err(|_| "function key number is invalid".to_string())?;
622 if (1..=24).contains(&number) {
623 KeyCode::F(number)
624 } else {
625 return Err("function keys must be between F1 and F24".to_string());
626 }
627 }
628 _ => {
629 let mut chars = input.chars();
630 let Some(character) = chars.next() else {
631 return Err("the key name must not be empty".to_string());
632 };
633 if chars.next().is_some() {
634 return Err(format!(
635 "unknown key {input:?}; use a named key, a single character, or F1-F24"
636 ));
637 }
638 KeyCode::Char(character)
639 }
640 };
641 Ok(code)
642}
643
644fn display_key_stroke(code: KeyCode, modifiers: Modifiers) -> String {
645 let mut names = Vec::with_capacity(6);
646 if modifiers.contains(Modifiers::CONTROL) {
647 names.push("Ctrl");
648 }
649 if modifiers.contains(Modifiers::ALT) {
650 names.push("Alt");
651 }
652 if modifiers.contains(Modifiers::SUPER) {
653 names.push("Cmd");
654 }
655 if modifiers.contains(Modifiers::META) {
656 names.push("Meta");
657 }
658 if modifiers.contains(Modifiers::HYPER) {
659 names.push("Hyper");
660 }
661 if modifiers.contains(Modifiers::SHIFT) {
662 names.push("Shift");
663 }
664 let key = display_key_code(code);
665 if names.is_empty() {
666 key
667 } else {
668 format!("{}+{key}", names.join("+"))
669 }
670}
671
672fn display_key_code(code: KeyCode) -> String {
673 match code {
674 KeyCode::Char(' ') => "Space".to_string(),
675 KeyCode::Char('+') => "Plus".to_string(),
676 KeyCode::Char(character) => character.to_string(),
677 KeyCode::Enter => "Enter".to_string(),
678 KeyCode::Esc => "Esc".to_string(),
679 KeyCode::Tab => "Tab".to_string(),
680 KeyCode::Backspace => "Backspace".to_string(),
681 KeyCode::Delete => "Delete".to_string(),
682 KeyCode::Insert => "Insert".to_string(),
683 KeyCode::Left => "Left".to_string(),
684 KeyCode::Right => "Right".to_string(),
685 KeyCode::Up => "Up".to_string(),
686 KeyCode::Down => "Down".to_string(),
687 KeyCode::Home => "Home".to_string(),
688 KeyCode::End => "End".to_string(),
689 KeyCode::PageUp => "PageUp".to_string(),
690 KeyCode::PageDown => "PageDown".to_string(),
691 KeyCode::F(number) => format!("F{number}"),
692 }
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698
699 #[test]
700 fn control_and_platform_modifiers_are_parsed_and_displayed() {
701 let control = KeyStroke::parse("Ctrl+a").expect("control key is valid");
702 assert!(control.matches(KeyCode::Char('a'), Modifiers::CONTROL));
703 assert_eq!(control.display(), "Ctrl+a");
704
705 let command = KeyStroke::parse("Cmd+q").expect("command key is valid");
706 assert!(command.matches(KeyCode::Char('q'), Modifiers::SUPER));
707 assert_eq!(command.display(), "Cmd+q");
708 }
709
710 #[test]
711 fn terminal_aliases_match_modified_printable_bindings() {
712 let control_letter = KeyStroke::parse("Ctrl+A").expect("control letter is valid");
713 assert!(control_letter.matches(KeyCode::Char('a'), Modifiers::CONTROL));
714
715 let unix_control_aliases = [
716 ("Ctrl+@", KeyCode::Char(' '), Modifiers::CONTROL),
717 ("Ctrl+[", KeyCode::Esc, Modifiers::NONE),
718 ("Ctrl+\\", KeyCode::Char('4'), Modifiers::CONTROL),
719 ("Ctrl+]", KeyCode::Char('5'), Modifiers::CONTROL),
720 ("Ctrl+^", KeyCode::Char('6'), Modifiers::CONTROL),
721 ("Ctrl+_", KeyCode::Char('7'), Modifiers::CONTROL),
722 ];
723 for (input, code, modifiers) in unix_control_aliases {
724 let stroke = KeyStroke::parse(input).expect("control punctuation is valid");
725 assert!(stroke.matches(code, modifiers), "input: {input}");
726 }
727
728 let regex_search = KeyStroke::parse("Ctrl+?").expect("regex search key is valid");
729 assert!(regex_search.matches(KeyCode::Backspace, Modifiers::NONE));
730 assert!(regex_search.matches(KeyCode::Char('?'), Modifiers::CONTROL));
731 assert!(regex_search.matches(KeyCode::Char('7'), Modifiers::CONTROL));
732
733 let alt_regex_search = KeyStroke::parse("Alt+?").expect("Alt binding is valid");
734 assert!(alt_regex_search.matches(KeyCode::Backspace, Modifiers::ALT));
735
736 let combined_regex_search =
737 KeyStroke::parse("Ctrl+Alt+?").expect("combined binding is valid");
738 assert!(combined_regex_search.matches(KeyCode::Backspace, Modifiers::ALT));
739 }
740
741 #[test]
742 fn default_bindings_keep_multiple_existing_keys() {
743 let bindings = KeyBindings::default();
744
745 assert_eq!(
746 bindings.keys(KeyAction::Quit),
747 ["q".to_string(), "Esc".to_string()]
748 );
749 assert_eq!(
750 bindings.keys(KeyAction::SelectLeft),
751 ["h".to_string(), "Left".to_string()]
752 );
753 assert!(
754 KeyAction::ALL
755 .iter()
756 .all(|action| !bindings.keys(*action).is_empty())
757 );
758 }
759
760 #[test]
761 fn invalid_key_spec_explains_the_supported_syntax() {
762 let error = KeyStroke::parse("Controlled+a")
763 .expect_err("unknown modifier must be rejected")
764 .to_string();
765
766 assert!(error.contains("Ctrl"));
767 assert!(error.contains("Alt"));
768 assert!(error.contains("Cmd"));
769 }
770
771 #[test]
772 fn parses_named_keys_aliases_and_function_keys() {
773 let cases = [
774 ("Esc", KeyCode::Esc),
775 ("Escape", KeyCode::Esc),
776 ("Enter", KeyCode::Enter),
777 ("Return", KeyCode::Enter),
778 ("Space", KeyCode::Char(' ')),
779 ("Tab", KeyCode::Tab),
780 ("Backspace", KeyCode::Backspace),
781 ("Back", KeyCode::Backspace),
782 ("Delete", KeyCode::Delete),
783 ("Del", KeyCode::Delete),
784 ("Insert", KeyCode::Insert),
785 ("Ins", KeyCode::Insert),
786 ("Left", KeyCode::Left),
787 ("Right", KeyCode::Right),
788 ("Up", KeyCode::Up),
789 ("Down", KeyCode::Down),
790 ("Home", KeyCode::Home),
791 ("End", KeyCode::End),
792 ("PageUp", KeyCode::PageUp),
793 ("page_up", KeyCode::PageUp),
794 ("PgUp", KeyCode::PageUp),
795 ("PageDown", KeyCode::PageDown),
796 ("page_down", KeyCode::PageDown),
797 ("PgDn", KeyCode::PageDown),
798 ("Plus", KeyCode::Char('+')),
799 ("F1", KeyCode::F(1)),
800 ("F24", KeyCode::F(24)),
801 ];
802
803 for (input, expected) in cases {
804 let stroke = KeyStroke::parse(input).expect("named key is valid");
805 assert!(stroke.matches(expected, Modifiers::NONE), "input: {input}");
806 }
807 assert_eq!(
808 KeyStroke::parse("Space").expect("space key").display(),
809 "Space"
810 );
811 assert_eq!(
812 KeyStroke::parse("Plus").expect("plus key").display(),
813 "Plus"
814 );
815 assert_eq!(KeyStroke::parse(".").expect("character key").display(), ".");
816 assert_eq!(
817 KeyStroke::parse("F1").expect("function key").display(),
818 "F1"
819 );
820 }
821
822 #[test]
823 fn parses_modifier_aliases_and_displays_combined_modifiers() {
824 let aliases = [
825 ("Shift+Left", Modifiers::SHIFT),
826 ("Ctrl+Left", Modifiers::CONTROL),
827 ("Control+Left", Modifiers::CONTROL),
828 ("Alt+Left", Modifiers::ALT),
829 ("Option+Left", Modifiers::ALT),
830 ("Cmd+Left", Modifiers::SUPER),
831 ("Command+Left", Modifiers::SUPER),
832 ("Super+Left", Modifiers::SUPER),
833 ("Windows+Left", Modifiers::SUPER),
834 ("Win+Left", Modifiers::SUPER),
835 ("Meta+Left", Modifiers::META),
836 ("Hyper+Left", Modifiers::HYPER),
837 ];
838
839 for (input, expected) in aliases {
840 let stroke = KeyStroke::parse(input).expect("modifier alias is valid");
841 assert!(stroke.matches(KeyCode::Left, expected), "input: {input}");
842 }
843
844 let combined = KeyStroke::parse("Ctrl+Alt+Cmd+Meta+Hyper+Shift+PageDown")
845 .expect("combined modifiers are valid");
846 assert_eq!(combined.display(), "Ctrl+Alt+Cmd+Meta+Hyper+Shift+PageDown");
847 assert!(combined.matches(
848 KeyCode::PageDown,
849 Modifiers::CONTROL
850 | Modifiers::ALT
851 | Modifiers::SUPER
852 | Modifiers::META
853 | Modifiers::HYPER
854 | Modifiers::SHIFT
855 ));
856 }
857
858 #[test]
859 fn rejects_malformed_key_expressions() {
860 for input in [
861 "",
862 "+",
863 "Ctrl+",
864 "Unknown+a",
865 "Shift+a",
866 "F0",
867 "F25",
868 "Fabc",
869 "left right",
870 ] {
871 assert!(KeyStroke::parse(input).is_err(), "input: {input:?}");
872 }
873 assert!(
874 KeyStroke::parse("Ctrl+")
875 .expect_err("missing key")
876 .to_string()
877 .contains("Plus")
878 );
879 assert!(
880 KeyStroke::parse("F25")
881 .expect_err("function key range")
882 .to_string()
883 .contains("F1 and F24")
884 );
885 }
886
887 #[test]
888 fn key_bindings_fill_defaults_and_report_validation_errors() {
889 let empty = KeyBindings(BTreeMap::new());
890 assert!(empty.keys(KeyAction::Quit).is_empty());
891 let completed = empty.with_defaults();
892 assert!(completed.validate().is_ok());
893 assert_eq!(completed.keys(KeyAction::Quit), ["q", "Esc"]);
894
895 let mut unknown = KeyBindings(BTreeMap::new());
896 unknown
897 .0
898 .insert("unknown".to_string(), vec!["q".to_string()]);
899 let error = unknown.validate().expect_err("unknown action must fail");
900 assert!(matches!(error, KeyBindingError::UnknownAction { .. }));
901 assert!(error.to_string().contains("unknown operation"));
902
903 let mut empty_action = KeyBindings(BTreeMap::new());
904 empty_action.0.insert("quit".to_string(), Vec::new());
905 let error = empty_action
906 .validate()
907 .expect_err("empty assignment must fail");
908 assert!(matches!(error, KeyBindingError::Empty { .. }));
909 assert!(error.to_string().contains("at least one key"));
910
911 let mut invalid = KeyBindings(BTreeMap::new());
912 invalid
913 .0
914 .insert("quit".to_string(), vec!["not a key".to_string()]);
915 let error = invalid.validate().expect_err("invalid key must fail");
916 assert!(matches!(
917 error,
918 KeyBindingError::InvalidKey { index: 0, .. }
919 ));
920 assert!(error.to_string().contains("key #0"));
921 }
922}