1use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::fmt;
18use std::path::{Path, PathBuf};
19use std::str::FromStr;
20
21#[derive(Debug)]
27pub struct KeyBindingsLoadResult {
28 pub bindings: KeyBindings,
30 pub path: PathBuf,
32 pub warnings: Vec<KeyBindingsWarning>,
34}
35
36impl KeyBindingsLoadResult {
37 #[must_use]
39 pub fn has_warnings(&self) -> bool {
40 !self.warnings.is_empty()
41 }
42
43 #[must_use]
45 pub fn format_warnings(&self) -> String {
46 self.warnings
47 .iter()
48 .map(std::string::ToString::to_string)
49 .collect::<Vec<_>>()
50 .join("\n")
51 }
52}
53
54#[derive(Debug, Clone)]
56pub enum KeyBindingsWarning {
57 ReadError { path: PathBuf, error: String },
59 ParseError { path: PathBuf, error: String },
61 UnknownAction { action: String, path: PathBuf },
63 InvalidKey {
65 action: String,
66 key: String,
67 error: String,
68 path: PathBuf,
69 },
70 InvalidKeyValue {
72 action: String,
73 index: usize,
74 path: PathBuf,
75 },
76}
77
78impl fmt::Display for KeyBindingsWarning {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 Self::ReadError { path, error } => {
82 write!(f, "Cannot read {}: {}", path.display(), error)
83 }
84 Self::ParseError { path, error } => {
85 write!(f, "Invalid JSON in {}: {}", path.display(), error)
86 }
87 Self::UnknownAction { action, path } => {
88 write!(
89 f,
90 "Unknown action '{}' in {} (ignored)",
91 action,
92 path.display()
93 )
94 }
95 Self::InvalidKey {
96 action,
97 key,
98 error,
99 path,
100 } => {
101 write!(
102 f,
103 "Invalid key '{}' for action '{}' in {}: {}",
104 key,
105 action,
106 path.display(),
107 error
108 )
109 }
110 Self::InvalidKeyValue {
111 action,
112 index,
113 path,
114 } => {
115 write!(
116 f,
117 "Invalid value type at index {} for action '{}' in {} (expected string)",
118 index,
119 action,
120 path.display()
121 )
122 }
123 }
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
133#[serde(rename_all = "snake_case")]
134pub enum ActionCategory {
135 CursorMovement,
136 Deletion,
137 TextInput,
138 KillRing,
139 Clipboard,
140 Application,
141 Session,
142 ModelsThinking,
143 Display,
144 MessageQueue,
145 Selection,
146 SessionPicker,
147}
148
149impl ActionCategory {
150 #[must_use]
152 pub const fn display_name(&self) -> &'static str {
153 match self {
154 Self::CursorMovement => "Cursor Movement",
155 Self::Deletion => "Deletion",
156 Self::TextInput => "Text Input",
157 Self::KillRing => "Kill Ring",
158 Self::Clipboard => "Clipboard",
159 Self::Application => "Application",
160 Self::Session => "Session",
161 Self::ModelsThinking => "Models & Thinking",
162 Self::Display => "Display",
163 Self::MessageQueue => "Message Queue",
164 Self::Selection => "Selection (Lists, Pickers)",
165 Self::SessionPicker => "Session Picker",
166 }
167 }
168
169 #[must_use]
171 pub const fn all() -> &'static [Self] {
172 &[
173 Self::CursorMovement,
174 Self::Deletion,
175 Self::TextInput,
176 Self::KillRing,
177 Self::Clipboard,
178 Self::Application,
179 Self::Session,
180 Self::ModelsThinking,
181 Self::Display,
182 Self::MessageQueue,
183 Self::Selection,
184 Self::SessionPicker,
185 ]
186 }
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub enum AppAction {
199 CursorUp,
201 CursorDown,
202 CursorLeft,
203 CursorRight,
204 CursorWordLeft,
205 CursorWordRight,
206 CursorLineStart,
207 CursorLineEnd,
208 JumpForward,
209 JumpBackward,
210 PageUp,
211 PageDown,
212
213 DeleteCharBackward,
215 DeleteCharForward,
216 DeleteWordBackward,
217 DeleteWordForward,
218 DeleteToLineStart,
219 DeleteToLineEnd,
220
221 NewLine,
223 Submit,
224 Tab,
225
226 Yank,
228 YankPop,
229 Undo,
230
231 Copy,
233 PasteImage,
234
235 Interrupt,
237 Clear,
238 Exit,
239 Suspend,
240 ExternalEditor,
241 Help,
242 OpenSettings,
243
244 NewSession,
246 Tree,
247 Fork,
248 BranchPicker,
249 BranchNextSibling,
250 BranchPrevSibling,
251
252 SelectModel,
254 CycleModelForward,
255 CycleModelBackward,
256 CycleThinkingLevel,
257
258 ExpandTools,
260 ToggleThinking,
261
262 FollowUp,
264 Dequeue,
265
266 SelectUp,
268 SelectDown,
269 SelectPageUp,
270 SelectPageDown,
271 SelectConfirm,
272 SelectCancel,
273
274 ToggleSessionPath,
276 ToggleSessionSort,
277 ToggleSessionNamedFilter,
278 RenameSession,
279 DeleteSession,
280 DeleteSessionNoninvasive,
281}
282
283impl AppAction {
284 #[must_use]
286 pub const fn display_name(&self) -> &'static str {
287 match self {
288 Self::CursorUp => "Move cursor up",
290 Self::CursorDown => "Move cursor down",
291 Self::CursorLeft => "Move cursor left",
292 Self::CursorRight => "Move cursor right",
293 Self::CursorWordLeft => "Move cursor word left",
294 Self::CursorWordRight => "Move cursor word right",
295 Self::CursorLineStart => "Move to line start",
296 Self::CursorLineEnd => "Move to line end",
297 Self::JumpForward => "Jump forward to character",
298 Self::JumpBackward => "Jump backward to character",
299 Self::PageUp => "Scroll up by page",
300 Self::PageDown => "Scroll down by page",
301
302 Self::DeleteCharBackward => "Delete character backward",
304 Self::DeleteCharForward => "Delete character forward",
305 Self::DeleteWordBackward => "Delete word backward",
306 Self::DeleteWordForward => "Delete word forward",
307 Self::DeleteToLineStart => "Delete to line start",
308 Self::DeleteToLineEnd => "Delete to line end",
309
310 Self::NewLine => "Insert new line",
312 Self::Submit => "Submit input",
313 Self::Tab => "Tab / autocomplete",
314
315 Self::Yank => "Paste most recently deleted text",
317 Self::YankPop => "Cycle through deleted text after yank",
318 Self::Undo => "Undo last edit",
319
320 Self::Copy => "Copy selection",
322 Self::PasteImage => "Paste image from clipboard",
323
324 Self::Interrupt => "Cancel / abort",
326 Self::Clear => "Clear editor",
327 Self::Exit => "Exit (when editor empty)",
328 Self::Suspend => "Suspend to background",
329 Self::ExternalEditor => "Open in external editor",
330 Self::Help => "Show help",
331 Self::OpenSettings => "Open settings",
332
333 Self::NewSession => "Start a new session",
335 Self::Tree => "Open session tree navigator",
336 Self::Fork => "Fork current session",
337 Self::BranchPicker => "Open branch picker",
338 Self::BranchNextSibling => "Switch to next sibling branch",
339 Self::BranchPrevSibling => "Switch to previous sibling branch",
340
341 Self::SelectModel => "Open model selector",
343 Self::CycleModelForward => "Cycle to next model",
344 Self::CycleModelBackward => "Cycle to previous model",
345 Self::CycleThinkingLevel => "Cycle thinking level",
346
347 Self::ExpandTools => "Collapse/expand tool output",
349 Self::ToggleThinking => "Collapse/expand thinking blocks",
350
351 Self::FollowUp => "Queue follow-up message",
353 Self::Dequeue => "Restore queued messages to editor",
354
355 Self::SelectUp => "Move selection up",
357 Self::SelectDown => "Move selection down",
358 Self::SelectPageUp => "Page up in list",
359 Self::SelectPageDown => "Page down in list",
360 Self::SelectConfirm => "Confirm selection",
361 Self::SelectCancel => "Cancel selection",
362
363 Self::ToggleSessionPath => "Toggle path display",
365 Self::ToggleSessionSort => "Toggle sort mode",
366 Self::ToggleSessionNamedFilter => "Toggle named-only filter",
367 Self::RenameSession => "Rename session",
368 Self::DeleteSession => "Delete session",
369 Self::DeleteSessionNoninvasive => "Delete session (when query empty)",
370 }
371 }
372
373 #[must_use]
375 pub const fn category(&self) -> ActionCategory {
376 match self {
377 Self::CursorUp
378 | Self::CursorDown
379 | Self::CursorLeft
380 | Self::CursorRight
381 | Self::CursorWordLeft
382 | Self::CursorWordRight
383 | Self::CursorLineStart
384 | Self::CursorLineEnd
385 | Self::JumpForward
386 | Self::JumpBackward
387 | Self::PageUp
388 | Self::PageDown => ActionCategory::CursorMovement,
389
390 Self::DeleteCharBackward
391 | Self::DeleteCharForward
392 | Self::DeleteWordBackward
393 | Self::DeleteWordForward
394 | Self::DeleteToLineStart
395 | Self::DeleteToLineEnd => ActionCategory::Deletion,
396
397 Self::NewLine | Self::Submit | Self::Tab => ActionCategory::TextInput,
398
399 Self::Yank | Self::YankPop | Self::Undo => ActionCategory::KillRing,
400
401 Self::Copy | Self::PasteImage => ActionCategory::Clipboard,
402
403 Self::Interrupt
404 | Self::Clear
405 | Self::Exit
406 | Self::Suspend
407 | Self::ExternalEditor
408 | Self::Help
409 | Self::OpenSettings => ActionCategory::Application,
410
411 Self::NewSession
412 | Self::Tree
413 | Self::Fork
414 | Self::BranchPicker
415 | Self::BranchNextSibling
416 | Self::BranchPrevSibling => ActionCategory::Session,
417
418 Self::SelectModel
419 | Self::CycleModelForward
420 | Self::CycleModelBackward
421 | Self::CycleThinkingLevel => ActionCategory::ModelsThinking,
422
423 Self::ExpandTools | Self::ToggleThinking => ActionCategory::Display,
424
425 Self::FollowUp | Self::Dequeue => ActionCategory::MessageQueue,
426
427 Self::SelectUp
428 | Self::SelectDown
429 | Self::SelectPageUp
430 | Self::SelectPageDown
431 | Self::SelectConfirm
432 | Self::SelectCancel => ActionCategory::Selection,
433
434 Self::ToggleSessionPath
435 | Self::ToggleSessionSort
436 | Self::ToggleSessionNamedFilter
437 | Self::RenameSession
438 | Self::DeleteSession
439 | Self::DeleteSessionNoninvasive => ActionCategory::SessionPicker,
440 }
441 }
442
443 #[must_use]
445 pub fn in_category(category: ActionCategory) -> Vec<Self> {
446 Self::all()
447 .iter()
448 .copied()
449 .filter(|a| a.category() == category)
450 .collect()
451 }
452
453 #[must_use]
455 pub const fn all() -> &'static [Self] {
456 &[
457 Self::CursorUp,
459 Self::CursorDown,
460 Self::CursorLeft,
461 Self::CursorRight,
462 Self::CursorWordLeft,
463 Self::CursorWordRight,
464 Self::CursorLineStart,
465 Self::CursorLineEnd,
466 Self::JumpForward,
467 Self::JumpBackward,
468 Self::PageUp,
469 Self::PageDown,
470 Self::DeleteCharBackward,
472 Self::DeleteCharForward,
473 Self::DeleteWordBackward,
474 Self::DeleteWordForward,
475 Self::DeleteToLineStart,
476 Self::DeleteToLineEnd,
477 Self::NewLine,
479 Self::Submit,
480 Self::Tab,
481 Self::Yank,
483 Self::YankPop,
484 Self::Undo,
485 Self::Copy,
487 Self::PasteImage,
488 Self::Interrupt,
490 Self::Clear,
491 Self::Exit,
492 Self::Suspend,
493 Self::ExternalEditor,
494 Self::Help,
495 Self::OpenSettings,
496 Self::NewSession,
498 Self::Tree,
499 Self::Fork,
500 Self::BranchPicker,
501 Self::BranchNextSibling,
502 Self::BranchPrevSibling,
503 Self::SelectModel,
505 Self::CycleModelForward,
506 Self::CycleModelBackward,
507 Self::CycleThinkingLevel,
508 Self::ExpandTools,
510 Self::ToggleThinking,
511 Self::FollowUp,
513 Self::Dequeue,
514 Self::SelectUp,
516 Self::SelectDown,
517 Self::SelectPageUp,
518 Self::SelectPageDown,
519 Self::SelectConfirm,
520 Self::SelectCancel,
521 Self::ToggleSessionPath,
523 Self::ToggleSessionSort,
524 Self::ToggleSessionNamedFilter,
525 Self::RenameSession,
526 Self::DeleteSession,
527 Self::DeleteSessionNoninvasive,
528 ]
529 }
530}
531
532impl fmt::Display for AppAction {
533 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
534 write!(
536 f,
537 "{}",
538 serde_json::to_string(self)
539 .unwrap_or_default()
540 .trim_matches('"')
541 )
542 }
543}
544
545#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
551pub struct KeyModifiers {
552 pub ctrl: bool,
553 pub shift: bool,
554 pub alt: bool,
555}
556
557impl KeyModifiers {
558 pub const NONE: Self = Self {
560 ctrl: false,
561 shift: false,
562 alt: false,
563 };
564
565 pub const CTRL: Self = Self {
567 ctrl: true,
568 shift: false,
569 alt: false,
570 };
571
572 pub const SHIFT: Self = Self {
574 ctrl: false,
575 shift: true,
576 alt: false,
577 };
578
579 pub const ALT: Self = Self {
581 ctrl: false,
582 shift: false,
583 alt: true,
584 };
585
586 pub const CTRL_SHIFT: Self = Self {
588 ctrl: true,
589 shift: true,
590 alt: false,
591 };
592
593 pub const CTRL_ALT: Self = Self {
595 ctrl: true,
596 shift: false,
597 alt: true,
598 };
599
600 pub const ALT_SHIFT: Self = Self {
602 ctrl: false,
603 shift: true,
604 alt: true,
605 };
606}
607
608#[derive(Debug, Clone, PartialEq, Eq, Hash)]
614pub struct KeyBinding {
615 pub key: String,
616 pub modifiers: KeyModifiers,
617}
618
619impl KeyBinding {
620 #[must_use]
622 pub fn new(key: impl Into<String>, modifiers: KeyModifiers) -> Self {
623 Self {
624 key: key.into(),
625 modifiers,
626 }
627 }
628
629 #[must_use]
631 pub fn plain(key: impl Into<String>) -> Self {
632 Self::new(key, KeyModifiers::NONE)
633 }
634
635 #[must_use]
637 pub fn ctrl(key: impl Into<String>) -> Self {
638 Self::new(key, KeyModifiers::CTRL)
639 }
640
641 #[must_use]
643 pub fn alt(key: impl Into<String>) -> Self {
644 Self::new(key, KeyModifiers::ALT)
645 }
646
647 #[must_use]
649 pub fn shift(key: impl Into<String>) -> Self {
650 Self::new(key, KeyModifiers::SHIFT)
651 }
652
653 #[must_use]
655 pub fn ctrl_shift(key: impl Into<String>) -> Self {
656 Self::new(key, KeyModifiers::CTRL_SHIFT)
657 }
658
659 #[must_use]
661 pub fn ctrl_alt(key: impl Into<String>) -> Self {
662 Self::new(key, KeyModifiers::CTRL_ALT)
663 }
664
665 #[allow(clippy::too_many_lines)]
670 #[must_use]
671 pub fn from_bubbletea_key(key: &bubbletea::KeyMsg) -> Option<Self> {
672 use bubbletea::KeyType;
673
674 if key.paste {
676 return None;
677 }
678
679 let (key_name, mut modifiers) = match key.key_type {
680 KeyType::Null => ("@", KeyModifiers::CTRL),
682 KeyType::CtrlA => ("a", KeyModifiers::CTRL),
683 KeyType::CtrlB => ("b", KeyModifiers::CTRL),
684 KeyType::CtrlC => ("c", KeyModifiers::CTRL),
685 KeyType::CtrlD => ("d", KeyModifiers::CTRL),
686 KeyType::CtrlE => ("e", KeyModifiers::CTRL),
687 KeyType::CtrlF => ("f", KeyModifiers::CTRL),
688 KeyType::CtrlG => ("g", KeyModifiers::CTRL),
689 KeyType::CtrlH => ("h", KeyModifiers::CTRL),
690 KeyType::Tab => ("tab", KeyModifiers::NONE),
691 KeyType::CtrlJ => ("j", KeyModifiers::CTRL),
692 KeyType::CtrlK => ("k", KeyModifiers::CTRL),
693 KeyType::CtrlL => ("l", KeyModifiers::CTRL),
694 KeyType::Enter => ("enter", KeyModifiers::NONE),
695 KeyType::ShiftEnter => ("enter", KeyModifiers::SHIFT),
696 KeyType::CtrlEnter => ("enter", KeyModifiers::CTRL),
697 KeyType::CtrlShiftEnter => ("enter", KeyModifiers::CTRL_SHIFT),
698 KeyType::CtrlN => ("n", KeyModifiers::CTRL),
699 KeyType::CtrlO => ("o", KeyModifiers::CTRL),
700 KeyType::CtrlP => ("p", KeyModifiers::CTRL),
701 KeyType::CtrlQ => ("q", KeyModifiers::CTRL),
702 KeyType::CtrlR => ("r", KeyModifiers::CTRL),
703 KeyType::CtrlS => ("s", KeyModifiers::CTRL),
704 KeyType::CtrlT => ("t", KeyModifiers::CTRL),
705 KeyType::CtrlU => ("u", KeyModifiers::CTRL),
706 KeyType::CtrlV => ("v", KeyModifiers::CTRL),
707 KeyType::CtrlW => ("w", KeyModifiers::CTRL),
708 KeyType::CtrlX => ("x", KeyModifiers::CTRL),
709 KeyType::CtrlY => ("y", KeyModifiers::CTRL),
710 KeyType::CtrlZ => ("z", KeyModifiers::CTRL),
711 KeyType::Esc => ("escape", KeyModifiers::NONE),
712 KeyType::CtrlBackslash => ("\\", KeyModifiers::CTRL),
713 KeyType::CtrlCloseBracket => ("]", KeyModifiers::CTRL),
714 KeyType::CtrlCaret => ("^", KeyModifiers::CTRL),
715 KeyType::CtrlUnderscore => ("_", KeyModifiers::CTRL),
716 KeyType::Backspace => ("backspace", KeyModifiers::NONE),
717
718 KeyType::Up => ("up", KeyModifiers::NONE),
720 KeyType::Down => ("down", KeyModifiers::NONE),
721 KeyType::Left => ("left", KeyModifiers::NONE),
722 KeyType::Right => ("right", KeyModifiers::NONE),
723
724 KeyType::ShiftTab => ("tab", KeyModifiers::SHIFT),
726 KeyType::ShiftUp => ("up", KeyModifiers::SHIFT),
727 KeyType::ShiftDown => ("down", KeyModifiers::SHIFT),
728 KeyType::ShiftLeft => ("left", KeyModifiers::SHIFT),
729 KeyType::ShiftRight => ("right", KeyModifiers::SHIFT),
730 KeyType::ShiftHome => ("home", KeyModifiers::SHIFT),
731 KeyType::ShiftEnd => ("end", KeyModifiers::SHIFT),
732
733 KeyType::CtrlUp => ("up", KeyModifiers::CTRL),
735 KeyType::CtrlDown => ("down", KeyModifiers::CTRL),
736 KeyType::CtrlLeft => ("left", KeyModifiers::CTRL),
737 KeyType::CtrlRight => ("right", KeyModifiers::CTRL),
738 KeyType::CtrlHome => ("home", KeyModifiers::CTRL),
739 KeyType::CtrlEnd => ("end", KeyModifiers::CTRL),
740 KeyType::CtrlPgUp => ("pageup", KeyModifiers::CTRL),
741 KeyType::CtrlPgDown => ("pagedown", KeyModifiers::CTRL),
742
743 KeyType::CtrlShiftUp => ("up", KeyModifiers::CTRL_SHIFT),
745 KeyType::CtrlShiftDown => ("down", KeyModifiers::CTRL_SHIFT),
746 KeyType::CtrlShiftLeft => ("left", KeyModifiers::CTRL_SHIFT),
747 KeyType::CtrlShiftRight => ("right", KeyModifiers::CTRL_SHIFT),
748 KeyType::CtrlShiftHome => ("home", KeyModifiers::CTRL_SHIFT),
749 KeyType::CtrlShiftEnd => ("end", KeyModifiers::CTRL_SHIFT),
750
751 KeyType::Home => ("home", KeyModifiers::NONE),
753 KeyType::End => ("end", KeyModifiers::NONE),
754 KeyType::PgUp => ("pageup", KeyModifiers::NONE),
755 KeyType::PgDown => ("pagedown", KeyModifiers::NONE),
756 KeyType::Delete => ("delete", KeyModifiers::NONE),
757 KeyType::Insert => ("insert", KeyModifiers::NONE),
758 KeyType::Space => ("space", KeyModifiers::NONE),
759
760 KeyType::F1 => ("f1", KeyModifiers::NONE),
762 KeyType::F2 => ("f2", KeyModifiers::NONE),
763 KeyType::F3 => ("f3", KeyModifiers::NONE),
764 KeyType::F4 => ("f4", KeyModifiers::NONE),
765 KeyType::F5 => ("f5", KeyModifiers::NONE),
766 KeyType::F6 => ("f6", KeyModifiers::NONE),
767 KeyType::F7 => ("f7", KeyModifiers::NONE),
768 KeyType::F8 => ("f8", KeyModifiers::NONE),
769 KeyType::F9 => ("f9", KeyModifiers::NONE),
770 KeyType::F10 => ("f10", KeyModifiers::NONE),
771 KeyType::F11 => ("f11", KeyModifiers::NONE),
772 KeyType::F12 => ("f12", KeyModifiers::NONE),
773 KeyType::F13 => ("f13", KeyModifiers::NONE),
774 KeyType::F14 => ("f14", KeyModifiers::NONE),
775 KeyType::F15 => ("f15", KeyModifiers::NONE),
776 KeyType::F16 => ("f16", KeyModifiers::NONE),
777 KeyType::F17 => ("f17", KeyModifiers::NONE),
778 KeyType::F18 => ("f18", KeyModifiers::NONE),
779 KeyType::F19 => ("f19", KeyModifiers::NONE),
780 KeyType::F20 => ("f20", KeyModifiers::NONE),
781
782 KeyType::Runes => {
784 if key.runes.len() != 1 {
786 return None;
787 }
788 let c = key.runes[0];
789 return Some(Self {
792 key: c.to_lowercase().to_string(),
793 modifiers: if key.alt {
794 KeyModifiers::ALT
795 } else {
796 KeyModifiers::NONE
797 },
798 });
799 }
800 };
801
802 if key.alt {
804 modifiers.alt = true;
805 }
806
807 Some(Self {
808 key: key_name.to_string(),
809 modifiers,
810 })
811 }
812}
813
814impl fmt::Display for KeyBinding {
815 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
816 let mut parts = Vec::new();
817 if self.modifiers.ctrl {
818 parts.push("ctrl");
819 }
820 if self.modifiers.alt {
821 parts.push("alt");
822 }
823 if self.modifiers.shift {
824 parts.push("shift");
825 }
826 parts.push(&self.key);
827 write!(f, "{}", parts.join("+"))
828 }
829}
830
831impl FromStr for KeyBinding {
832 type Err = KeyBindingParseError;
833
834 fn from_str(s: &str) -> Result<Self, Self::Err> {
835 parse_key_binding(s)
836 }
837}
838
839#[derive(Debug, Clone, PartialEq, Eq)]
841pub enum KeyBindingParseError {
842 Empty,
844 NoKey,
846 MultipleKeys { binding: String },
848 DuplicateModifier { modifier: String, binding: String },
850 UnknownModifier { modifier: String, binding: String },
852 UnknownKey { key: String, binding: String },
854}
855
856impl fmt::Display for KeyBindingParseError {
857 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
858 match self {
859 Self::Empty => write!(f, "Empty key binding"),
860 Self::NoKey => write!(f, "No key in binding (only modifiers)"),
861 Self::MultipleKeys { binding } => write!(f, "Multiple keys in binding: {binding}"),
862 Self::DuplicateModifier { modifier, binding } => {
863 write!(f, "Duplicate modifier '{modifier}' in binding: {binding}")
864 }
865 Self::UnknownModifier { modifier, binding } => {
866 write!(f, "Unknown modifier '{modifier}' in binding: {binding}")
867 }
868 Self::UnknownKey { key, binding } => {
869 write!(f, "Unknown key '{key}' in binding: {binding}")
870 }
871 }
872 }
873}
874
875impl std::error::Error for KeyBindingParseError {}
876
877fn normalize_key_name(key: &str) -> Option<String> {
881 let lower = key.to_lowercase();
882
883 let canonical = match lower.as_str() {
885 "esc" => "escape",
887 "return" => "enter",
888
889 "escape" | "enter" | "tab" | "space" | "backspace" | "delete" | "insert" | "clear"
891 | "home" | "end" | "pageup" | "pagedown" | "up" | "down" | "left" | "right" | "f1"
892 | "f2" | "f3" | "f4" | "f5" | "f6" | "f7" | "f8" | "f9" | "f10" | "f11" | "f12" | "f13"
893 | "f14" | "f15" | "f16" | "f17" | "f18" | "f19" | "f20" => &lower,
894
895 s if s.len() == 1 && s.chars().next().is_some_and(|c| c.is_ascii_lowercase()) => &lower,
897
898 "`" | "-" | "=" | "[" | "]" | "\\" | ";" | "'" | "," | "." | "/" | "!" | "@" | "#"
900 | "$" | "%" | "^" | "&" | "*" | "(" | ")" | "_" | "+" | "|" | "~" | "{" | "}" | ":"
901 | "<" | ">" | "?" | "\"" => &lower,
902
903 _ => return None,
905 };
906
907 Some(canonical.to_string())
908}
909
910fn parse_key_binding(s: &str) -> Result<KeyBinding, KeyBindingParseError> {
927 let binding = s.trim();
928 if binding.is_empty() {
929 return Err(KeyBindingParseError::Empty);
930 }
931
932 let compacted = binding
934 .chars()
935 .filter(|c| !c.is_whitespace())
936 .collect::<String>();
937 let normalized = compacted.to_lowercase();
938 let mut rest = normalized.as_str();
939
940 let mut ctrl_seen = false;
941 let mut alt_seen = false;
942 let mut shift_seen = false;
943
944 loop {
946 if let Some(after) = rest.strip_prefix("ctrl+") {
947 if ctrl_seen {
948 return Err(KeyBindingParseError::DuplicateModifier {
949 modifier: "ctrl".to_string(),
950 binding: binding.to_string(),
951 });
952 }
953 ctrl_seen = true;
954 rest = after;
955 continue;
956 }
957 if let Some(after) = rest.strip_prefix("control+") {
958 if ctrl_seen {
959 return Err(KeyBindingParseError::DuplicateModifier {
960 modifier: "ctrl".to_string(),
961 binding: binding.to_string(),
962 });
963 }
964 ctrl_seen = true;
965 rest = after;
966 continue;
967 }
968 if let Some(after) = rest.strip_prefix("alt+") {
969 if alt_seen {
970 return Err(KeyBindingParseError::DuplicateModifier {
971 modifier: "alt".to_string(),
972 binding: binding.to_string(),
973 });
974 }
975 alt_seen = true;
976 rest = after;
977 continue;
978 }
979 if let Some(after) = rest.strip_prefix("shift+") {
980 if shift_seen {
981 return Err(KeyBindingParseError::DuplicateModifier {
982 modifier: "shift".to_string(),
983 binding: binding.to_string(),
984 });
985 }
986 shift_seen = true;
987 rest = after;
988 continue;
989 }
990 break;
991 }
992
993 if rest.is_empty() {
994 return Err(KeyBindingParseError::NoKey);
995 }
996
997 if matches!(rest, "ctrl" | "control" | "alt" | "shift") {
999 return Err(KeyBindingParseError::NoKey);
1000 }
1001
1002 if rest.contains('+') && rest != "+" {
1006 let first = rest.split('+').next().unwrap_or("");
1007 if first.is_empty() || normalize_key_name(first).is_some() {
1008 return Err(KeyBindingParseError::MultipleKeys {
1009 binding: binding.to_string(),
1010 });
1011 }
1012 return Err(KeyBindingParseError::UnknownModifier {
1013 modifier: first.to_string(),
1014 binding: binding.to_string(),
1015 });
1016 }
1017
1018 let key = normalize_key_name(rest).ok_or_else(|| KeyBindingParseError::UnknownKey {
1019 key: rest.to_string(),
1020 binding: binding.to_string(),
1021 })?;
1022
1023 Ok(KeyBinding {
1024 key,
1025 modifiers: KeyModifiers {
1026 ctrl: ctrl_seen,
1027 shift: shift_seen,
1028 alt: alt_seen,
1029 },
1030 })
1031}
1032
1033#[must_use]
1035pub fn is_valid_key(s: &str) -> bool {
1036 parse_key_binding(s).is_ok()
1037}
1038
1039impl Serialize for KeyBinding {
1040 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1041 where
1042 S: serde::Serializer,
1043 {
1044 serializer.serialize_str(&self.to_string())
1045 }
1046}
1047
1048impl<'de> Deserialize<'de> for KeyBinding {
1049 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1050 where
1051 D: serde::Deserializer<'de>,
1052 {
1053 let s = String::deserialize(deserializer)?;
1054 s.parse().map_err(serde::de::Error::custom)
1055 }
1056}
1057
1058#[derive(Debug, Clone)]
1064pub struct KeyBindings {
1065 bindings: HashMap<AppAction, Vec<KeyBinding>>,
1067 reverse: HashMap<KeyBinding, AppAction>,
1069}
1070
1071impl KeyBindings {
1072 #[must_use]
1074 pub fn new() -> Self {
1075 let bindings = Self::default_bindings();
1076 let reverse = Self::build_reverse_map(&bindings);
1077 Self { bindings, reverse }
1078 }
1079
1080 pub fn load(path: &Path) -> Result<Self, std::io::Error> {
1082 let content = std::fs::read_to_string(path)?;
1083 let overrides: HashMap<AppAction, Vec<KeyBinding>> = serde_json::from_str(&content)
1084 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
1085
1086 let mut bindings = Self::default_bindings();
1087 for (action, keys) in overrides {
1088 bindings.insert(action, keys);
1089 }
1090
1091 let reverse = Self::build_reverse_map(&bindings);
1092 Ok(Self { bindings, reverse })
1093 }
1094
1095 #[must_use]
1097 pub fn user_config_path() -> std::path::PathBuf {
1098 crate::config::Config::global_dir().join("keybindings.json")
1099 }
1100
1101 #[must_use]
1118 pub fn load_from_user_config() -> KeyBindingsLoadResult {
1119 let path = Self::user_config_path();
1120 Self::load_from_path_with_diagnostics(&path)
1121 }
1122
1123 #[must_use]
1131 pub fn load_from_path_with_diagnostics(path: &Path) -> KeyBindingsLoadResult {
1132 let mut warnings = Vec::new();
1133
1134 if !path.exists() {
1136 return KeyBindingsLoadResult {
1137 bindings: Self::new(),
1138 path: path.to_path_buf(),
1139 warnings,
1140 };
1141 }
1142
1143 let content = match std::fs::read_to_string(path) {
1145 Ok(c) => c,
1146 Err(e) => {
1147 warnings.push(KeyBindingsWarning::ReadError {
1148 path: path.to_path_buf(),
1149 error: e.to_string(),
1150 });
1151 return KeyBindingsLoadResult {
1152 bindings: Self::new(),
1153 path: path.to_path_buf(),
1154 warnings,
1155 };
1156 }
1157 };
1158
1159 let raw: HashMap<String, serde_json::Value> = match serde_json::from_str(&content) {
1161 Ok(v) => v,
1162 Err(e) => {
1163 warnings.push(KeyBindingsWarning::ParseError {
1164 path: path.to_path_buf(),
1165 error: e.to_string(),
1166 });
1167 return KeyBindingsLoadResult {
1168 bindings: Self::new(),
1169 path: path.to_path_buf(),
1170 warnings,
1171 };
1172 }
1173 };
1174
1175 let mut bindings = Self::default_bindings();
1177
1178 for (action_str, value) in raw {
1180 let action: AppAction =
1182 if let Ok(a) = serde_json::from_value(serde_json::json!(action_str)) {
1183 a
1184 } else {
1185 warnings.push(KeyBindingsWarning::UnknownAction {
1186 action: action_str,
1187 path: path.to_path_buf(),
1188 });
1189 continue;
1190 };
1191
1192 let key_strings: Vec<String> = match value {
1194 serde_json::Value::String(s) => vec![s],
1195 serde_json::Value::Array(arr) => {
1196 let mut keys = Vec::new();
1197 for (idx, v) in arr.into_iter().enumerate() {
1198 match v {
1199 serde_json::Value::String(s) => keys.push(s),
1200 _ => {
1201 warnings.push(KeyBindingsWarning::InvalidKeyValue {
1202 action: action.to_string(),
1203 index: idx,
1204 path: path.to_path_buf(),
1205 });
1206 }
1207 }
1208 }
1209 keys
1210 }
1211 _ => {
1212 warnings.push(KeyBindingsWarning::InvalidKeyValue {
1213 action: action.to_string(),
1214 index: 0,
1215 path: path.to_path_buf(),
1216 });
1217 continue;
1218 }
1219 };
1220
1221 let mut parsed_keys = Vec::new();
1223 for key_str in key_strings {
1224 match key_str.parse::<KeyBinding>() {
1225 Ok(binding) => parsed_keys.push(binding),
1226 Err(e) => {
1227 warnings.push(KeyBindingsWarning::InvalidKey {
1228 action: action.to_string(),
1229 key: key_str,
1230 error: e.to_string(),
1231 path: path.to_path_buf(),
1232 });
1233 }
1234 }
1235 }
1236
1237 if !parsed_keys.is_empty() {
1239 bindings.insert(action, parsed_keys);
1240 }
1241 }
1242
1243 let reverse = Self::build_reverse_map(&bindings);
1244 KeyBindingsLoadResult {
1245 bindings: Self { bindings, reverse },
1246 path: path.to_path_buf(),
1247 warnings,
1248 }
1249 }
1250
1251 #[must_use]
1253 pub fn lookup(&self, binding: &KeyBinding) -> Option<AppAction> {
1254 self.reverse.get(binding).copied()
1255 }
1256
1257 #[must_use]
1262 pub fn matching_actions(&self, binding: &KeyBinding) -> Vec<AppAction> {
1263 AppAction::all()
1264 .iter()
1265 .copied()
1266 .filter(|&action| self.get_bindings(action).contains(binding))
1267 .collect()
1268 }
1269
1270 #[must_use]
1272 pub fn get_bindings(&self, action: AppAction) -> &[KeyBinding] {
1273 self.bindings.get(&action).map_or(&[], Vec::as_slice)
1274 }
1275
1276 pub fn iter(&self) -> impl Iterator<Item = (AppAction, &[KeyBinding])> {
1278 AppAction::all()
1279 .iter()
1280 .map(|&action| (action, self.get_bindings(action)))
1281 }
1282
1283 pub fn iter_category(
1285 &self,
1286 category: ActionCategory,
1287 ) -> impl Iterator<Item = (AppAction, &[KeyBinding])> {
1288 AppAction::in_category(category)
1289 .into_iter()
1290 .map(|action| (action, self.get_bindings(action)))
1291 }
1292
1293 fn build_reverse_map(
1294 bindings: &HashMap<AppAction, Vec<KeyBinding>>,
1295 ) -> HashMap<KeyBinding, AppAction> {
1296 let mut reverse = HashMap::new();
1297 for &action in AppAction::all() {
1301 let Some(keys) = bindings.get(&action) else {
1302 continue;
1303 };
1304 for key in keys {
1305 reverse.entry(key.clone()).or_insert(action);
1306 }
1307 }
1308 reverse
1309 }
1310
1311 #[allow(clippy::too_many_lines)]
1313 fn default_bindings() -> HashMap<AppAction, Vec<KeyBinding>> {
1314 let mut m = HashMap::new();
1315
1316 m.insert(AppAction::CursorUp, vec![KeyBinding::plain("up")]);
1318 m.insert(AppAction::CursorDown, vec![KeyBinding::plain("down")]);
1319 m.insert(
1320 AppAction::CursorLeft,
1321 vec![KeyBinding::plain("left"), KeyBinding::ctrl("b")],
1322 );
1323 m.insert(
1324 AppAction::CursorRight,
1325 vec![KeyBinding::plain("right"), KeyBinding::ctrl("f")],
1326 );
1327 m.insert(
1328 AppAction::CursorWordLeft,
1329 vec![
1330 KeyBinding::alt("left"),
1331 KeyBinding::ctrl("left"),
1332 KeyBinding::alt("b"),
1333 ],
1334 );
1335 m.insert(
1336 AppAction::CursorWordRight,
1337 vec![
1338 KeyBinding::alt("right"),
1339 KeyBinding::ctrl("right"),
1340 KeyBinding::alt("f"),
1341 ],
1342 );
1343 m.insert(
1344 AppAction::CursorLineStart,
1345 vec![KeyBinding::plain("home"), KeyBinding::ctrl("a")],
1346 );
1347 m.insert(
1348 AppAction::CursorLineEnd,
1349 vec![KeyBinding::plain("end"), KeyBinding::ctrl("e")],
1350 );
1351 m.insert(AppAction::JumpForward, vec![KeyBinding::ctrl("]")]);
1352 m.insert(AppAction::JumpBackward, vec![KeyBinding::ctrl_alt("]")]);
1353 m.insert(AppAction::PageUp, vec![KeyBinding::plain("pageup")]);
1354 m.insert(AppAction::PageDown, vec![KeyBinding::plain("pagedown")]);
1355
1356 m.insert(
1358 AppAction::DeleteCharBackward,
1359 vec![KeyBinding::plain("backspace")],
1360 );
1361 m.insert(
1362 AppAction::DeleteCharForward,
1363 vec![KeyBinding::plain("delete"), KeyBinding::ctrl("d")],
1364 );
1365 m.insert(
1366 AppAction::DeleteWordBackward,
1367 vec![KeyBinding::ctrl("w"), KeyBinding::alt("backspace")],
1368 );
1369 m.insert(
1370 AppAction::DeleteWordForward,
1371 vec![KeyBinding::alt("d"), KeyBinding::alt("delete")],
1372 );
1373 m.insert(AppAction::DeleteToLineStart, vec![KeyBinding::ctrl("u")]);
1374 m.insert(AppAction::DeleteToLineEnd, vec![KeyBinding::ctrl("k")]);
1375
1376 m.insert(
1378 AppAction::NewLine,
1379 vec![KeyBinding::shift("enter"), KeyBinding::ctrl("enter")],
1380 );
1381 m.insert(AppAction::Submit, vec![KeyBinding::plain("enter")]);
1382 m.insert(AppAction::Tab, vec![KeyBinding::plain("tab")]);
1383
1384 m.insert(AppAction::Yank, vec![KeyBinding::ctrl("y")]);
1386 m.insert(AppAction::YankPop, vec![KeyBinding::alt("y")]);
1387 m.insert(AppAction::Undo, vec![KeyBinding::ctrl("-")]);
1388
1389 m.insert(AppAction::Copy, vec![KeyBinding::ctrl("c")]);
1391 m.insert(AppAction::PasteImage, vec![KeyBinding::ctrl("v")]);
1392
1393 m.insert(AppAction::Interrupt, vec![KeyBinding::plain("escape")]);
1395 m.insert(AppAction::Clear, vec![KeyBinding::ctrl("c")]);
1396 m.insert(AppAction::Exit, vec![KeyBinding::ctrl("d")]);
1397 m.insert(AppAction::Suspend, vec![KeyBinding::ctrl("z")]);
1398 m.insert(AppAction::ExternalEditor, vec![KeyBinding::ctrl("g")]);
1399 m.insert(AppAction::Help, vec![KeyBinding::plain("f1")]);
1400 m.insert(AppAction::OpenSettings, vec![KeyBinding::plain("f2")]);
1401
1402 m.insert(AppAction::NewSession, vec![]);
1404 m.insert(AppAction::Tree, vec![]);
1405 m.insert(AppAction::Fork, vec![]);
1406 m.insert(AppAction::BranchPicker, vec![]);
1407 m.insert(
1408 AppAction::BranchNextSibling,
1409 vec![KeyBinding::ctrl_shift("right")],
1410 );
1411 m.insert(
1412 AppAction::BranchPrevSibling,
1413 vec![KeyBinding::ctrl_shift("left")],
1414 );
1415
1416 m.insert(AppAction::SelectModel, vec![KeyBinding::ctrl("l")]);
1418 m.insert(AppAction::CycleModelForward, vec![KeyBinding::ctrl("p")]);
1419 m.insert(
1420 AppAction::CycleModelBackward,
1421 vec![KeyBinding::ctrl_shift("p")],
1422 );
1423 m.insert(
1424 AppAction::CycleThinkingLevel,
1425 vec![KeyBinding::shift("tab")],
1426 );
1427
1428 m.insert(AppAction::ExpandTools, vec![KeyBinding::ctrl("o")]);
1430 m.insert(AppAction::ToggleThinking, vec![KeyBinding::ctrl("t")]);
1431
1432 m.insert(AppAction::FollowUp, vec![KeyBinding::alt("enter")]);
1434 m.insert(AppAction::Dequeue, vec![KeyBinding::alt("up")]);
1435
1436 m.insert(AppAction::SelectUp, vec![KeyBinding::plain("up")]);
1438 m.insert(AppAction::SelectDown, vec![KeyBinding::plain("down")]);
1439 m.insert(AppAction::SelectPageUp, vec![KeyBinding::plain("pageup")]);
1440 m.insert(
1441 AppAction::SelectPageDown,
1442 vec![KeyBinding::plain("pagedown")],
1443 );
1444 m.insert(AppAction::SelectConfirm, vec![KeyBinding::plain("enter")]);
1445 m.insert(
1446 AppAction::SelectCancel,
1447 vec![KeyBinding::plain("escape"), KeyBinding::ctrl("c")],
1448 );
1449
1450 m.insert(AppAction::ToggleSessionPath, vec![KeyBinding::ctrl("p")]);
1452 m.insert(AppAction::ToggleSessionSort, vec![KeyBinding::ctrl("s")]);
1453 m.insert(
1454 AppAction::ToggleSessionNamedFilter,
1455 vec![KeyBinding::ctrl("n")],
1456 );
1457 m.insert(AppAction::RenameSession, vec![KeyBinding::ctrl("r")]);
1458 m.insert(AppAction::DeleteSession, vec![KeyBinding::ctrl("d")]);
1459 m.insert(
1460 AppAction::DeleteSessionNoninvasive,
1461 vec![KeyBinding::ctrl("backspace")],
1462 );
1463
1464 m
1465 }
1466}
1467
1468impl Default for KeyBindings {
1469 fn default() -> Self {
1470 Self::new()
1471 }
1472}
1473
1474#[cfg(test)]
1479mod tests {
1480 use super::*;
1481
1482 #[test]
1483 fn test_key_binding_parse() {
1484 let binding: KeyBinding = "ctrl+a".parse().unwrap();
1485 assert_eq!(binding.key, "a");
1486 assert!(binding.modifiers.ctrl);
1487 assert!(!binding.modifiers.alt);
1488 assert!(!binding.modifiers.shift);
1489
1490 let binding: KeyBinding = "alt+shift+f".parse().unwrap();
1491 assert_eq!(binding.key, "f");
1492 assert!(!binding.modifiers.ctrl);
1493 assert!(binding.modifiers.alt);
1494 assert!(binding.modifiers.shift);
1495
1496 let binding: KeyBinding = "enter".parse().unwrap();
1497 assert_eq!(binding.key, "enter");
1498 assert!(!binding.modifiers.ctrl);
1499 assert!(!binding.modifiers.alt);
1500 assert!(!binding.modifiers.shift);
1501 }
1502
1503 #[test]
1504 fn test_key_binding_display() {
1505 let binding = KeyBinding::ctrl("a");
1506 assert_eq!(binding.to_string(), "ctrl+a");
1507
1508 let binding = KeyBinding::new("f", KeyModifiers::ALT_SHIFT);
1509 assert_eq!(binding.to_string(), "alt+shift+f");
1510
1511 let binding = KeyBinding::plain("enter");
1512 assert_eq!(binding.to_string(), "enter");
1513 }
1514
1515 #[test]
1516 fn test_default_bindings() {
1517 let bindings = KeyBindings::new();
1518
1519 let cursor_left = bindings.get_bindings(AppAction::CursorLeft);
1521 assert!(cursor_left.contains(&KeyBinding::plain("left")));
1522 assert!(cursor_left.contains(&KeyBinding::ctrl("b")));
1523
1524 let ctrl_c = KeyBinding::ctrl("c");
1526 let action = bindings.lookup(&ctrl_c);
1529 assert!(action == Some(AppAction::Copy) || action == Some(AppAction::Clear));
1530 }
1531
1532 #[test]
1533 fn test_action_categories() {
1534 assert_eq!(
1535 AppAction::CursorUp.category(),
1536 ActionCategory::CursorMovement
1537 );
1538 assert_eq!(
1539 AppAction::DeleteWordBackward.category(),
1540 ActionCategory::Deletion
1541 );
1542 assert_eq!(AppAction::Submit.category(), ActionCategory::TextInput);
1543 assert_eq!(AppAction::Yank.category(), ActionCategory::KillRing);
1544 }
1545
1546 #[test]
1547 fn test_action_iteration() {
1548 let bindings = KeyBindings::new();
1549
1550 assert!(bindings.iter().next().is_some());
1552
1553 let cursor_actions: Vec<_> = bindings
1555 .iter_category(ActionCategory::CursorMovement)
1556 .collect();
1557 assert!(
1558 cursor_actions
1559 .iter()
1560 .any(|(a, _)| *a == AppAction::CursorUp)
1561 );
1562 }
1563
1564 #[test]
1565 fn test_action_display_names() {
1566 assert_eq!(AppAction::CursorUp.display_name(), "Move cursor up");
1567 assert_eq!(AppAction::Submit.display_name(), "Submit input");
1568 assert_eq!(
1569 AppAction::ExternalEditor.display_name(),
1570 "Open in external editor"
1571 );
1572 }
1573
1574 #[test]
1575 fn test_all_actions_have_categories() {
1576 for action in AppAction::all() {
1577 let _ = action.category();
1579 }
1580 }
1581
1582 #[test]
1583 fn test_json_serialization() {
1584 let action = AppAction::CursorWordLeft;
1585 let json = serde_json::to_string(&action).unwrap();
1586 assert_eq!(json, "\"cursorWordLeft\"");
1587
1588 let parsed: AppAction = serde_json::from_str(&json).unwrap();
1589 assert_eq!(parsed, action);
1590 }
1591
1592 #[test]
1593 fn test_key_binding_json_roundtrip() {
1594 let binding = KeyBinding::ctrl_shift("p");
1595 let json = serde_json::to_string(&binding).unwrap();
1596 let parsed: KeyBinding = serde_json::from_str(&json).unwrap();
1597 assert_eq!(parsed, binding);
1598 }
1599
1600 #[test]
1605 fn test_parse_synonym_esc() {
1606 let binding: KeyBinding = "esc".parse().unwrap();
1607 assert_eq!(binding.key, "escape");
1608
1609 let binding: KeyBinding = "ESC".parse().unwrap();
1610 assert_eq!(binding.key, "escape");
1611 }
1612
1613 #[test]
1614 fn test_parse_synonym_return() {
1615 let binding: KeyBinding = "return".parse().unwrap();
1616 assert_eq!(binding.key, "enter");
1617
1618 let binding: KeyBinding = "RETURN".parse().unwrap();
1619 assert_eq!(binding.key, "enter");
1620 }
1621
1622 #[test]
1627 fn test_parse_case_insensitive_modifiers() {
1628 let binding: KeyBinding = "CTRL+a".parse().unwrap();
1629 assert!(binding.modifiers.ctrl);
1630 assert_eq!(binding.key, "a");
1631
1632 let binding: KeyBinding = "Ctrl+Shift+A".parse().unwrap();
1633 assert!(binding.modifiers.ctrl);
1634 assert!(binding.modifiers.shift);
1635 assert_eq!(binding.key, "a");
1636
1637 let binding: KeyBinding = "ALT+F".parse().unwrap();
1638 assert!(binding.modifiers.alt);
1639 assert_eq!(binding.key, "f");
1640 }
1641
1642 #[test]
1643 fn test_parse_case_insensitive_special_keys() {
1644 let binding: KeyBinding = "PageUp".parse().unwrap();
1645 assert_eq!(binding.key, "pageup");
1646
1647 let binding: KeyBinding = "PAGEDOWN".parse().unwrap();
1648 assert_eq!(binding.key, "pagedown");
1649
1650 let binding: KeyBinding = "ESCAPE".parse().unwrap();
1651 assert_eq!(binding.key, "escape");
1652
1653 let binding: KeyBinding = "Tab".parse().unwrap();
1654 assert_eq!(binding.key, "tab");
1655 }
1656
1657 #[test]
1662 fn test_parse_all_special_keys() {
1663 let special_keys = [
1665 "escape",
1666 "enter",
1667 "tab",
1668 "space",
1669 "backspace",
1670 "delete",
1671 "insert",
1672 "clear",
1673 "home",
1674 "end",
1675 "pageup",
1676 "pagedown",
1677 "up",
1678 "down",
1679 "left",
1680 "right",
1681 ];
1682
1683 for key in special_keys {
1684 let binding: KeyBinding = key.parse().unwrap();
1685 assert_eq!(binding.key, key, "Failed to parse special key: {key}");
1686 }
1687 }
1688
1689 #[test]
1690 fn test_parse_function_keys() {
1691 for i in 1..=20 {
1693 let key = format!("f{i}");
1694 let binding: KeyBinding = key.parse().unwrap();
1695 assert_eq!(binding.key, key, "Failed to parse function key: {key}");
1696 }
1697 }
1698
1699 #[test]
1700 fn test_parse_letters() {
1701 for c in 'a'..='z' {
1702 let key = c.to_string();
1703 let binding: KeyBinding = key.parse().unwrap();
1704 assert_eq!(binding.key, key);
1705 }
1706 }
1707
1708 #[test]
1709 fn test_parse_symbols() {
1710 let symbols = [
1711 "`", "-", "=", "[", "]", "\\", ";", "'", ",", ".", "/", "!", "@", "#", "$", "%", "^",
1712 "&", "*", "(", ")", "_", "+", "|", "~", "{", "}", ":", "<", ">", "?",
1713 ];
1714
1715 for sym in symbols {
1716 let binding: KeyBinding = sym.parse().unwrap();
1717 assert_eq!(binding.key, sym, "Failed to parse symbol: {sym}");
1718 }
1719 }
1720
1721 #[test]
1722 fn test_parse_plus_key_with_modifiers() {
1723 let binding: KeyBinding = "ctrl++".parse().unwrap();
1724 assert!(binding.modifiers.ctrl);
1725 assert_eq!(binding.key, "+");
1726 assert_eq!(binding.to_string(), "ctrl++");
1727
1728 let binding: KeyBinding = "ctrl + +".parse().unwrap();
1729 assert!(binding.modifiers.ctrl);
1730 assert_eq!(binding.key, "+");
1731 assert_eq!(binding.to_string(), "ctrl++");
1732 }
1733
1734 #[test]
1739 fn test_parse_all_modifier_combinations() {
1740 let binding: KeyBinding = "ctrl+x".parse().unwrap();
1742 assert!(binding.modifiers.ctrl);
1743 assert!(!binding.modifiers.alt);
1744 assert!(!binding.modifiers.shift);
1745
1746 let binding: KeyBinding = "alt+x".parse().unwrap();
1748 assert!(!binding.modifiers.ctrl);
1749 assert!(binding.modifiers.alt);
1750 assert!(!binding.modifiers.shift);
1751
1752 let binding: KeyBinding = "shift+x".parse().unwrap();
1754 assert!(!binding.modifiers.ctrl);
1755 assert!(!binding.modifiers.alt);
1756 assert!(binding.modifiers.shift);
1757
1758 let binding: KeyBinding = "ctrl+alt+x".parse().unwrap();
1760 assert!(binding.modifiers.ctrl);
1761 assert!(binding.modifiers.alt);
1762 assert!(!binding.modifiers.shift);
1763
1764 let binding: KeyBinding = "ctrl+shift+x".parse().unwrap();
1766 assert!(binding.modifiers.ctrl);
1767 assert!(!binding.modifiers.alt);
1768 assert!(binding.modifiers.shift);
1769
1770 let binding: KeyBinding = "alt+shift+x".parse().unwrap();
1772 assert!(!binding.modifiers.ctrl);
1773 assert!(binding.modifiers.alt);
1774 assert!(binding.modifiers.shift);
1775
1776 let binding: KeyBinding = "ctrl+shift+alt+x".parse().unwrap();
1778 assert!(binding.modifiers.ctrl);
1779 assert!(binding.modifiers.alt);
1780 assert!(binding.modifiers.shift);
1781 }
1782
1783 #[test]
1784 fn test_parse_control_synonym() {
1785 let binding: KeyBinding = "control+a".parse().unwrap();
1786 assert!(binding.modifiers.ctrl);
1787 assert_eq!(binding.key, "a");
1788 }
1789
1790 #[test]
1795 fn test_parse_empty_string() {
1796 let result: Result<KeyBinding, _> = "".parse();
1797 assert!(matches!(result, Err(KeyBindingParseError::Empty)));
1798 }
1799
1800 #[test]
1801 fn test_parse_whitespace_only() {
1802 let result: Result<KeyBinding, _> = " ".parse();
1803 assert!(matches!(result, Err(KeyBindingParseError::Empty)));
1804 }
1805
1806 #[test]
1807 fn test_parse_only_modifiers() {
1808 let result: Result<KeyBinding, _> = "ctrl".parse();
1809 assert!(matches!(result, Err(KeyBindingParseError::NoKey)));
1810
1811 let result: Result<KeyBinding, _> = "ctrl+shift".parse();
1812 assert!(matches!(result, Err(KeyBindingParseError::NoKey)));
1813 }
1814
1815 #[test]
1816 fn test_parse_multiple_keys() {
1817 let result: Result<KeyBinding, _> = "a+b".parse();
1818 assert!(matches!(
1819 result,
1820 Err(KeyBindingParseError::MultipleKeys { .. })
1821 ));
1822
1823 let result: Result<KeyBinding, _> = "ctrl+a+b".parse();
1824 assert!(matches!(
1825 result,
1826 Err(KeyBindingParseError::MultipleKeys { .. })
1827 ));
1828 }
1829
1830 #[test]
1831 fn test_parse_duplicate_modifiers() {
1832 let result: Result<KeyBinding, _> = "ctrl+ctrl+x".parse();
1833 assert!(matches!(
1834 result,
1835 Err(KeyBindingParseError::DuplicateModifier {
1836 modifier,
1837 ..
1838 }) if modifier == "ctrl"
1839 ));
1840
1841 let result: Result<KeyBinding, _> = "alt+alt+x".parse();
1842 assert!(matches!(
1843 result,
1844 Err(KeyBindingParseError::DuplicateModifier {
1845 modifier,
1846 ..
1847 }) if modifier == "alt"
1848 ));
1849
1850 let result: Result<KeyBinding, _> = "shift+shift+x".parse();
1851 assert!(matches!(
1852 result,
1853 Err(KeyBindingParseError::DuplicateModifier {
1854 modifier,
1855 ..
1856 }) if modifier == "shift"
1857 ));
1858 }
1859
1860 #[test]
1861 fn test_parse_unknown_key() {
1862 let result: Result<KeyBinding, _> = "unknownkey".parse();
1863 assert!(matches!(
1864 result,
1865 Err(KeyBindingParseError::UnknownKey { .. })
1866 ));
1867
1868 let result: Result<KeyBinding, _> = "ctrl+xyz".parse();
1869 assert!(matches!(
1870 result,
1871 Err(KeyBindingParseError::UnknownKey { .. })
1872 ));
1873 }
1874
1875 #[test]
1876 fn test_parse_unknown_modifier() {
1877 let result: Result<KeyBinding, _> = "meta+enter".parse();
1878 assert!(matches!(
1879 result,
1880 Err(KeyBindingParseError::UnknownModifier { modifier, .. }) if modifier == "meta"
1881 ));
1882
1883 let result: Result<KeyBinding, _> = "ctrl+meta+enter".parse();
1884 assert!(matches!(
1885 result,
1886 Err(KeyBindingParseError::UnknownModifier { modifier, .. }) if modifier == "meta"
1887 ));
1888 }
1889
1890 #[test]
1895 fn test_normalization_output_stable() {
1896 let binding1: KeyBinding = "CTRL+SHIFT+P".parse().unwrap();
1898 let binding2: KeyBinding = "ctrl+shift+p".parse().unwrap();
1899 let binding3: KeyBinding = "Ctrl+Shift+P".parse().unwrap();
1900
1901 assert_eq!(binding1.to_string(), binding2.to_string());
1902 assert_eq!(binding2.to_string(), binding3.to_string());
1903 assert_eq!(binding1.to_string(), "ctrl+shift+p");
1904 }
1905
1906 #[test]
1907 fn test_synonym_normalization_stable() {
1908 let binding1: KeyBinding = "esc".parse().unwrap();
1909 let binding2: KeyBinding = "escape".parse().unwrap();
1910 let binding3: KeyBinding = "ESCAPE".parse().unwrap();
1911
1912 assert_eq!(binding1.key, "escape");
1913 assert_eq!(binding2.key, "escape");
1914 assert_eq!(binding3.key, "escape");
1915 }
1916
1917 #[test]
1922 fn test_parse_all_legacy_default_bindings() {
1923 let legacy_bindings = [
1925 "up",
1926 "down",
1927 "left",
1928 "ctrl+b",
1929 "right",
1930 "ctrl+f",
1931 "alt+left",
1932 "ctrl+left",
1933 "alt+b",
1934 "alt+right",
1935 "ctrl+right",
1936 "alt+f",
1937 "home",
1938 "ctrl+a",
1939 "end",
1940 "ctrl+e",
1941 "ctrl+]",
1942 "ctrl+alt+]",
1943 "pageUp",
1944 "pageDown",
1945 "backspace",
1946 "delete",
1947 "ctrl+d",
1948 "ctrl+w",
1949 "alt+backspace",
1950 "alt+d",
1951 "alt+delete",
1952 "ctrl+u",
1953 "ctrl+k",
1954 "shift+enter",
1955 "enter",
1956 "tab",
1957 "ctrl+y",
1958 "alt+y",
1959 "ctrl+-",
1960 "ctrl+c",
1961 "ctrl+v",
1962 "escape",
1963 "ctrl+z",
1964 "ctrl+g",
1965 "ctrl+l",
1966 "ctrl+p",
1967 "shift+ctrl+p",
1968 "shift+tab",
1969 "ctrl+o",
1970 "ctrl+t",
1971 "alt+enter",
1972 "alt+up",
1973 "ctrl+s",
1974 "ctrl+n",
1975 "ctrl+r",
1976 "ctrl+backspace",
1977 ];
1978
1979 for key in legacy_bindings {
1980 let result: Result<KeyBinding, _> = key.parse();
1981 assert!(result.is_ok(), "Failed to parse legacy binding: {key}");
1982 }
1983 }
1984
1985 #[test]
1990 fn test_is_valid_key() {
1991 assert!(is_valid_key("ctrl+a"));
1992 assert!(is_valid_key("enter"));
1993 assert!(is_valid_key("shift+tab"));
1994
1995 assert!(!is_valid_key(""));
1996 assert!(!is_valid_key("ctrl+ctrl+x"));
1997 assert!(!is_valid_key("unknownkey"));
1998 }
1999
2000 #[test]
2001 fn test_error_display() {
2002 let err = KeyBindingParseError::Empty;
2003 assert_eq!(err.to_string(), "Empty key binding");
2004
2005 let err = KeyBindingParseError::DuplicateModifier {
2006 modifier: "ctrl".to_string(),
2007 binding: "ctrl+ctrl+x".to_string(),
2008 };
2009 assert!(err.to_string().contains("ctrl"));
2010 assert!(err.to_string().contains("ctrl+ctrl+x"));
2011
2012 let err = KeyBindingParseError::UnknownKey {
2013 key: "xyz".to_string(),
2014 binding: "ctrl+xyz".to_string(),
2015 };
2016 assert!(err.to_string().contains("xyz"));
2017
2018 let err = KeyBindingParseError::UnknownModifier {
2019 modifier: "meta".to_string(),
2020 binding: "meta+enter".to_string(),
2021 };
2022 assert!(err.to_string().contains("meta"));
2023 assert!(err.to_string().contains("meta+enter"));
2024 }
2025
2026 #[test]
2031 fn test_user_config_path_matches_global_dir() {
2032 let expected = crate::config::Config::global_dir().join("keybindings.json");
2033 assert_eq!(KeyBindings::user_config_path(), expected);
2034 }
2035
2036 #[test]
2037 fn test_load_from_nonexistent_path_returns_defaults() {
2038 let path = std::path::Path::new("/nonexistent/keybindings.json");
2039 let result = KeyBindings::load_from_path_with_diagnostics(path);
2040
2041 assert!(!result.has_warnings());
2043 assert!(result.bindings.lookup(&KeyBinding::ctrl("a")).is_some());
2044 }
2045
2046 #[test]
2047 fn test_load_valid_override() {
2048 let temp = tempfile::tempdir().unwrap();
2049 let path = temp.path().join("keybindings.json");
2050
2051 std::fs::write(
2052 &path,
2053 r#"{
2054 "cursorUp": ["up", "ctrl+p"],
2055 "cursorDown": "down"
2056 }"#,
2057 )
2058 .unwrap();
2059
2060 let result = KeyBindings::load_from_path_with_diagnostics(&path);
2061
2062 assert!(!result.has_warnings());
2063
2064 let up_bindings = result.bindings.get_bindings(AppAction::CursorUp);
2066 assert!(up_bindings.contains(&KeyBinding::plain("up")));
2067 assert!(up_bindings.contains(&KeyBinding::ctrl("p")));
2068
2069 let down_bindings = result.bindings.get_bindings(AppAction::CursorDown);
2071 assert!(down_bindings.contains(&KeyBinding::plain("down")));
2072 }
2073
2074 #[test]
2075 fn test_load_warns_on_unknown_action() {
2076 let temp = tempfile::tempdir().unwrap();
2077 let path = temp.path().join("keybindings.json");
2078
2079 std::fs::write(
2080 &path,
2081 r#"{
2082 "cursorUp": ["up"],
2083 "unknownAction": ["ctrl+x"],
2084 "anotherBadAction": ["ctrl+y"]
2085 }"#,
2086 )
2087 .unwrap();
2088
2089 let result = KeyBindings::load_from_path_with_diagnostics(&path);
2090
2091 assert_eq!(result.warnings.len(), 2);
2093 assert!(result.format_warnings().contains("unknownAction"));
2094 assert!(result.format_warnings().contains("anotherBadAction"));
2095
2096 let up_bindings = result.bindings.get_bindings(AppAction::CursorUp);
2098 assert!(up_bindings.contains(&KeyBinding::plain("up")));
2099 }
2100
2101 #[test]
2102 fn test_load_warns_on_invalid_key() {
2103 let temp = tempfile::tempdir().unwrap();
2104 let path = temp.path().join("keybindings.json");
2105
2106 std::fs::write(
2107 &path,
2108 r#"{
2109 "cursorUp": ["up", "invalidkey123", "ctrl+p"]
2110 }"#,
2111 )
2112 .unwrap();
2113
2114 let result = KeyBindings::load_from_path_with_diagnostics(&path);
2115
2116 assert_eq!(result.warnings.len(), 1);
2118 assert!(result.format_warnings().contains("invalidkey123"));
2119
2120 let up_bindings = result.bindings.get_bindings(AppAction::CursorUp);
2122 assert!(up_bindings.contains(&KeyBinding::plain("up")));
2123 assert!(up_bindings.contains(&KeyBinding::ctrl("p")));
2124 assert_eq!(up_bindings.len(), 2); }
2126
2127 #[test]
2128 fn test_load_warns_on_invalid_json() {
2129 let temp = tempfile::tempdir().unwrap();
2130 let path = temp.path().join("keybindings.json");
2131
2132 std::fs::write(&path, "{ not valid json }").unwrap();
2133
2134 let result = KeyBindings::load_from_path_with_diagnostics(&path);
2135
2136 assert_eq!(result.warnings.len(), 1);
2138 assert!(matches!(
2139 result.warnings[0],
2140 KeyBindingsWarning::ParseError { .. }
2141 ));
2142
2143 assert!(result.bindings.lookup(&KeyBinding::ctrl("a")).is_some());
2145 }
2146
2147 #[test]
2148 fn test_load_handles_invalid_value_type() {
2149 let temp = tempfile::tempdir().unwrap();
2150 let path = temp.path().join("keybindings.json");
2151
2152 std::fs::write(
2153 &path,
2154 r#"{
2155 "cursorUp": 123,
2156 "cursorDown": ["down"]
2157 }"#,
2158 )
2159 .unwrap();
2160
2161 let result = KeyBindings::load_from_path_with_diagnostics(&path);
2162
2163 assert_eq!(result.warnings.len(), 1);
2165 assert!(matches!(
2166 result.warnings[0],
2167 KeyBindingsWarning::InvalidKeyValue { .. }
2168 ));
2169
2170 let down_bindings = result.bindings.get_bindings(AppAction::CursorDown);
2172 assert!(down_bindings.contains(&KeyBinding::plain("down")));
2173 }
2174
2175 #[test]
2176 fn test_warning_display_format() {
2177 let warning = KeyBindingsWarning::UnknownAction {
2178 action: "badAction".to_string(),
2179 path: PathBuf::from("/test/keybindings.json"),
2180 };
2181 let msg = warning.to_string();
2182 assert!(msg.contains("badAction"));
2183 assert!(msg.contains("/test/keybindings.json"));
2184 assert!(msg.contains("ignored"));
2185 }
2186
2187 #[test]
2192 fn test_from_bubbletea_key_ctrl_keys() {
2193 use bubbletea::{KeyMsg, KeyType};
2194
2195 let key = KeyMsg::from_type(KeyType::CtrlC);
2197 let binding = KeyBinding::from_bubbletea_key(&key).unwrap();
2198 assert_eq!(binding.key, "c");
2199 assert!(binding.modifiers.ctrl);
2200 assert!(!binding.modifiers.alt);
2201
2202 let key = KeyMsg::from_type(KeyType::CtrlP);
2204 let binding = KeyBinding::from_bubbletea_key(&key).unwrap();
2205 assert_eq!(binding.key, "p");
2206 assert!(binding.modifiers.ctrl);
2207 }
2208
2209 #[test]
2210 fn test_from_bubbletea_key_special_keys() {
2211 use bubbletea::{KeyMsg, KeyType};
2212
2213 let binding = KeyBinding::from_bubbletea_key(&KeyMsg::from_type(KeyType::Enter)).unwrap();
2215 assert_eq!(binding.key, "enter");
2216 assert_eq!(binding.modifiers, KeyModifiers::NONE);
2217
2218 let binding = KeyBinding::from_bubbletea_key(&KeyMsg::from_type(KeyType::Esc)).unwrap();
2220 assert_eq!(binding.key, "escape");
2221
2222 let binding = KeyBinding::from_bubbletea_key(&KeyMsg::from_type(KeyType::Tab)).unwrap();
2224 assert_eq!(binding.key, "tab");
2225
2226 let binding =
2228 KeyBinding::from_bubbletea_key(&KeyMsg::from_type(KeyType::Backspace)).unwrap();
2229 assert_eq!(binding.key, "backspace");
2230 }
2231
2232 #[test]
2233 fn test_from_bubbletea_key_arrow_keys() {
2234 use bubbletea::{KeyMsg, KeyType};
2235
2236 let binding = KeyBinding::from_bubbletea_key(&KeyMsg::from_type(KeyType::Up)).unwrap();
2238 assert_eq!(binding.key, "up");
2239 assert_eq!(binding.modifiers, KeyModifiers::NONE);
2240
2241 let binding = KeyBinding::from_bubbletea_key(&KeyMsg::from_type(KeyType::ShiftUp)).unwrap();
2243 assert_eq!(binding.key, "up");
2244 assert!(binding.modifiers.shift);
2245
2246 let binding =
2248 KeyBinding::from_bubbletea_key(&KeyMsg::from_type(KeyType::CtrlLeft)).unwrap();
2249 assert_eq!(binding.key, "left");
2250 assert!(binding.modifiers.ctrl);
2251
2252 let binding =
2254 KeyBinding::from_bubbletea_key(&KeyMsg::from_type(KeyType::CtrlShiftDown)).unwrap();
2255 assert_eq!(binding.key, "down");
2256 assert!(binding.modifiers.ctrl);
2257 assert!(binding.modifiers.shift);
2258 }
2259
2260 #[test]
2261 fn test_from_bubbletea_key_with_alt() {
2262 use bubbletea::{KeyMsg, KeyType};
2263
2264 let key = KeyMsg::from_type(KeyType::Up).with_alt();
2266 let binding = KeyBinding::from_bubbletea_key(&key).unwrap();
2267 assert_eq!(binding.key, "up");
2268 assert!(binding.modifiers.alt);
2269 assert!(!binding.modifiers.ctrl);
2270
2271 let key = KeyMsg::from_char('f').with_alt();
2273 let binding = KeyBinding::from_bubbletea_key(&key).unwrap();
2274 assert_eq!(binding.key, "f");
2275 assert!(binding.modifiers.alt);
2276 }
2277
2278 #[test]
2279 fn test_from_bubbletea_key_runes() {
2280 use bubbletea::KeyMsg;
2281
2282 let key = KeyMsg::from_char('a');
2284 let binding = KeyBinding::from_bubbletea_key(&key).unwrap();
2285 assert_eq!(binding.key, "a");
2286 assert_eq!(binding.modifiers, KeyModifiers::NONE);
2287
2288 let key = KeyMsg::from_char('A');
2290 let binding = KeyBinding::from_bubbletea_key(&key).unwrap();
2291 assert_eq!(binding.key, "a");
2292 }
2293
2294 #[test]
2295 fn test_from_bubbletea_key_multi_char_returns_none() {
2296 use bubbletea::KeyMsg;
2297
2298 let key = KeyMsg::from_runes(vec!['a', 'b']);
2300 assert!(KeyBinding::from_bubbletea_key(&key).is_none());
2301 }
2302
2303 #[test]
2304 fn test_from_bubbletea_key_paste_returns_none() {
2305 use bubbletea::KeyMsg;
2306
2307 let key = KeyMsg::from_char('a').with_paste();
2309 assert!(KeyBinding::from_bubbletea_key(&key).is_none());
2310 }
2311
2312 #[test]
2313 fn test_from_bubbletea_key_function_keys() {
2314 use bubbletea::{KeyMsg, KeyType};
2315
2316 let binding = KeyBinding::from_bubbletea_key(&KeyMsg::from_type(KeyType::F1)).unwrap();
2317 assert_eq!(binding.key, "f1");
2318
2319 let binding = KeyBinding::from_bubbletea_key(&KeyMsg::from_type(KeyType::F12)).unwrap();
2320 assert_eq!(binding.key, "f12");
2321 }
2322
2323 #[test]
2324 fn test_from_bubbletea_key_navigation() {
2325 use bubbletea::{KeyMsg, KeyType};
2326
2327 let binding = KeyBinding::from_bubbletea_key(&KeyMsg::from_type(KeyType::Home)).unwrap();
2328 assert_eq!(binding.key, "home");
2329
2330 let binding = KeyBinding::from_bubbletea_key(&KeyMsg::from_type(KeyType::PgUp)).unwrap();
2331 assert_eq!(binding.key, "pageup");
2332
2333 let binding = KeyBinding::from_bubbletea_key(&KeyMsg::from_type(KeyType::Delete)).unwrap();
2334 assert_eq!(binding.key, "delete");
2335 }
2336
2337 #[test]
2338 fn test_keybinding_lookup_via_conversion() {
2339 use bubbletea::{KeyMsg, KeyType};
2340
2341 let bindings = KeyBindings::new();
2342
2343 let key = KeyMsg::from_type(KeyType::CtrlC);
2345 let binding = KeyBinding::from_bubbletea_key(&key).unwrap();
2346 assert!(bindings.lookup(&binding).is_some());
2347
2348 let key = KeyMsg::from_type(KeyType::PgUp);
2350 let binding = KeyBinding::from_bubbletea_key(&key).unwrap();
2351 let action = bindings.lookup(&binding);
2352 assert_eq!(action, Some(AppAction::PageUp));
2353
2354 let key = KeyMsg::from_type(KeyType::Enter);
2356 let binding = KeyBinding::from_bubbletea_key(&key).unwrap();
2357 let action = bindings.lookup(&binding);
2358 assert_eq!(action, Some(AppAction::Submit));
2359 }
2360
2361 mod proptest_keybindings {
2364 use super::*;
2365 use proptest::prelude::*;
2366
2367 fn arb_valid_key() -> impl Strategy<Value = String> {
2368 prop::sample::select(
2369 vec![
2370 "a",
2371 "b",
2372 "c",
2373 "z",
2374 "escape",
2375 "enter",
2376 "tab",
2377 "space",
2378 "backspace",
2379 "delete",
2380 "home",
2381 "end",
2382 "pageup",
2383 "pagedown",
2384 "up",
2385 "down",
2386 "left",
2387 "right",
2388 "f1",
2389 "f5",
2390 "f12",
2391 "f20",
2392 "`",
2393 "-",
2394 "=",
2395 "[",
2396 "]",
2397 ";",
2398 ",",
2399 ".",
2400 "/",
2401 ]
2402 .into_iter()
2403 .map(String::from)
2404 .collect::<Vec<_>>(),
2405 )
2406 }
2407
2408 fn arb_modifiers() -> impl Strategy<Value = (bool, bool, bool)> {
2409 (any::<bool>(), any::<bool>(), any::<bool>())
2410 }
2411
2412 fn arb_binding_string() -> impl Strategy<Value = String> {
2413 (arb_modifiers(), arb_valid_key()).prop_map(|((ctrl, alt, shift), key)| {
2414 let mut parts = Vec::new();
2415 if ctrl {
2416 parts.push("ctrl".to_string());
2417 }
2418 if alt {
2419 parts.push("alt".to_string());
2420 }
2421 if shift {
2422 parts.push("shift".to_string());
2423 }
2424 parts.push(key);
2425 parts.join("+")
2426 })
2427 }
2428
2429 proptest! {
2430 #[test]
2431 fn normalize_key_name_is_idempotent(key in arb_valid_key()) {
2432 if let Some(normalized) = normalize_key_name(&key) {
2433 let double = normalize_key_name(&normalized);
2434 assert_eq!(
2435 double.as_deref(), Some(normalized.as_str()),
2436 "normalizing twice should equal normalizing once"
2437 );
2438 }
2439 }
2440
2441 #[test]
2442 fn normalize_key_name_is_case_insensitive(key in arb_valid_key()) {
2443 let lower = normalize_key_name(&key.to_lowercase());
2444 let upper = normalize_key_name(&key.to_uppercase());
2445 assert_eq!(
2446 lower, upper,
2447 "normalize should be case-insensitive for '{key}'"
2448 );
2449 }
2450
2451 #[test]
2452 fn normalize_key_name_output_is_lowercase(key in arb_valid_key()) {
2453 if let Some(normalized) = normalize_key_name(&key) {
2454 assert_eq!(
2455 normalized, normalized.to_lowercase(),
2456 "normalized key should be lowercase"
2457 );
2458 }
2459 }
2460
2461 #[test]
2462 fn parse_key_binding_roundtrips_valid_bindings(s in arb_binding_string()) {
2463 let parsed = parse_key_binding(&s);
2464 if let Ok(binding) = parsed {
2465 let displayed = binding.to_string();
2466 let reparsed = parse_key_binding(&displayed);
2467 assert_eq!(
2468 reparsed.as_ref(), Ok(&binding),
2469 "roundtrip failed: '{s}' → '{displayed}' → {reparsed:?}"
2470 );
2471 }
2472 }
2473
2474 #[test]
2475 fn parse_key_binding_is_case_insensitive(s in arb_binding_string()) {
2476 let lower = parse_key_binding(&s.to_lowercase());
2477 let upper = parse_key_binding(&s.to_uppercase());
2478 assert_eq!(
2479 lower, upper,
2480 "parse should be case-insensitive"
2481 );
2482 }
2483
2484 #[test]
2485 fn parse_key_binding_tolerates_whitespace(s in arb_binding_string()) {
2486 let spaced = s.replace('+', " + ");
2487 let normal = parse_key_binding(&s);
2488 let with_spaces = parse_key_binding(&spaced);
2489 assert_eq!(
2490 normal, with_spaces,
2491 "whitespace around + should not matter"
2492 );
2493 }
2494
2495 #[test]
2496 fn is_valid_key_matches_parse(s in arb_binding_string()) {
2497 let valid = is_valid_key(&s);
2498 let parsed = parse_key_binding(&s).is_ok();
2499 assert_eq!(
2500 valid, parsed,
2501 "is_valid_key should match parse_key_binding.is_ok()"
2502 );
2503 }
2504
2505 #[test]
2506 fn parse_key_binding_never_panics(s in ".*") {
2507 let _ = parse_key_binding(&s);
2509 }
2510
2511 #[test]
2512 fn modifier_order_independence(
2513 key in arb_valid_key(),
2514 ) {
2515 let ca = parse_key_binding(&format!("ctrl+alt+{key}"));
2517 let ac = parse_key_binding(&format!("alt+ctrl+{key}"));
2518 assert_eq!(ca, ac, "modifier order should not matter");
2519
2520 let cs = parse_key_binding(&format!("ctrl+shift+{key}"));
2522 let sc = parse_key_binding(&format!("shift+ctrl+{key}"));
2523 assert_eq!(cs, sc, "modifier order should not matter");
2524 }
2525
2526 #[test]
2527 fn display_always_canonical_modifier_order(
2528 (ctrl, alt, shift) in arb_modifiers(),
2529 key in arb_valid_key(),
2530 ) {
2531 let binding = KeyBinding {
2532 key: normalize_key_name(&key).unwrap_or_else(|| key.clone()),
2533 modifiers: KeyModifiers { ctrl, shift, alt },
2534 };
2535 let displayed = binding.to_string();
2536 let ctrl_pos = displayed.find("ctrl+");
2538 let alt_pos = displayed.find("alt+");
2539 let shift_pos = displayed.find("shift+");
2540 if let (Some(c), Some(a)) = (ctrl_pos, alt_pos) {
2541 assert!(c < a, "ctrl should come before alt in display");
2542 }
2543 if let (Some(a), Some(s)) = (alt_pos, shift_pos) {
2544 assert!(a < s, "alt should come before shift in display");
2545 }
2546 if let (Some(c), Some(s)) = (ctrl_pos, shift_pos) {
2547 assert!(c < s, "ctrl should come before shift in display");
2548 }
2549 }
2550
2551 #[test]
2552 fn synonym_normalization_consistent(
2553 synonym in prop::sample::select(vec![
2554 ("esc", "escape"),
2555 ("return", "enter"),
2556 ]),
2557 ) {
2558 let (alias, canonical) = synonym;
2559 let n1 = normalize_key_name(alias);
2560 let n2 = normalize_key_name(canonical);
2561 assert_eq!(
2562 n1, n2,
2563 "'{alias}' and '{canonical}' should normalize the same"
2564 );
2565 }
2566
2567 #[test]
2568 fn single_letters_always_valid(
2569 idx in 0..26usize,
2570 ) {
2571 #[allow(clippy::cast_possible_truncation)]
2572 let c = (b'a' + idx as u8) as char;
2573 let s = c.to_string();
2574 assert!(
2575 normalize_key_name(&s).is_some(),
2576 "single letter '{c}' should be valid"
2577 );
2578 assert!(
2579 is_valid_key(&s),
2580 "single letter '{c}' should be a valid key binding"
2581 );
2582 }
2583
2584 #[test]
2585 fn function_keys_f1_to_f20_valid(n in 1..=20u8) {
2586 let key = format!("f{n}");
2587 assert!(
2588 normalize_key_name(&key).is_some(),
2589 "function key '{key}' should be valid"
2590 );
2591 }
2592
2593 #[test]
2594 fn function_keys_beyond_f20_invalid(n in 21..99u8) {
2595 let key = format!("f{n}");
2596 assert!(
2597 normalize_key_name(&key).is_none(),
2598 "function key '{key}' should be invalid"
2599 );
2600 }
2601 }
2602 }
2603}