Skip to main content

pi/
keybindings.rs

1//! Keybindings and action catalog for interactive mode.
2//!
3//! This module defines all available actions and their default key bindings,
4//! matching the legacy Pi Agent behavior from keybindings.md.
5//!
6//! ## Usage
7//!
8//! ```ignore
9//! use pi::keybindings::{AppAction, KeyBindings};
10//!
11//! let bindings = KeyBindings::default();
12//! let action = bindings.lookup(&key_event);
13//! ```
14
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::fmt;
18use std::path::{Path, PathBuf};
19use std::str::FromStr;
20
21// ============================================================================
22// Load Result (for user config loading with diagnostics)
23// ============================================================================
24
25/// Result of loading keybindings with diagnostics.
26#[derive(Debug)]
27pub struct KeyBindingsLoadResult {
28    /// The loaded keybindings (defaults if loading failed).
29    pub bindings: KeyBindings,
30    /// Path that was attempted to load.
31    pub path: PathBuf,
32    /// Warnings encountered during loading.
33    pub warnings: Vec<KeyBindingsWarning>,
34}
35
36impl KeyBindingsLoadResult {
37    /// Check if there were any warnings.
38    #[must_use]
39    pub fn has_warnings(&self) -> bool {
40        !self.warnings.is_empty()
41    }
42
43    /// Format warnings for display.
44    #[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/// Warning types for keybindings loading.
55#[derive(Debug, Clone)]
56pub enum KeyBindingsWarning {
57    /// Could not read the config file.
58    ReadError { path: PathBuf, error: String },
59    /// Could not parse the config file as JSON.
60    ParseError { path: PathBuf, error: String },
61    /// Unknown action ID in config.
62    UnknownAction { action: String, path: PathBuf },
63    /// Invalid key string in config.
64    InvalidKey {
65        action: String,
66        key: String,
67        error: String,
68        path: PathBuf,
69    },
70    /// Invalid value type for key (not a string).
71    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// ============================================================================
128// Action Categories (for /hotkeys display grouping)
129// ============================================================================
130
131/// Categories for organizing actions in /hotkeys display.
132#[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    /// Human-readable display name for the category.
151    #[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    /// Get all categories in display order.
170    #[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// ============================================================================
190// App Actions
191// ============================================================================
192
193/// All available actions that can be bound to keys.
194///
195/// Action IDs are stable (snake_case) for JSON serialization/deserialization.
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub enum AppAction {
199    // Cursor Movement
200    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    // Deletion
214    DeleteCharBackward,
215    DeleteCharForward,
216    DeleteWordBackward,
217    DeleteWordForward,
218    DeleteToLineStart,
219    DeleteToLineEnd,
220
221    // Text Input
222    NewLine,
223    Submit,
224    Tab,
225
226    // Kill Ring
227    Yank,
228    YankPop,
229    Undo,
230
231    // Clipboard
232    Copy,
233    PasteImage,
234
235    // Application
236    Interrupt,
237    Clear,
238    Exit,
239    Suspend,
240    ExternalEditor,
241    Help,
242    OpenSettings,
243
244    // Session
245    NewSession,
246    Tree,
247    Fork,
248    BranchPicker,
249    BranchNextSibling,
250    BranchPrevSibling,
251
252    // Models & Thinking
253    SelectModel,
254    CycleModelForward,
255    CycleModelBackward,
256    CycleThinkingLevel,
257
258    // Display
259    ExpandTools,
260    ToggleThinking,
261
262    // Message Queue
263    FollowUp,
264    Dequeue,
265
266    // Selection (Lists, Pickers)
267    SelectUp,
268    SelectDown,
269    SelectPageUp,
270    SelectPageDown,
271    SelectConfirm,
272    SelectCancel,
273
274    // Session Picker
275    ToggleSessionPath,
276    ToggleSessionSort,
277    ToggleSessionNamedFilter,
278    RenameSession,
279    DeleteSession,
280    DeleteSessionNoninvasive,
281}
282
283impl AppAction {
284    /// Human-readable display name for the action.
285    #[must_use]
286    pub const fn display_name(&self) -> &'static str {
287        match self {
288            // Cursor Movement
289            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            // Deletion
303            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            // Text Input
311            Self::NewLine => "Insert new line",
312            Self::Submit => "Submit input",
313            Self::Tab => "Tab / autocomplete",
314
315            // Kill Ring
316            Self::Yank => "Paste most recently deleted text",
317            Self::YankPop => "Cycle through deleted text after yank",
318            Self::Undo => "Undo last edit",
319
320            // Clipboard
321            Self::Copy => "Copy selection",
322            Self::PasteImage => "Paste image from clipboard",
323
324            // Application
325            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            // Session
334            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            // Models & Thinking
342            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            // Display
348            Self::ExpandTools => "Collapse/expand tool output",
349            Self::ToggleThinking => "Collapse/expand thinking blocks",
350
351            // Message Queue
352            Self::FollowUp => "Queue follow-up message",
353            Self::Dequeue => "Restore queued messages to editor",
354
355            // Selection
356            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            // Session Picker
364            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    /// Get the category this action belongs to.
374    #[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    /// Get all actions in a category.
444    #[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    /// Get all actions.
454    #[must_use]
455    pub const fn all() -> &'static [Self] {
456        &[
457            // Cursor Movement
458            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            // Deletion
471            Self::DeleteCharBackward,
472            Self::DeleteCharForward,
473            Self::DeleteWordBackward,
474            Self::DeleteWordForward,
475            Self::DeleteToLineStart,
476            Self::DeleteToLineEnd,
477            // Text Input
478            Self::NewLine,
479            Self::Submit,
480            Self::Tab,
481            // Kill Ring
482            Self::Yank,
483            Self::YankPop,
484            Self::Undo,
485            // Clipboard
486            Self::Copy,
487            Self::PasteImage,
488            // Application
489            Self::Interrupt,
490            Self::Clear,
491            Self::Exit,
492            Self::Suspend,
493            Self::ExternalEditor,
494            Self::Help,
495            Self::OpenSettings,
496            // Session
497            Self::NewSession,
498            Self::Tree,
499            Self::Fork,
500            Self::BranchPicker,
501            Self::BranchNextSibling,
502            Self::BranchPrevSibling,
503            // Models & Thinking
504            Self::SelectModel,
505            Self::CycleModelForward,
506            Self::CycleModelBackward,
507            Self::CycleThinkingLevel,
508            // Display
509            Self::ExpandTools,
510            Self::ToggleThinking,
511            // Message Queue
512            Self::FollowUp,
513            Self::Dequeue,
514            // Selection
515            Self::SelectUp,
516            Self::SelectDown,
517            Self::SelectPageUp,
518            Self::SelectPageDown,
519            Self::SelectConfirm,
520            Self::SelectCancel,
521            // Session Picker
522            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        // Use serde's camelCase serialization for display
535        write!(
536            f,
537            "{}",
538            serde_json::to_string(self)
539                .unwrap_or_default()
540                .trim_matches('"')
541        )
542    }
543}
544
545// ============================================================================
546// Key Modifiers
547// ============================================================================
548
549/// Key modifiers (ctrl, shift, alt).
550#[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    /// No modifiers.
559    pub const NONE: Self = Self {
560        ctrl: false,
561        shift: false,
562        alt: false,
563    };
564
565    /// Ctrl modifier only.
566    pub const CTRL: Self = Self {
567        ctrl: true,
568        shift: false,
569        alt: false,
570    };
571
572    /// Shift modifier only.
573    pub const SHIFT: Self = Self {
574        ctrl: false,
575        shift: true,
576        alt: false,
577    };
578
579    /// Alt modifier only.
580    pub const ALT: Self = Self {
581        ctrl: false,
582        shift: false,
583        alt: true,
584    };
585
586    /// Ctrl+Shift modifiers.
587    pub const CTRL_SHIFT: Self = Self {
588        ctrl: true,
589        shift: true,
590        alt: false,
591    };
592
593    /// Ctrl+Alt modifiers.
594    pub const CTRL_ALT: Self = Self {
595        ctrl: true,
596        shift: false,
597        alt: true,
598    };
599
600    /// Alt+Shift modifiers (alias for consistency).
601    pub const ALT_SHIFT: Self = Self {
602        ctrl: false,
603        shift: true,
604        alt: true,
605    };
606}
607
608// ============================================================================
609// Key Binding
610// ============================================================================
611
612/// A key binding (key + modifiers).
613#[derive(Debug, Clone, PartialEq, Eq, Hash)]
614pub struct KeyBinding {
615    pub key: String,
616    pub modifiers: KeyModifiers,
617}
618
619impl KeyBinding {
620    /// Create a new key binding.
621    #[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    /// Create a key binding with no modifiers.
630    #[must_use]
631    pub fn plain(key: impl Into<String>) -> Self {
632        Self::new(key, KeyModifiers::NONE)
633    }
634
635    /// Create a key binding with ctrl modifier.
636    #[must_use]
637    pub fn ctrl(key: impl Into<String>) -> Self {
638        Self::new(key, KeyModifiers::CTRL)
639    }
640
641    /// Create a key binding with alt modifier.
642    #[must_use]
643    pub fn alt(key: impl Into<String>) -> Self {
644        Self::new(key, KeyModifiers::ALT)
645    }
646
647    /// Create a key binding with shift modifier.
648    #[must_use]
649    pub fn shift(key: impl Into<String>) -> Self {
650        Self::new(key, KeyModifiers::SHIFT)
651    }
652
653    /// Create a key binding with ctrl+shift modifiers.
654    #[must_use]
655    pub fn ctrl_shift(key: impl Into<String>) -> Self {
656        Self::new(key, KeyModifiers::CTRL_SHIFT)
657    }
658
659    /// Create a key binding with ctrl+alt modifiers.
660    #[must_use]
661    pub fn ctrl_alt(key: impl Into<String>) -> Self {
662        Self::new(key, KeyModifiers::CTRL_ALT)
663    }
664
665    /// Convert a bubbletea KeyMsg to a KeyBinding for lookup.
666    ///
667    /// Returns `None` for paste events or multi-character input that
668    /// cannot map to a single key binding.
669    #[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        // Skip paste events - they're not keybindings
675        if key.paste {
676            return None;
677        }
678
679        let (key_name, mut modifiers) = match key.key_type {
680            // Control keys map to ctrl+letter
681            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            // Arrow keys
719            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            // Shift variants
725            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            // Ctrl variants
734            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            // Ctrl+Shift variants
744            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            // Navigation
752            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            // Function keys
761            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            // Character input
783            KeyType::Runes => {
784                // Only handle single-character input
785                if key.runes.len() != 1 {
786                    return None;
787                }
788                let c = key.runes[0];
789                // Return a binding for the character
790                // Alt modifier is handled below
791                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        // Apply alt modifier if set (for non-Runes keys)
803        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/// Error type for key binding parsing.
840#[derive(Debug, Clone, PartialEq, Eq)]
841pub enum KeyBindingParseError {
842    /// The input string was empty.
843    Empty,
844    /// No key found in the binding (only modifiers).
845    NoKey,
846    /// Multiple keys found (e.g., "a+b").
847    MultipleKeys { binding: String },
848    /// Duplicate modifier (e.g., "ctrl+ctrl+x").
849    DuplicateModifier { modifier: String, binding: String },
850    /// Unknown modifier (e.g., "meta+enter").
851    UnknownModifier { modifier: String, binding: String },
852    /// Unknown key name.
853    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
877/// Normalize a key name to its canonical form.
878///
879/// Handles synonyms (esc→escape, return→enter) and case normalization.
880fn normalize_key_name(key: &str) -> Option<String> {
881    let lower = key.to_lowercase();
882
883    // Check synonyms first
884    let canonical = match lower.as_str() {
885        // Synonyms
886        "esc" => "escape",
887        "return" => "enter",
888
889        // Valid special keys and function keys (f1-f20 to match bubbletea KeyType coverage)
890        "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        // Single letters (a-z)
896        s if s.len() == 1 && s.chars().next().is_some_and(|c| c.is_ascii_lowercase()) => &lower,
897
898        // Symbols (single characters that are valid keys)
899        "`" | "-" | "=" | "[" | "]" | "\\" | ";" | "'" | "," | "." | "/" | "!" | "@" | "#"
900        | "$" | "%" | "^" | "&" | "*" | "(" | ")" | "_" | "+" | "|" | "~" | "{" | "}" | ":"
901        | "<" | ">" | "?" | "\"" => &lower,
902
903        // Invalid key
904        _ => return None,
905    };
906
907    Some(canonical.to_string())
908}
909
910/// Parse a key binding string into a KeyBinding.
911///
912/// Supports formats like:
913/// - "a" (single key)
914/// - "ctrl+a" (modifier + key)
915/// - "ctrl+shift+p" (multiple modifiers + key)
916/// - "pageUp" (special key, case insensitive)
917///
918/// # Errors
919///
920/// Returns an error for:
921/// - Empty strings
922/// - No key (only modifiers)
923/// - Multiple keys
924/// - Duplicate modifiers
925/// - Unknown keys
926fn 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    // Be forgiving about whitespace: "ctrl + a" is treated as "ctrl+a".
933    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    // Parse modifiers as a prefix chain so we can represent the '+' key itself (e.g. "ctrl++").
945    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    // Allow "ctrl" / "ctrl+shift" to be treated as "only modifiers".
998    if matches!(rest, "ctrl" | "control" | "alt" | "shift") {
999        return Err(KeyBindingParseError::NoKey);
1000    }
1001
1002    // After consuming known modifiers, any remaining '+' means either:
1003    // - the '+' key itself (rest == "+")
1004    // - multiple keys (e.g. "a+b") or an unknown modifier (e.g. "meta+enter")
1005    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/// Check if a key string is valid (for validation without full parsing).
1034#[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// ============================================================================
1059// Key Bindings Map
1060// ============================================================================
1061
1062/// Complete keybindings configuration.
1063#[derive(Debug, Clone)]
1064pub struct KeyBindings {
1065    /// Map from action to list of key bindings.
1066    bindings: HashMap<AppAction, Vec<KeyBinding>>,
1067    /// Reverse map for fast lookup.
1068    reverse: HashMap<KeyBinding, AppAction>,
1069}
1070
1071impl KeyBindings {
1072    /// Create keybindings with default bindings.
1073    #[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    /// Load keybindings from a JSON file, merging with defaults.
1081    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    /// Get the default user keybindings path: `~/.pi/agent/keybindings.json`
1096    #[must_use]
1097    pub fn user_config_path() -> std::path::PathBuf {
1098        crate::config::Config::global_dir().join("keybindings.json")
1099    }
1100
1101    /// Load keybindings from user config, returning defaults with diagnostics if loading fails.
1102    ///
1103    /// This method never fails - it always returns valid keybindings (defaults at minimum).
1104    /// Warnings are collected in `KeyBindingsLoadResult` for display to the user.
1105    ///
1106    /// # User Config Format
1107    ///
1108    /// The config file is a JSON object mapping action IDs (camelCase) to key bindings:
1109    ///
1110    /// ```json
1111    /// {
1112    ///   "cursorUp": ["up", "ctrl+p"],
1113    ///   "cursorDown": ["down", "ctrl+n"],
1114    ///   "deleteWordBackward": ["ctrl+w", "alt+backspace"]
1115    /// }
1116    /// ```
1117    #[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    /// Load keybindings from a specific path with full diagnostics.
1124    ///
1125    /// Returns defaults with warnings if:
1126    /// - File doesn't exist (no warning - this is normal)
1127    /// - File is not valid JSON
1128    /// - File contains unknown action IDs
1129    /// - File contains invalid key strings
1130    #[must_use]
1131    pub fn load_from_path_with_diagnostics(path: &Path) -> KeyBindingsLoadResult {
1132        let mut warnings = Vec::new();
1133
1134        // Check if file exists
1135        if !path.exists() {
1136            return KeyBindingsLoadResult {
1137                bindings: Self::new(),
1138                path: path.to_path_buf(),
1139                warnings,
1140            };
1141        }
1142
1143        // Read file
1144        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        // Parse as loose JSON (object with string keys and string/array values)
1160        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        // Start with defaults
1176        let mut bindings = Self::default_bindings();
1177
1178        // Process each entry
1179        for (action_str, value) in raw {
1180            // Try to parse action ID
1181            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            // Parse key bindings (can be string or array of strings)
1193            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            // Parse each key string
1222            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            // Only override if we got at least one valid key
1238            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    /// Look up the action for a key binding.
1252    #[must_use]
1253    pub fn lookup(&self, binding: &KeyBinding) -> Option<AppAction> {
1254        self.reverse.get(binding).copied()
1255    }
1256
1257    /// Return all actions bound to a key binding.
1258    ///
1259    /// Many bindings are context-dependent (e.g. `ctrl+d` can mean "delete forward" in the editor
1260    /// but "exit" when the editor is empty). Callers should resolve collisions based on UI state.
1261    #[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    /// Get all key bindings for an action.
1271    #[must_use]
1272    pub fn get_bindings(&self, action: AppAction) -> &[KeyBinding] {
1273        self.bindings.get(&action).map_or(&[], Vec::as_slice)
1274    }
1275
1276    /// Iterate all actions with their bindings (for /hotkeys display).
1277    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    /// Iterate actions in a category with their bindings.
1284    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        // Deterministic reverse map:
1298        // - iterate actions in stable order (AppAction::all)
1299        // - keep the first mapping for a given key (collisions are context-dependent)
1300        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    /// Default key bindings matching legacy Pi Agent.
1312    #[allow(clippy::too_many_lines)]
1313    fn default_bindings() -> HashMap<AppAction, Vec<KeyBinding>> {
1314        let mut m = HashMap::new();
1315
1316        // Cursor Movement
1317        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        // Deletion
1357        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        // Text Input
1377        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        // Kill Ring
1385        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        // Clipboard
1390        m.insert(AppAction::Copy, vec![KeyBinding::ctrl("c")]);
1391        m.insert(AppAction::PasteImage, vec![KeyBinding::ctrl("v")]);
1392
1393        // Application
1394        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        // Session (no default bindings)
1403        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        // Models & Thinking
1417        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        // Display
1429        m.insert(AppAction::ExpandTools, vec![KeyBinding::ctrl("o")]);
1430        m.insert(AppAction::ToggleThinking, vec![KeyBinding::ctrl("t")]);
1431
1432        // Message Queue
1433        m.insert(AppAction::FollowUp, vec![KeyBinding::alt("enter")]);
1434        m.insert(AppAction::Dequeue, vec![KeyBinding::alt("up")]);
1435
1436        // Selection (Lists, Pickers)
1437        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        // Session Picker
1451        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// ============================================================================
1475// Tests
1476// ============================================================================
1477
1478#[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        // Check cursor movement
1520        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        // Check ctrl+c maps to multiple actions (context-dependent)
1525        let ctrl_c = KeyBinding::ctrl("c");
1526        // Note: ctrl+c is bound to both Copy and Clear in legacy
1527        // The reverse lookup returns one of them
1528        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        // All actions should be iterable
1551        assert!(bindings.iter().next().is_some());
1552
1553        // Category iteration
1554        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            // Should not panic
1578            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    // ============================================================================
1601    // Key Parsing: Synonyms
1602    // ============================================================================
1603
1604    #[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    // ============================================================================
1623    // Key Parsing: Case Insensitivity
1624    // ============================================================================
1625
1626    #[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    // ============================================================================
1658    // Key Parsing: Special Keys
1659    // ============================================================================
1660
1661    #[test]
1662    fn test_parse_all_special_keys() {
1663        // All special keys from the spec should parse
1664        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        // Test f1-f20 (matching bubbletea KeyType coverage)
1692        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    // ============================================================================
1735    // Key Parsing: Modifiers
1736    // ============================================================================
1737
1738    #[test]
1739    fn test_parse_all_modifier_combinations() {
1740        // ctrl only
1741        let binding: KeyBinding = "ctrl+x".parse().unwrap();
1742        assert!(binding.modifiers.ctrl);
1743        assert!(!binding.modifiers.alt);
1744        assert!(!binding.modifiers.shift);
1745
1746        // alt only
1747        let binding: KeyBinding = "alt+x".parse().unwrap();
1748        assert!(!binding.modifiers.ctrl);
1749        assert!(binding.modifiers.alt);
1750        assert!(!binding.modifiers.shift);
1751
1752        // shift only
1753        let binding: KeyBinding = "shift+x".parse().unwrap();
1754        assert!(!binding.modifiers.ctrl);
1755        assert!(!binding.modifiers.alt);
1756        assert!(binding.modifiers.shift);
1757
1758        // ctrl+alt
1759        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        // ctrl+shift
1765        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        // alt+shift
1771        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        // all three
1777        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    // ============================================================================
1791    // Key Parsing: Error Cases
1792    // ============================================================================
1793
1794    #[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    // ============================================================================
1891    // Key Parsing: Normalization Stability
1892    // ============================================================================
1893
1894    #[test]
1895    fn test_normalization_output_stable() {
1896        // Regardless of input casing, output should be stable
1897        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    // ============================================================================
1918    // Key Parsing: Legacy Keybindings from Docs
1919    // ============================================================================
1920
1921    #[test]
1922    fn test_parse_all_legacy_default_bindings() {
1923        // All keys from the legacy keybindings.md should parse
1924        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    // ============================================================================
1986    // Utility Functions
1987    // ============================================================================
1988
1989    #[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    // ============================================================================
2027    // User Config Loading (bd-3qm)
2028    // ============================================================================
2029
2030    #[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        // Should return defaults with no warnings (missing file is normal)
2042        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        // Check overrides
2065        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        // Check single string value works
2070        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        // Should have 2 warnings for unknown actions
2092        assert_eq!(result.warnings.len(), 2);
2093        assert!(result.format_warnings().contains("unknownAction"));
2094        assert!(result.format_warnings().contains("anotherBadAction"));
2095
2096        // Valid action should still work
2097        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        // Should have 1 warning for invalid key
2117        assert_eq!(result.warnings.len(), 1);
2118        assert!(result.format_warnings().contains("invalidkey123"));
2119
2120        // Valid keys should still be applied
2121        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); // not 3
2125    }
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        // Should have 1 warning for parse error
2137        assert_eq!(result.warnings.len(), 1);
2138        assert!(matches!(
2139            result.warnings[0],
2140            KeyBindingsWarning::ParseError { .. }
2141        ));
2142
2143        // Should return defaults
2144        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        // Should have 1 warning for invalid value type
2164        assert_eq!(result.warnings.len(), 1);
2165        assert!(matches!(
2166            result.warnings[0],
2167            KeyBindingsWarning::InvalidKeyValue { .. }
2168        ));
2169
2170        // Valid action should still work
2171        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    // ============================================================================
2188    // KeyMsg → KeyBinding Conversion (bd-gze)
2189    // ============================================================================
2190
2191    #[test]
2192    fn test_from_bubbletea_key_ctrl_keys() {
2193        use bubbletea::{KeyMsg, KeyType};
2194
2195        // Test Ctrl+C
2196        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        // Test Ctrl+P
2203        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        // Enter
2214        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        // Escape
2219        let binding = KeyBinding::from_bubbletea_key(&KeyMsg::from_type(KeyType::Esc)).unwrap();
2220        assert_eq!(binding.key, "escape");
2221
2222        // Tab
2223        let binding = KeyBinding::from_bubbletea_key(&KeyMsg::from_type(KeyType::Tab)).unwrap();
2224        assert_eq!(binding.key, "tab");
2225
2226        // Backspace
2227        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        // Plain arrows
2237        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        // Shift+arrows
2242        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        // Ctrl+arrows
2247        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        // Ctrl+Shift+arrows
2253        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        // Alt+arrow
2265        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        // Alt+letter (via Runes)
2272        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        // Single character
2283        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        // Uppercase becomes lowercase
2289        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        // Multi-character input (e.g., IME) cannot be a keybinding
2299        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        // Paste events should not be keybindings
2308        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        // Ctrl+C should map to an action (Copy or Clear depending on context)
2344        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        // PageUp should map to PageUp action
2349        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        // Enter should map to Submit
2355        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    // ── Property tests ──────────────────────────────────────────────────
2362
2363    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                // Should never panic, even on arbitrary input
2508                let _ = parse_key_binding(&s);
2509            }
2510
2511            #[test]
2512            fn modifier_order_independence(
2513                key in arb_valid_key(),
2514            ) {
2515                // ctrl+alt+key vs alt+ctrl+key should parse identically
2516                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                // ctrl+shift+key vs shift+ctrl+key
2521                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                // Canonical order: ctrl before alt before shift before key
2537                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}