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
35        .split_whitespace()
36        .flat_map(parse_binding_token)
37        .collect()
38}
39
40/// Parse a single chord token (e.g. `"ctrl+s"`), returning `None` if it is not
41/// recognised. The strict variant is [`try_parse_key`].
42pub fn parse_key(token: &str) -> Option<KeyChord> {
43    try_parse_key(token).ok()
44}
45
46/// Parse a whitespace-separated chord sequence, returning [`ParseKeyError`] on
47/// the first unrecognised token (or if the string has no tokens).
48///
49/// Use this when binding from a user-editable config or a remap UI so a typo
50/// like `"ctrl+shft+x"` is reported instead of silently ignored.
51pub fn try_parse_binding(binding: &str) -> Result<Vec<KeyChord>, ParseKeyError> {
52    let chords = binding
53        .split_whitespace()
54        .map(try_parse_binding_token)
55        .collect::<Result<Vec<_>, _>>()?;
56    let chords = chords.into_iter().flatten().collect::<Vec<_>>();
57    if chords.is_empty() {
58        return Err(ParseKeyError::Empty);
59    }
60    Ok(chords)
61}
62
63fn parse_binding_token(token: &str) -> Vec<KeyChord> {
64    try_parse_binding_token(token).unwrap_or_default()
65}
66
67fn try_parse_binding_token(token: &str) -> Result<Vec<KeyChord>, ParseKeyError> {
68    let parts = token.split('+').collect::<Vec<_>>();
69    if parts.len() <= 1 || is_modifier_sequence(&parts) {
70        return try_parse_key(token).map(|key| vec![key]);
71    }
72
73    parts
74        .into_iter()
75        .map(try_parse_key)
76        .collect::<Result<Vec<_>, _>>()
77        // A failure here means the token is neither a valid chord nor a valid
78        // sequence (e.g. a typo'd modifier like `ctrl+shft+x`). Report the whole
79        // token the user wrote, not the fragment that happened to fail first.
80        .map_err(|_| ParseKeyError::UnknownKey(token.to_string()))
81}
82
83fn is_modifier_sequence(parts: &[&str]) -> bool {
84    parts
85        .iter()
86        .take(parts.len().saturating_sub(1))
87        .all(|part| is_modifier(part))
88}
89
90fn is_modifier(part: &str) -> bool {
91    matches!(
92        part.to_ascii_lowercase().as_str(),
93        "ctrl" | "control" | "c" | "alt" | "meta" | "m" | "shift" | "s"
94    )
95}
96
97/// Parse a single chord token, returning [`ParseKeyError`] when it is not
98/// recognised. The lenient variant is [`parse_key`].
99pub fn try_parse_key(token: &str) -> Result<KeyChord, ParseKeyError> {
100    let mut modifiers = KeyModifiers::empty();
101    let mut key = token.trim();
102    if key.is_empty() {
103        return Err(ParseKeyError::Empty);
104    }
105
106    loop {
107        let Some((prefix, rest)) = key.split_once('+') else {
108            break;
109        };
110
111        match prefix.to_ascii_lowercase().as_str() {
112            "ctrl" | "control" | "c" => modifiers |= KeyModifiers::CONTROL,
113            "alt" | "meta" | "m" => modifiers |= KeyModifiers::ALT,
114            "shift" | "s" => modifiers |= KeyModifiers::SHIFT,
115            // Not a modifier — the remainder (including this prefix, e.g. the
116            // literal `+` key) is the key code.
117            _ => break,
118        }
119        key = rest;
120    }
121
122    let unknown = || ParseKeyError::UnknownKey(token.to_string());
123    let code = match key.to_ascii_lowercase().as_str() {
124        "enter" | "return" => KeyCode::Enter,
125        "tab" => KeyCode::Tab,
126        "backtab" => KeyCode::BackTab,
127        "esc" | "escape" => KeyCode::Esc,
128        "backspace" | "bs" => KeyCode::Backspace,
129        "space" => KeyCode::Char(' '),
130        "up" => KeyCode::Up,
131        "down" => KeyCode::Down,
132        "left" => KeyCode::Left,
133        "right" => KeyCode::Right,
134        "home" => KeyCode::Home,
135        "end" => KeyCode::End,
136        "pageup" | "page_up" => KeyCode::PageUp,
137        "pagedown" | "page_down" => KeyCode::PageDown,
138        "delete" | "del" => KeyCode::Delete,
139        "insert" | "ins" => KeyCode::Insert,
140        // Function keys `f1`..`f12`. A bare `f` falls through to the single
141        // character branch below so it can still be bound as the letter.
142        text if text.starts_with('f') && text.len() > 1 => {
143            let number = text[1..].parse().map_err(|_| unknown())?;
144            KeyCode::F(number)
145        }
146        text => {
147            let mut chars = text.chars();
148            let first = chars.next().ok_or_else(unknown)?;
149            if chars.next().is_some() {
150                return Err(unknown());
151            }
152            KeyCode::Char(first)
153        }
154    };
155
156    let (code, modifiers) = if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
157        (KeyCode::BackTab, modifiers - KeyModifiers::SHIFT)
158    } else {
159        (code, modifiers)
160    };
161
162    Ok(KeyChord::new(code, modifiers))
163}