Skip to main content

buffr_modal/
key.rs

1//! Vim-notation key parser.
2//!
3//! Parses strings like `<C-w>v`, `gT`, `<leader>fa`, `<C-S-Tab>` into
4//! a list of [`KeyChord`]s the page-mode dispatcher can match against.
5//!
6//! Notation supported:
7//!
8//! - `<C-...>` — Ctrl
9//! - `<S-...>` — Shift (also implied by uppercase ASCII letters when
10//!   bare)
11//! - `<M-...>` / `<A-...>` — Alt / Meta
12//! - `<D-...>` — Super (Cmd on macOS)
13//! - `<leader>` — abstract placeholder; resolution to a concrete char
14//!   happens at trie-build time, not here.
15//! - `<Space>`, `<Esc>`, `<CR>` / `<Enter>`, `<BS>` / `<Backspace>`,
16//!   `<Tab>`, `<S-Tab>` / `<BackTab>`, `<Up>`, `<Down>`, `<Left>`,
17//!   `<Right>`, `<Home>`, `<End>`, `<PageUp>`, `<PageDown>`,
18//!   `<Insert>`, `<Delete>`, `<F1>`–`<F12>`
19//! - Bare characters, including punctuation: `g`, `T`, `,`, `;`, `:`,
20//!   `/`, `?`, `<` (escaped as `<lt>`).
21//!
22//! Whitespace in the input is ignored. Empty input yields an empty
23//! `Vec`. Embedded literals like `Hello` parse as five separate
24//! chords (one per char) — this parser is *not* a string-mode parser.
25
26use bitflags::bitflags;
27use std::fmt;
28
29bitflags! {
30    /// Modifier set for one [`KeyChord`].
31    ///
32    /// Vim notation maps as: `C-` → [`Modifiers::CTRL`], `S-` →
33    /// [`Modifiers::SHIFT`], `M-`/`A-` → [`Modifiers::ALT`], `D-` →
34    /// [`Modifiers::SUPER`].
35    #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
36    pub struct Modifiers: u8 {
37        const CTRL  = 0b0001;
38        const SHIFT = 0b0010;
39        const ALT   = 0b0100;
40        const SUPER = 0b1000;
41    }
42}
43
44/// One position in a chord sequence.
45///
46/// Field order is `modifiers` then `key` so debug output reads as
47/// `KeyChord { modifiers: CTRL, key: Char('w') }` — matches how vim
48/// docs render these.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub struct KeyChord {
51    pub modifiers: Modifiers,
52    pub key: Key,
53}
54
55impl KeyChord {
56    pub const fn new(modifiers: Modifiers, key: Key) -> Self {
57        Self { modifiers, key }
58    }
59
60    pub const fn plain(key: Key) -> Self {
61        Self {
62            modifiers: Modifiers::empty(),
63            key,
64        }
65    }
66
67    pub const fn char(c: char) -> Self {
68        Self::plain(Key::Char(c))
69    }
70}
71
72/// What was pressed.
73///
74/// `Char` carries the printable codepoint; `Named(NamedKey)` covers
75/// the discrete keys vim notation has dedicated names for. `Leader`
76/// is abstract and gets resolved by the keymap when bindings are
77/// inserted.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
79pub enum Key {
80    Char(char),
81    Named(NamedKey),
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85pub enum NamedKey {
86    Esc,
87    /// `<CR>` / `<Enter>` / `<Return>`.
88    CR,
89    Tab,
90    /// `<S-Tab>` / `<BackTab>`. Distinct atom because some terminals
91    /// emit it as its own keysym rather than Shift+Tab.
92    BackTab,
93    BS,
94    Space,
95    Up,
96    Down,
97    Left,
98    Right,
99    Home,
100    End,
101    PageUp,
102    PageDown,
103    Insert,
104    Delete,
105    F(u8),
106    /// Abstract leader placeholder. The keymap resolves this to a
107    /// concrete `Char` at bind time using its configured leader.
108    Leader,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
112pub enum ParseError {
113    #[error("unknown key name <{0}>")]
114    UnknownNamed(String),
115    #[error("unclosed `<` in key notation")]
116    UnclosedBracket,
117    #[error("empty `<...>` block")]
118    EmptyBracket,
119    #[error("modifier `<{0}->` with no following key")]
120    DanglingModifier(String),
121    #[error("expected exactly one chord, got {0}")]
122    ExpectedOneChord(usize),
123}
124
125/// Parse `input` into a sequence of [`KeyChord`]s.
126pub fn parse_keys(input: &str) -> Result<Vec<KeyChord>, ParseError> {
127    let mut out = Vec::new();
128    let mut chars = input.chars().peekable();
129    while let Some(c) = chars.next() {
130        // Drop ASCII whitespace between chords; configs may format
131        // bindings with spaces for readability (`<C-w> v`).
132        if c.is_ascii_whitespace() {
133            continue;
134        }
135        if c == '<' {
136            let mut name = String::new();
137            let mut closed = false;
138            for nc in chars.by_ref() {
139                if nc == '>' {
140                    closed = true;
141                    break;
142                }
143                name.push(nc);
144            }
145            if !closed {
146                return Err(ParseError::UnclosedBracket);
147            }
148            if name.is_empty() {
149                return Err(ParseError::EmptyBracket);
150            }
151            out.push(parse_named(&name)?);
152        } else {
153            out.push(parse_char(c));
154        }
155    }
156    Ok(out)
157}
158
159/// Parse exactly one chord. Errors if the input parses to zero chords
160/// or more than one.
161pub fn parse_key(input: &str) -> Result<KeyChord, ParseError> {
162    let chords = parse_keys(input)?;
163    if chords.len() != 1 {
164        return Err(ParseError::ExpectedOneChord(chords.len()));
165    }
166    Ok(chords.into_iter().next().expect("len checked above"))
167}
168
169fn parse_char(c: char) -> KeyChord {
170    let mut mods = Modifiers::empty();
171    if c.is_ascii_uppercase() {
172        mods |= Modifiers::SHIFT;
173    }
174    KeyChord {
175        modifiers: mods,
176        key: Key::Char(c),
177    }
178}
179
180fn parse_named(raw: &str) -> Result<KeyChord, ParseError> {
181    // Special: `<lt>` is the literal `<`.
182    if raw.eq_ignore_ascii_case("lt") {
183        return Ok(KeyChord::char('<'));
184    }
185
186    let (mods, tail) = parse_modifiers(raw);
187
188    if tail.is_empty() {
189        return Err(ParseError::DanglingModifier(raw.to_string()));
190    }
191
192    // Resolve the tail.
193    let key = if tail.eq_ignore_ascii_case("leader") {
194        Key::Named(NamedKey::Leader)
195    } else if tail.eq_ignore_ascii_case("space") {
196        // `<Space>` and a literal ` ` chord must produce the same
197        // canonical form so a leader=' ' binding matches whether the
198        // user wrote `<leader>p` or `<Space>p`. Adapter does the same
199        // mapping on input.
200        Key::Char(' ')
201    } else if tail.chars().count() == 1 {
202        // `chars().count() == 1` so we don't slice on a multi-byte
203        // char by accident.
204        let ch = tail.chars().next().expect("len checked above");
205        // Ctrl+letter is case-insensitive; preserve case otherwise so
206        // `<S-a>` and `<S-A>` parse the same.
207        let ch = if mods.contains(Modifiers::CTRL) {
208            ch.to_ascii_lowercase()
209        } else if mods.contains(Modifiers::SHIFT) && ch.is_ascii_alphabetic() {
210            ch.to_ascii_uppercase()
211        } else {
212            ch
213        };
214        Key::Char(ch)
215    } else {
216        Key::Named(parse_named_key(tail)?)
217    };
218
219    Ok(KeyChord {
220        modifiers: mods,
221        key,
222    })
223}
224
225/// Strip leading `C-` / `S-` / `M-` / `A-` / `D-` prefixes (ASCII
226/// case-insensitive) and return the remaining tail.
227fn parse_modifiers(raw: &str) -> (Modifiers, &str) {
228    let mut mods = Modifiers::empty();
229    let mut tail = raw;
230    loop {
231        let lower_prefix = tail.get(..2).map(str::to_ascii_lowercase);
232        match lower_prefix.as_deref() {
233            Some("c-") => {
234                mods |= Modifiers::CTRL;
235                tail = &tail[2..];
236            }
237            Some("s-") => {
238                mods |= Modifiers::SHIFT;
239                tail = &tail[2..];
240            }
241            Some("m-") | Some("a-") => {
242                mods |= Modifiers::ALT;
243                tail = &tail[2..];
244            }
245            Some("d-") => {
246                mods |= Modifiers::SUPER;
247                tail = &tail[2..];
248            }
249            _ => return (mods, tail),
250        }
251    }
252}
253
254fn parse_named_key(name: &str) -> Result<NamedKey, ParseError> {
255    let n = name.to_ascii_lowercase();
256    Ok(match n.as_str() {
257        "esc" | "escape" => NamedKey::Esc,
258        "cr" | "enter" | "return" => NamedKey::CR,
259        "bs" | "backspace" => NamedKey::BS,
260        "tab" => NamedKey::Tab,
261        "backtab" => NamedKey::BackTab,
262        "space" => NamedKey::Space,
263        "up" => NamedKey::Up,
264        "down" => NamedKey::Down,
265        "left" => NamedKey::Left,
266        "right" => NamedKey::Right,
267        "home" => NamedKey::Home,
268        "end" => NamedKey::End,
269        "pageup" | "pgup" => NamedKey::PageUp,
270        "pagedown" | "pgdn" => NamedKey::PageDown,
271        "insert" | "ins" => NamedKey::Insert,
272        "delete" | "del" => NamedKey::Delete,
273        s if s.starts_with('f') => {
274            let num: u8 = s[1..]
275                .parse()
276                .map_err(|_| ParseError::UnknownNamed(name.to_string()))?;
277            if !(1..=12).contains(&num) {
278                return Err(ParseError::UnknownNamed(name.to_string()));
279            }
280            NamedKey::F(num)
281        }
282        _ => return Err(ParseError::UnknownNamed(name.to_string())),
283    })
284}
285
286impl fmt::Display for Modifiers {
287    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288        bitflags::parser::to_writer(self, f)
289    }
290}
291
292impl fmt::Display for NamedKey {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        let s = match self {
295            NamedKey::Esc => "Esc",
296            NamedKey::CR => "CR",
297            NamedKey::Tab => "Tab",
298            NamedKey::BackTab => "S-Tab",
299            NamedKey::BS => "BS",
300            NamedKey::Space => "Space",
301            NamedKey::Up => "Up",
302            NamedKey::Down => "Down",
303            NamedKey::Left => "Left",
304            NamedKey::Right => "Right",
305            NamedKey::Home => "Home",
306            NamedKey::End => "End",
307            NamedKey::PageUp => "PageUp",
308            NamedKey::PageDown => "PageDown",
309            NamedKey::Insert => "Insert",
310            NamedKey::Delete => "Delete",
311            NamedKey::F(n) => return write!(f, "F{n}"),
312            NamedKey::Leader => "leader",
313        };
314        f.write_str(s)
315    }
316}
317
318impl fmt::Display for KeyChord {
319    /// Render as vim notation — `<C-w>`, `gT`, `<S-Tab>`. Bare ASCII
320    /// chars without modifiers (or with only the implicit Shift on an
321    /// uppercase letter) print without angle brackets to match how
322    /// users write the binding in their config.
323    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324        let mods = self.modifiers;
325        // Bare chord (no modifiers) — letter / punctuation prints raw.
326        if mods.is_empty() {
327            return match self.key {
328                Key::Char(c) => write!(f, "{c}"),
329                Key::Named(n) => write!(f, "<{n}>"),
330            };
331        }
332        // Shift-only on uppercase letter: drop the modifier since the
333        // uppercase form already implies Shift in the parser.
334        if mods == Modifiers::SHIFT
335            && let Key::Char(c) = self.key
336            && c.is_ascii_uppercase()
337        {
338            return write!(f, "{c}");
339        }
340        write!(f, "<")?;
341        if mods.contains(Modifiers::CTRL) {
342            write!(f, "C-")?;
343        }
344        if mods.contains(Modifiers::SHIFT) {
345            write!(f, "S-")?;
346        }
347        if mods.contains(Modifiers::ALT) {
348            write!(f, "A-")?;
349        }
350        if mods.contains(Modifiers::SUPER) {
351            write!(f, "D-")?;
352        }
353        match self.key {
354            Key::Char(c) => write!(f, "{c}>"),
355            Key::Named(n) => write!(f, "{n}>"),
356        }
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    fn k(ch: char) -> KeyChord {
365        KeyChord::char(ch)
366    }
367
368    fn shift_k(ch: char) -> KeyChord {
369        KeyChord {
370            key: Key::Char(ch),
371            modifiers: Modifiers::SHIFT,
372        }
373    }
374
375    fn ctrl(ch: char) -> KeyChord {
376        KeyChord {
377            key: Key::Char(ch),
378            modifiers: Modifiers::CTRL,
379        }
380    }
381
382    #[test]
383    fn empty_input() {
384        assert_eq!(parse_keys("").unwrap(), vec![]);
385    }
386
387    #[test]
388    fn bare_chars() {
389        assert_eq!(parse_keys("gT").unwrap(), vec![k('g'), shift_k('T')]);
390    }
391
392    #[test]
393    fn ctrl_w_v() {
394        assert_eq!(parse_keys("<C-w>v").unwrap(), vec![ctrl('w'), k('v')]);
395    }
396
397    #[test]
398    fn ctrl_shift_tab() {
399        let chords = parse_keys("<C-S-Tab>").unwrap();
400        assert_eq!(chords.len(), 1);
401        assert_eq!(chords[0].key, Key::Named(NamedKey::Tab));
402        assert!(chords[0].modifiers.contains(Modifiers::CTRL));
403        assert!(chords[0].modifiers.contains(Modifiers::SHIFT));
404    }
405
406    #[test]
407    fn shift_tab_alias() {
408        let a = parse_keys("<S-Tab>").unwrap();
409        let b = parse_keys("<BackTab>").unwrap();
410        // <S-Tab> parses Tab + SHIFT; <BackTab> parses BackTab no
411        // modifier. Both representations are valid emit paths from
412        // the host depending on terminal/CEF.
413        assert_eq!(a[0].key, Key::Named(NamedKey::Tab));
414        assert!(a[0].modifiers.contains(Modifiers::SHIFT));
415        assert_eq!(b[0].key, Key::Named(NamedKey::BackTab));
416    }
417
418    #[test]
419    fn meta_alias_for_alt() {
420        let m = parse_keys("<M-x>").unwrap();
421        let a = parse_keys("<A-x>").unwrap();
422        assert_eq!(m, a);
423        assert!(m[0].modifiers.contains(Modifiers::ALT));
424    }
425
426    #[test]
427    fn leader_is_abstract() {
428        let chords = parse_keys("<leader>n").unwrap();
429        assert_eq!(chords[0].key, Key::Named(NamedKey::Leader));
430        assert_eq!(chords[1], k('n'));
431    }
432
433    #[test]
434    fn space_special() {
435        // <Space> normalizes to Char(' ') so it shares the canonical
436        // form a leader=' ' binding produces — keeps the input adapter
437        // and the parser in lock-step.
438        let chords = parse_keys("<Space>x").unwrap();
439        assert_eq!(chords[0].key, Key::Char(' '));
440        assert_eq!(chords[1], k('x'));
441    }
442
443    #[test]
444    fn esc_aliases() {
445        for s in ["<Esc>", "<escape>", "<ESC>"] {
446            assert_eq!(
447                parse_keys(s).unwrap(),
448                vec![KeyChord::plain(Key::Named(NamedKey::Esc))],
449                "alias {s}"
450            );
451        }
452    }
453
454    #[test]
455    fn enter_cr_alias() {
456        let cr = parse_keys("<CR>").unwrap();
457        let enter = parse_keys("<Enter>").unwrap();
458        let ret = parse_keys("<Return>").unwrap();
459        assert_eq!(cr, enter);
460        assert_eq!(cr, ret);
461    }
462
463    #[test]
464    fn bs_aliases() {
465        let bs = parse_keys("<BS>").unwrap();
466        let bksp = parse_keys("<Backspace>").unwrap();
467        assert_eq!(bs, bksp);
468        assert_eq!(bs[0].key, Key::Named(NamedKey::BS));
469    }
470
471    #[test]
472    fn lt_escape() {
473        let chords = parse_keys("<lt>").unwrap();
474        assert_eq!(chords, vec![k('<')]);
475    }
476
477    #[test]
478    fn function_keys() {
479        for n in 1..=12u8 {
480            let s = format!("<F{n}>");
481            let chords = parse_keys(&s).unwrap();
482            assert_eq!(chords[0].key, Key::Named(NamedKey::F(n)));
483        }
484    }
485
486    #[test]
487    fn function_key_out_of_range_errors() {
488        assert!(matches!(
489            parse_keys("<F13>"),
490            Err(ParseError::UnknownNamed(_))
491        ));
492        assert!(matches!(
493            parse_keys("<F0>"),
494            Err(ParseError::UnknownNamed(_))
495        ));
496    }
497
498    #[test]
499    fn unclosed_bracket_errors() {
500        assert_eq!(parse_keys("<C-w").unwrap_err(), ParseError::UnclosedBracket);
501    }
502
503    #[test]
504    fn dangling_modifier_errors() {
505        // `<C->` has the C- prefix but no following key.
506        assert!(matches!(
507            parse_keys("<C->"),
508            Err(ParseError::DanglingModifier(_))
509        ));
510    }
511
512    #[test]
513    fn empty_bracket_errors() {
514        assert!(matches!(parse_keys("<>"), Err(ParseError::EmptyBracket)));
515    }
516
517    #[test]
518    fn unknown_named_errors() {
519        match parse_keys("<NoSuchKey>") {
520            Err(ParseError::UnknownNamed(name)) => {
521                assert!(name.eq_ignore_ascii_case("nosuchkey"));
522            }
523            other => panic!("expected UnknownNamed, got {other:?}"),
524        }
525    }
526
527    #[test]
528    fn whitespace_ignored() {
529        let chords = parse_keys("<C-w>  v   ").unwrap();
530        assert_eq!(chords, vec![ctrl('w'), k('v')]);
531    }
532
533    #[test]
534    fn modifier_order_independent() {
535        let a = parse_keys("<C-S-Tab>").unwrap();
536        let b = parse_keys("<S-C-Tab>").unwrap();
537        assert_eq!(a, b);
538    }
539
540    #[test]
541    fn shift_letter_normalises_to_uppercase() {
542        let a = parse_keys("<S-a>").unwrap();
543        let b = parse_keys("<S-A>").unwrap();
544        assert_eq!(a, b);
545        assert_eq!(a[0].key, Key::Char('A'));
546        assert!(a[0].modifiers.contains(Modifiers::SHIFT));
547    }
548
549    #[test]
550    fn ctrl_letter_is_case_insensitive() {
551        let a = parse_keys("<C-A>").unwrap();
552        let b = parse_keys("<C-a>").unwrap();
553        assert_eq!(a, b);
554        assert_eq!(a[0].key, Key::Char('a'));
555    }
556
557    #[test]
558    fn punctuation_passes_through() {
559        let chords = parse_keys(":/?,;").unwrap();
560        assert_eq!(chords, vec![k(':'), k('/'), k('?'), k(','), k(';')]);
561    }
562
563    #[test]
564    fn embedded_literals_split_per_char() {
565        // "Hello" → 5 chords; capital H carries SHIFT, ello are bare.
566        let chords = parse_keys("Hello").unwrap();
567        assert_eq!(chords.len(), 5);
568        assert_eq!(chords[0], shift_k('H'));
569        assert_eq!(chords[1], k('e'));
570        assert_eq!(chords[4], k('o'));
571    }
572
573    #[test]
574    fn register_prefix_quote_a() {
575        // `"ay` is three chords: ", a, y. The Engine — not the
576        // parser — recognises `"<char>` as a register selector.
577        let chords = parse_keys("\"ay").unwrap();
578        assert_eq!(chords, vec![k('"'), k('a'), k('y')]);
579    }
580
581    #[test]
582    fn parse_key_single() {
583        let kc = parse_key("<C-w>").unwrap();
584        assert_eq!(kc, ctrl('w'));
585    }
586
587    #[test]
588    fn parse_key_residual_errors() {
589        assert!(matches!(
590            parse_key("<C-w>v"),
591            Err(ParseError::ExpectedOneChord(2))
592        ));
593        assert!(matches!(
594            parse_key(""),
595            Err(ParseError::ExpectedOneChord(0))
596        ));
597    }
598}