supercode-harness 0.4.3

The optional native Supercode agent and tool harness
Documentation
//! P5-4 (§3.1 `capabilities.tui.keymap.<action> = "<key>"`, "configurable
//! keybindings"): a name→[`KeyEvent`] table for the small set of GLOBAL
//! actions a user can rebind, layered over sensible defaults. Modal-local
//! navigation (arrow keys, `Enter`/`Esc` to confirm/cancel a prompt) is
//! deliberately NOT part of this table — those are fixed, universal
//! conventions, not a rebind surface — only the actions listed in
//! [`KeymapAction::ALL`] are.

use std::collections::HashMap;

use super::key::{Key, KeyEvent};

/// A rebindable global TUI action.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeymapAction {
    /// Send the current input buffer as a turn.
    Submit,
    /// Insert a newline without submitting (multi-line composing).
    Newline,
    /// Quit the TUI (falls back to the REPL's own double-press-to-exit
    /// convention at the state-machine level — see
    /// [`crate::tui::state::TuiState::handle_key`]'s doc comment).
    Quit,
    /// Open cross-session prompt-history search (D5, "Ctrl+R-style
    /// search").
    HistorySearch,
    /// Toggle the dark/light [`crate::tui::Theme`].
    ToggleTheme,
    /// Scroll the transcript up one page.
    ScrollUp,
    /// Scroll the transcript down one page.
    ScrollDown,
    /// Open `$EDITOR` on the current input buffer.
    ExternalEditor,
}

impl KeymapAction {
    /// Every action, for iterating a whole keymap (defaults, config
    /// parsing).
    pub const ALL: &'static [KeymapAction] = &[
        KeymapAction::Submit,
        KeymapAction::Newline,
        KeymapAction::Quit,
        KeymapAction::HistorySearch,
        KeymapAction::ToggleTheme,
        KeymapAction::ScrollUp,
        KeymapAction::ScrollDown,
        KeymapAction::ExternalEditor,
    ];

    /// The `capabilities.tui.keymap.<name>` config key this action reads
    /// from.
    pub fn name(self) -> &'static str {
        match self {
            KeymapAction::Submit => "submit",
            KeymapAction::Newline => "newline",
            KeymapAction::Quit => "quit",
            KeymapAction::HistorySearch => "history_search",
            KeymapAction::ToggleTheme => "toggle_theme",
            KeymapAction::ScrollUp => "scroll_up",
            KeymapAction::ScrollDown => "scroll_down",
            KeymapAction::ExternalEditor => "external_editor",
        }
    }

    /// This action's built-in default binding.
    pub fn default_key(self) -> KeyEvent {
        match self {
            KeymapAction::Submit => KeyEvent::plain(Key::Enter),
            KeymapAction::Newline => KeyEvent {
                key: Key::Enter,
                ctrl: false,
                alt: true,
                shift: false,
            },
            KeymapAction::Quit => KeyEvent::ctrl(Key::Char('c')),
            KeymapAction::HistorySearch => KeyEvent::ctrl(Key::Char('r')),
            KeymapAction::ToggleTheme => KeyEvent::ctrl(Key::Char('t')),
            KeymapAction::ScrollUp => KeyEvent::plain(Key::PageUp),
            KeymapAction::ScrollDown => KeyEvent::plain(Key::PageDown),
            KeymapAction::ExternalEditor => KeyEvent::ctrl(Key::Char('e')),
        }
    }
}

/// The resolved action→key table — [`KeymapAction::default_key`] for every
/// action, with any [`Self::with_overrides`] substitutions applied.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Keymap {
    bindings: HashMap<KeymapAction, KeyEvent>,
}

impl Default for Keymap {
    fn default() -> Self {
        let bindings = KeymapAction::ALL
            .iter()
            .map(|&a| (a, a.default_key()))
            .collect();
        Keymap { bindings }
    }
}

impl Keymap {
    /// Build a keymap from [`Config::tui_keymap`](crate::Config)-shaped
    /// overrides (`action name` → key spec string, [`KeyEvent::parse`]
    /// syntax). An unknown action name, or a key spec that fails to parse,
    /// is silently skipped (keeps the default for that action) — a typo in
    /// a user's keymap table degrades to "unchanged", never a startup
    /// failure or a panic.
    pub fn with_overrides(overrides: &HashMap<String, String>) -> Self {
        let mut km = Keymap::default();
        for action in KeymapAction::ALL {
            if let Some(spec) = overrides.get(action.name()) {
                if let Some(key) = KeyEvent::parse(spec) {
                    km.bindings.insert(*action, key);
                }
            }
        }
        km
    }

    /// The key currently bound to `action`.
    pub fn key_for(&self, action: KeymapAction) -> KeyEvent {
        self.bindings
            .get(&action)
            .copied()
            .unwrap_or_else(|| action.default_key())
    }

    /// Which action (if any) `key` is bound to.
    pub fn action_for(&self, key: KeyEvent) -> Option<KeymapAction> {
        self.bindings
            .iter()
            .find(|(_, &bound)| bound == key)
            .map(|(&a, _)| a)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_keymap_round_trips_every_action() {
        let km = Keymap::default();
        for &action in KeymapAction::ALL {
            assert_eq!(km.key_for(action), action.default_key());
            assert_eq!(km.action_for(action.default_key()), Some(action));
        }
    }

    #[test]
    fn override_replaces_one_binding_leaves_others_default() {
        let mut overrides = HashMap::new();
        overrides.insert("history_search".to_string(), "ctrl+h".to_string());
        let km = Keymap::with_overrides(&overrides);
        assert_eq!(
            km.key_for(KeymapAction::HistorySearch),
            KeyEvent::ctrl(Key::Char('h'))
        );
        // Unrelated action keeps its default.
        assert_eq!(
            km.key_for(KeymapAction::Submit),
            KeymapAction::Submit.default_key()
        );
    }

    #[test]
    fn unknown_action_name_is_ignored() {
        let mut overrides = HashMap::new();
        overrides.insert("not_a_real_action".to_string(), "ctrl+z".to_string());
        let km = Keymap::with_overrides(&overrides);
        assert_eq!(km, Keymap::default());
    }

    #[test]
    fn unparseable_key_spec_keeps_default() {
        let mut overrides = HashMap::new();
        overrides.insert("submit".to_string(), "not a key".to_string());
        let km = Keymap::with_overrides(&overrides);
        assert_eq!(
            km.key_for(KeymapAction::Submit),
            KeymapAction::Submit.default_key()
        );
    }

    #[test]
    fn action_for_unbound_key_is_none() {
        let km = Keymap::default();
        assert_eq!(km.action_for(KeyEvent::ch('q')), None);
    }
}