Skip to main content

ite_cli/
keys.rs

1//! Key representation and parsing of config key strings like `ctrl+e`.
2
3use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
4
5/// A normalized key: uppercase-char keys absorb the SHIFT modifier, so
6/// `shift+j`, `J`, and a crossterm event for shift-j all compare equal.
7#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
8pub struct Key {
9    pub code: KeyCode,
10    pub mods: KeyModifiers,
11}
12
13impl Key {
14    pub fn new(code: KeyCode, mods: KeyModifiers) -> Self {
15        Self { code, mods }.normalized()
16    }
17
18    fn normalized(mut self) -> Self {
19        if let KeyCode::Char(c) = self.code {
20            if self.mods.contains(KeyModifiers::SHIFT) && c.is_alphabetic() {
21                self.code = KeyCode::Char(c.to_ascii_uppercase());
22            }
23            // SHIFT carries no extra information for character keys.
24            self.mods.remove(KeyModifiers::SHIFT);
25        }
26        self
27    }
28
29    /// Parse a config key string such as `j`, `J`, `ctrl+e`, `shift+right`,
30    /// `ctrl+enter`, or `alt+s`.
31    pub fn parse(s: &str) -> Result<Self, String> {
32        let mut mods = KeyModifiers::NONE;
33        let parts: Vec<&str> = s.split('+').collect();
34        let (mod_parts, key_part) = match parts.split_last() {
35            Some((last, rest)) if !last.is_empty() => (rest, *last),
36            _ => return Err(format!("invalid key: {s:?}")),
37        };
38        for part in mod_parts {
39            match part.to_ascii_lowercase().as_str() {
40                "ctrl" | "control" => mods |= KeyModifiers::CONTROL,
41                "alt" | "meta" => mods |= KeyModifiers::ALT,
42                "shift" => mods |= KeyModifiers::SHIFT,
43                other => return Err(format!("unknown modifier: {other:?}")),
44            }
45        }
46        let code = parse_key_name(key_part)?;
47        Ok(Self::new(code, mods))
48    }
49
50    /// Normalize an incoming crossterm event into a `Key`.
51    pub fn from_event(ev: KeyEvent) -> Self {
52        Self::new(ev.code, ev.modifiers)
53    }
54}
55
56fn parse_key_name(name: &str) -> Result<KeyCode, String> {
57    let mut chars = name.chars();
58    if let (Some(c), None) = (chars.next(), chars.next()) {
59        return Ok(KeyCode::Char(c));
60    }
61    let code = match name.to_ascii_lowercase().as_str() {
62        "enter" | "return" => KeyCode::Enter,
63        "tab" => KeyCode::Tab,
64        "esc" | "escape" => KeyCode::Esc,
65        "space" => KeyCode::Char(' '),
66        "up" | "up-arrow" => KeyCode::Up,
67        "down" | "down-arrow" => KeyCode::Down,
68        "left" | "left-arrow" => KeyCode::Left,
69        "right" | "right-arrow" => KeyCode::Right,
70        "home" => KeyCode::Home,
71        "end" => KeyCode::End,
72        "pageup" | "page-up" => KeyCode::PageUp,
73        "pagedown" | "page-down" => KeyCode::PageDown,
74        "backspace" => KeyCode::Backspace,
75        "delete" | "del" => KeyCode::Delete,
76        "insert" => KeyCode::Insert,
77        f if f.starts_with('f') => {
78            let n: u8 = f[1..]
79                .parse()
80                .map_err(|_| format!("unknown key: {name:?}"))?;
81            if (1..=24).contains(&n) {
82                KeyCode::F(n)
83            } else {
84                return Err(format!("unknown key: {name:?}"));
85            }
86        }
87        _ => return Err(format!("unknown key: {name:?}")),
88    };
89    Ok(code)
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    fn key(code: KeyCode, mods: KeyModifiers) -> Key {
97        Key::new(code, mods)
98    }
99
100    #[test]
101    fn parses_bare_char() {
102        assert_eq!(
103            Key::parse("j").unwrap(),
104            key(KeyCode::Char('j'), KeyModifiers::NONE)
105        );
106    }
107
108    #[test]
109    fn parses_ctrl_char() {
110        assert_eq!(
111            Key::parse("ctrl+e").unwrap(),
112            key(KeyCode::Char('e'), KeyModifiers::CONTROL)
113        );
114    }
115
116    #[test]
117    fn parses_alt_char() {
118        assert_eq!(
119            Key::parse("alt+s").unwrap(),
120            key(KeyCode::Char('s'), KeyModifiers::ALT)
121        );
122    }
123
124    #[test]
125    fn shift_letter_normalizes_to_uppercase_char() {
126        // "shift+j" and "J" are the same key.
127        assert_eq!(Key::parse("shift+j").unwrap(), Key::parse("J").unwrap());
128        assert_eq!(
129            Key::parse("J").unwrap(),
130            key(KeyCode::Char('J'), KeyModifiers::NONE)
131        );
132    }
133
134    #[test]
135    fn parses_named_keys() {
136        assert_eq!(
137            Key::parse("enter").unwrap(),
138            key(KeyCode::Enter, KeyModifiers::NONE)
139        );
140        assert_eq!(
141            Key::parse("ctrl+enter").unwrap(),
142            key(KeyCode::Enter, KeyModifiers::CONTROL)
143        );
144        assert_eq!(
145            Key::parse("alt+enter").unwrap(),
146            key(KeyCode::Enter, KeyModifiers::ALT)
147        );
148        assert_eq!(
149            Key::parse("tab").unwrap(),
150            key(KeyCode::Tab, KeyModifiers::NONE)
151        );
152        assert_eq!(
153            Key::parse("shift+right").unwrap(),
154            key(KeyCode::Right, KeyModifiers::SHIFT)
155        );
156        assert_eq!(
157            Key::parse("esc").unwrap(),
158            key(KeyCode::Esc, KeyModifiers::NONE)
159        );
160        assert_eq!(
161            Key::parse("space").unwrap(),
162            key(KeyCode::Char(' '), KeyModifiers::NONE)
163        );
164    }
165
166    #[test]
167    fn rejects_unknown_keys_and_modifiers() {
168        assert!(Key::parse("bogus").is_err());
169        assert!(Key::parse("hyper+j").is_err());
170        assert!(Key::parse("").is_err());
171    }
172
173    #[test]
174    fn event_normalization_matches_parse() {
175        // Terminals report shift+j as Char('J') with SHIFT set.
176        let ev = KeyEvent::new(KeyCode::Char('J'), KeyModifiers::SHIFT);
177        assert_eq!(Key::from_event(ev), Key::parse("J").unwrap());
178
179        let ev = KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL);
180        assert_eq!(Key::from_event(ev), Key::parse("ctrl+e").unwrap());
181
182        // SHIFT is preserved for non-char keys.
183        let ev = KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT);
184        assert_eq!(Key::from_event(ev), Key::parse("shift+right").unwrap());
185    }
186}