Skip to main content

supercode_harness/tui/
keymap.rs

1//! P5-4 (§3.1 `capabilities.tui.keymap.<action> = "<key>"`, "configurable
2//! keybindings"): a name→[`KeyEvent`] table for the small set of GLOBAL
3//! actions a user can rebind, layered over sensible defaults. Modal-local
4//! navigation (arrow keys, `Enter`/`Esc` to confirm/cancel a prompt) is
5//! deliberately NOT part of this table — those are fixed, universal
6//! conventions, not a rebind surface — only the actions listed in
7//! [`KeymapAction::ALL`] are.
8
9use std::collections::HashMap;
10
11use super::key::{Key, KeyEvent};
12
13/// A rebindable global TUI action.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum KeymapAction {
16    /// Send the current input buffer as a turn.
17    Submit,
18    /// Insert a newline without submitting (multi-line composing).
19    Newline,
20    /// Quit the TUI (falls back to the REPL's own double-press-to-exit
21    /// convention at the state-machine level — see
22    /// [`crate::tui::state::TuiState::handle_key`]'s doc comment).
23    Quit,
24    /// Open cross-session prompt-history search (D5, "Ctrl+R-style
25    /// search").
26    HistorySearch,
27    /// Toggle the dark/light [`crate::tui::Theme`].
28    ToggleTheme,
29    /// Scroll the transcript up one page.
30    ScrollUp,
31    /// Scroll the transcript down one page.
32    ScrollDown,
33    /// Open `$EDITOR` on the current input buffer.
34    ExternalEditor,
35}
36
37impl KeymapAction {
38    /// Every action, for iterating a whole keymap (defaults, config
39    /// parsing).
40    pub const ALL: &'static [KeymapAction] = &[
41        KeymapAction::Submit,
42        KeymapAction::Newline,
43        KeymapAction::Quit,
44        KeymapAction::HistorySearch,
45        KeymapAction::ToggleTheme,
46        KeymapAction::ScrollUp,
47        KeymapAction::ScrollDown,
48        KeymapAction::ExternalEditor,
49    ];
50
51    /// The `capabilities.tui.keymap.<name>` config key this action reads
52    /// from.
53    pub fn name(self) -> &'static str {
54        match self {
55            KeymapAction::Submit => "submit",
56            KeymapAction::Newline => "newline",
57            KeymapAction::Quit => "quit",
58            KeymapAction::HistorySearch => "history_search",
59            KeymapAction::ToggleTheme => "toggle_theme",
60            KeymapAction::ScrollUp => "scroll_up",
61            KeymapAction::ScrollDown => "scroll_down",
62            KeymapAction::ExternalEditor => "external_editor",
63        }
64    }
65
66    /// This action's built-in default binding.
67    pub fn default_key(self) -> KeyEvent {
68        match self {
69            KeymapAction::Submit => KeyEvent::plain(Key::Enter),
70            KeymapAction::Newline => KeyEvent {
71                key: Key::Enter,
72                ctrl: false,
73                alt: true,
74                shift: false,
75            },
76            KeymapAction::Quit => KeyEvent::ctrl(Key::Char('c')),
77            KeymapAction::HistorySearch => KeyEvent::ctrl(Key::Char('r')),
78            KeymapAction::ToggleTheme => KeyEvent::ctrl(Key::Char('t')),
79            KeymapAction::ScrollUp => KeyEvent::plain(Key::PageUp),
80            KeymapAction::ScrollDown => KeyEvent::plain(Key::PageDown),
81            KeymapAction::ExternalEditor => KeyEvent::ctrl(Key::Char('e')),
82        }
83    }
84}
85
86/// The resolved action→key table — [`KeymapAction::default_key`] for every
87/// action, with any [`Self::with_overrides`] substitutions applied.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct Keymap {
90    bindings: HashMap<KeymapAction, KeyEvent>,
91}
92
93impl Default for Keymap {
94    fn default() -> Self {
95        let bindings = KeymapAction::ALL
96            .iter()
97            .map(|&a| (a, a.default_key()))
98            .collect();
99        Keymap { bindings }
100    }
101}
102
103impl Keymap {
104    /// Build a keymap from [`Config::tui_keymap`](crate::Config)-shaped
105    /// overrides (`action name` → key spec string, [`KeyEvent::parse`]
106    /// syntax). An unknown action name, or a key spec that fails to parse,
107    /// is silently skipped (keeps the default for that action) — a typo in
108    /// a user's keymap table degrades to "unchanged", never a startup
109    /// failure or a panic.
110    pub fn with_overrides(overrides: &HashMap<String, String>) -> Self {
111        let mut km = Keymap::default();
112        for action in KeymapAction::ALL {
113            if let Some(spec) = overrides.get(action.name()) {
114                if let Some(key) = KeyEvent::parse(spec) {
115                    km.bindings.insert(*action, key);
116                }
117            }
118        }
119        km
120    }
121
122    /// The key currently bound to `action`.
123    pub fn key_for(&self, action: KeymapAction) -> KeyEvent {
124        self.bindings
125            .get(&action)
126            .copied()
127            .unwrap_or_else(|| action.default_key())
128    }
129
130    /// Which action (if any) `key` is bound to.
131    pub fn action_for(&self, key: KeyEvent) -> Option<KeymapAction> {
132        self.bindings
133            .iter()
134            .find(|(_, &bound)| bound == key)
135            .map(|(&a, _)| a)
136    }
137}