supercode-harness 0.4.13

The optional native Supercode agent and tool harness
Documentation
//! P5-4 (§2 module 30): a terminal-library-agnostic key event — the
//! [`crate::tui::state::TuiState::handle_key`] state machine consumes THIS
//! type, not `crossterm::event::KeyEvent`, so the whole view-model core
//! stays free of a `crossterm`/`ratatui` dependency (neither is in
//! `crates/harness`'s `Cargo.toml` — see that crate's own doc comment for why:
//! a real terminal can't be driven in a unit test, but this struct can be
//! constructed by hand). `crates/cli`'s render/event-loop layer is the one
//! place that translates a real `crossterm::event::KeyEvent` into this
//! shape before handing it to the state machine.

/// One logical key, independent of any terminal library's own enum.
/// `#[non_exhaustive]` so a future key (e.g. a specific F-key past `F(12)`,
/// or a media key) can be added without breaking a downstream `match`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Key {
    /// A printable character (already case-correct — `Shift` is folded in
    /// by the translator, not carried as a separate modifier for letters).
    Char(char),
    /// Enter/Return.
    Enter,
    /// Escape.
    Escape,
    /// Backspace.
    Backspace,
    /// Delete (forward-delete).
    Delete,
    /// Tab.
    Tab,
    /// Shift+Tab.
    BackTab,
    /// Left arrow.
    Left,
    /// Right arrow.
    Right,
    /// Up arrow.
    Up,
    /// Down arrow.
    Down,
    /// Home.
    Home,
    /// End.
    End,
    /// Page Up.
    PageUp,
    /// Page Down.
    PageDown,
    /// A function key, `F(1)..=F(12)`.
    F(u8),
}

/// A [`Key`] plus the modifier keys held with it. Named fields (not a
/// bitflag) so a test/keymap-string reader can construct one by hand
/// without needing to know a bit layout.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KeyEvent {
    /// The key itself.
    pub key: Key,
    /// Ctrl held.
    pub ctrl: bool,
    /// Alt held.
    pub alt: bool,
    /// Shift held. Deliberately still tracked even for [`Key::Char`] (whose
    /// case already reflects shift) because some non-alpha chars (arrows,
    /// tab) have a distinct shifted meaning (`BackTab` for Shift+Tab is
    /// modeled as its own [`Key`] variant by the translator, not via this
    /// flag — this flag exists for completeness/testability of a caller
    /// that wants to distinguish, e.g., a raw Shift+Left selection chord in
    /// a future extension).
    pub shift: bool,
}

impl KeyEvent {
    /// A plain, unmodified key — the common case in tests/default bindings.
    pub const fn plain(key: Key) -> Self {
        KeyEvent {
            key,
            ctrl: false,
            alt: false,
            shift: false,
        }
    }

    /// A Ctrl+`key` chord.
    pub const fn ctrl(key: Key) -> Self {
        KeyEvent {
            key,
            ctrl: true,
            alt: false,
            shift: false,
        }
    }

    /// A plain printable character — shorthand for
    /// `KeyEvent::plain(Key::Char(c))`, the single most common test/keymap
    /// construction.
    pub const fn ch(c: char) -> Self {
        KeyEvent::plain(Key::Char(c))
    }

    /// Parse the small keymap-string syntax `capabilities.tui.keymap.<action>
    /// = "<key>"` (§3.1) understands: an optional `ctrl+`/`alt+`/`shift+`
    /// prefix (any order, `+`-joined) followed by either a single
    /// character or a named key (`enter`, `esc`/`escape`, `backspace`,
    /// `delete`/`del`, `tab`, `up`, `down`, `left`, `right`, `home`, `end`,
    /// `pageup`/`pgup`, `pagedown`/`pgdn`, `f1`..`f12`). Case-insensitive
    /// except the bare single-character form, which is taken literally (so
    /// `"R"` and `"r"` are distinct plain characters, while `"Ctrl+R"` and
    /// `"ctrl+r"` are the same chord). Returns `None` for anything it
    /// doesn't recognize — a caller (`crate::tui::Keymap::with_overrides`)
    /// treats an unparseable override as "keep the default", never a panic
    /// or a silently-wrong binding.
    pub fn parse(spec: &str) -> Option<Self> {
        let mut ctrl = false;
        let mut alt = false;
        let mut shift = false;
        let mut last = spec.trim();
        loop {
            let lower = last.to_ascii_lowercase();
            if let Some(rest) = lower.strip_prefix("ctrl+") {
                ctrl = true;
                last = &last[last.len() - rest.len()..];
            } else if let Some(rest) = lower.strip_prefix("alt+") {
                alt = true;
                last = &last[last.len() - rest.len()..];
            } else if let Some(rest) = lower.strip_prefix("shift+") {
                shift = true;
                last = &last[last.len() - rest.len()..];
            } else {
                break;
            }
        }
        let key = if last.chars().count() == 1 {
            Key::Char(last.chars().next()?)
        } else {
            match last.to_ascii_lowercase().as_str() {
                "enter" | "return" => Key::Enter,
                "esc" | "escape" => Key::Escape,
                "backspace" => Key::Backspace,
                "delete" | "del" => Key::Delete,
                "tab" => Key::Tab,
                "backtab" => Key::BackTab,
                "up" => Key::Up,
                "down" => Key::Down,
                "left" => Key::Left,
                "right" => Key::Right,
                "home" => Key::Home,
                "end" => Key::End,
                "pageup" | "pgup" => Key::PageUp,
                "pagedown" | "pgdn" => Key::PageDown,
                other if other.starts_with('f') && other.len() <= 3 => {
                    let n: u8 = other[1..].parse().ok()?;
                    if (1..=12).contains(&n) {
                        Key::F(n)
                    } else {
                        return None;
                    }
                }
                _ => return None,
            }
        };
        Some(KeyEvent {
            key,
            ctrl,
            alt,
            shift,
        })
    }
}

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

    #[test]
    fn parse_plain_char() {
        assert_eq!(KeyEvent::parse("r"), Some(KeyEvent::ch('r')));
        assert_eq!(KeyEvent::parse("R"), Some(KeyEvent::ch('R')));
    }

    #[test]
    fn parse_ctrl_chord_case_insensitive() {
        assert_eq!(
            KeyEvent::parse("Ctrl+R"),
            Some(KeyEvent::ctrl(Key::Char('R')))
        );
        assert_eq!(
            KeyEvent::parse("ctrl+r"),
            Some(KeyEvent::ctrl(Key::Char('r')))
        );
    }

    #[test]
    fn parse_named_keys() {
        assert_eq!(KeyEvent::parse("enter"), Some(KeyEvent::plain(Key::Enter)));
        assert_eq!(KeyEvent::parse("Esc"), Some(KeyEvent::plain(Key::Escape)));
        assert_eq!(
            KeyEvent::parse("ctrl+pgup"),
            Some(KeyEvent::ctrl(Key::PageUp))
        );
    }

    #[test]
    fn parse_function_keys() {
        assert_eq!(KeyEvent::parse("f1"), Some(KeyEvent::plain(Key::F(1))));
        assert_eq!(KeyEvent::parse("F12"), Some(KeyEvent::plain(Key::F(12))));
        assert_eq!(KeyEvent::parse("f13"), None);
        assert_eq!(KeyEvent::parse("f0"), None);
    }

    #[test]
    fn parse_multiple_modifiers() {
        assert_eq!(
            KeyEvent::parse("ctrl+alt+x"),
            Some(KeyEvent {
                key: Key::Char('x'),
                ctrl: true,
                alt: true,
                shift: false
            })
        );
    }

    #[test]
    fn parse_unknown_returns_none() {
        assert_eq!(KeyEvent::parse(""), None);
        assert_eq!(KeyEvent::parse("bogus-key"), None);
        assert_eq!(KeyEvent::parse("ctrl+"), None);
    }
}