Skip to main content

tui_pages/input/
key_sequence.rs

1use crate::input::KeyChord;
2use crossterm::event::{KeyCode, KeyModifiers};
3use std::fmt;
4
5/// Why a binding string failed to parse. Returned by [`try_parse_key`] and
6/// [`try_parse_binding`]; the lenient [`parse_key`] / [`parse_binding`] drop
7/// the offending token instead.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum ParseKeyError {
10    /// The binding string was empty or contained no tokens.
11    Empty,
12    /// A token could not be recognised as a key (a typo, or a multi-character
13    /// name that is not a known key). Carries the offending token.
14    UnknownKey(String),
15}
16
17impl fmt::Display for ParseKeyError {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match self {
20            ParseKeyError::Empty => write!(f, "empty key binding"),
21            ParseKeyError::UnknownKey(token) => write!(f, "unrecognised key token: {token:?}"),
22        }
23    }
24}
25
26impl std::error::Error for ParseKeyError {}
27
28/// Parse a whitespace-separated chord sequence (e.g. `"ctrl+shift+x z"`),
29/// silently dropping any token that does not parse.
30///
31/// Convenient for trusted, in-code bindings. For user-supplied config where a
32/// typo should surface rather than vanish, use [`try_parse_binding`].
33pub fn parse_binding(binding: &str) -> Vec<KeyChord> {
34    binding.split_whitespace().filter_map(parse_key).collect()
35}
36
37/// Parse a single chord token (e.g. `"ctrl+s"`), returning `None` if it is not
38/// recognised. The strict variant is [`try_parse_key`].
39pub fn parse_key(token: &str) -> Option<KeyChord> {
40    try_parse_key(token).ok()
41}
42
43/// Parse a whitespace-separated chord sequence, returning [`ParseKeyError`] on
44/// the first unrecognised token (or if the string has no tokens).
45///
46/// Use this when binding from a user-editable config or a remap UI so a typo
47/// like `"ctrl+shft+x"` is reported instead of silently ignored.
48pub fn try_parse_binding(binding: &str) -> Result<Vec<KeyChord>, ParseKeyError> {
49    let chords = binding
50        .split_whitespace()
51        .map(try_parse_key)
52        .collect::<Result<Vec<_>, _>>()?;
53    if chords.is_empty() {
54        return Err(ParseKeyError::Empty);
55    }
56    Ok(chords)
57}
58
59/// Parse a single chord token, returning [`ParseKeyError`] when it is not
60/// recognised. The lenient variant is [`parse_key`].
61pub fn try_parse_key(token: &str) -> Result<KeyChord, ParseKeyError> {
62    let mut modifiers = KeyModifiers::empty();
63    let mut key = token.trim();
64    if key.is_empty() {
65        return Err(ParseKeyError::Empty);
66    }
67
68    loop {
69        let Some((prefix, rest)) = key.split_once('+') else {
70            break;
71        };
72
73        match prefix.to_ascii_lowercase().as_str() {
74            "ctrl" | "control" | "c" => modifiers |= KeyModifiers::CONTROL,
75            "alt" | "meta" | "m" => modifiers |= KeyModifiers::ALT,
76            "shift" | "s" => modifiers |= KeyModifiers::SHIFT,
77            // Not a modifier — the remainder (including this prefix, e.g. the
78            // literal `+` key) is the key code.
79            _ => break,
80        }
81        key = rest;
82    }
83
84    let unknown = || ParseKeyError::UnknownKey(token.to_string());
85    let code = match key.to_ascii_lowercase().as_str() {
86        "enter" | "return" => KeyCode::Enter,
87        "tab" => KeyCode::Tab,
88        "backtab" => KeyCode::BackTab,
89        "esc" | "escape" => KeyCode::Esc,
90        "backspace" | "bs" => KeyCode::Backspace,
91        "space" => KeyCode::Char(' '),
92        "up" => KeyCode::Up,
93        "down" => KeyCode::Down,
94        "left" => KeyCode::Left,
95        "right" => KeyCode::Right,
96        "home" => KeyCode::Home,
97        "end" => KeyCode::End,
98        "pageup" | "page_up" => KeyCode::PageUp,
99        "pagedown" | "page_down" => KeyCode::PageDown,
100        "delete" | "del" => KeyCode::Delete,
101        "insert" | "ins" => KeyCode::Insert,
102        // Function keys `f1`..`f12`. A bare `f` falls through to the single
103        // character branch below so it can still be bound as the letter.
104        text if text.starts_with('f') && text.len() > 1 => {
105            let number = text[1..].parse().map_err(|_| unknown())?;
106            KeyCode::F(number)
107        }
108        text => {
109            let mut chars = text.chars();
110            let first = chars.next().ok_or_else(unknown)?;
111            if chars.next().is_some() {
112                return Err(unknown());
113            }
114            KeyCode::Char(first)
115        }
116    };
117
118    let (code, modifiers) = if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
119        (KeyCode::BackTab, modifiers - KeyModifiers::SHIFT)
120    } else {
121        (code, modifiers)
122    };
123
124    Ok(KeyChord::new(code, modifiers))
125}