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