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