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