Skip to main content

basalt_tui/config/
key_binding.rs

1use std::{fmt, slice};
2
3use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
4
5use serde::{
6    de::{self, Visitor},
7    Deserialize, Deserializer,
8};
9
10use crate::{command::Command, config::ConfigError};
11
12#[derive(Clone, Debug, PartialEq, Deserialize)]
13pub(crate) struct KeyBinding {
14    pub key: KeySpec,
15    pub command: Command,
16}
17
18impl KeyBinding {
19    pub const fn new(key: KeySpec, command: Command) -> Self {
20        Self { key, command }
21    }
22}
23
24#[derive(Clone, Debug, Eq, Hash, PartialEq)]
25pub struct Keystroke {
26    pub code: KeyCode,
27    pub modifiers: KeyModifiers,
28}
29
30impl Keystroke {
31    pub const fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
32        Self { code, modifiers }
33    }
34}
35
36impl From<KeyEvent> for Keystroke {
37    fn from(value: KeyEvent) -> Self {
38        Self::from((value.code, value.modifiers))
39    }
40}
41
42impl From<KeyCode> for Keystroke {
43    fn from(code: KeyCode) -> Self {
44        Keystroke::from((code, KeyModifiers::NONE))
45    }
46}
47
48impl From<(KeyCode, KeyModifiers)> for Keystroke {
49    fn from((code, mut modifiers): (KeyCode, KeyModifiers)) -> Self {
50        let code = match code {
51            KeyCode::Char(ch) if ch.is_uppercase() => {
52                modifiers.insert(KeyModifiers::SHIFT);
53                code
54            }
55            KeyCode::Char(ch)
56                if modifiers.contains(KeyModifiers::SHIFT) && ch.is_ascii_lowercase() =>
57            {
58                // Normalize lowercase+SHIFT to uppercase
59                KeyCode::Char(ch.to_ascii_uppercase())
60            }
61            _ => code,
62        };
63        Self { code, modifiers }
64    }
65}
66
67impl From<(char, KeyModifiers)> for Keystroke {
68    fn from((c, modifiers): (char, KeyModifiers)) -> Self {
69        Keystroke::from((KeyCode::Char(c), modifiers))
70    }
71}
72
73impl From<&KeyEvent> for Keystroke {
74    fn from(event: &KeyEvent) -> Self {
75        Self::from((event.code, event.modifiers))
76    }
77}
78
79impl fmt::Display for Keystroke {
80    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
81        let code = self.code.to_string().replace(" ", "_");
82
83        // Uppercase chars carry SHIFT implicitly — strip it from the display
84        // so the string representation stays canonical (e.g. "G" not "shift-G")
85        let modifiers = match self.code {
86            KeyCode::Char(ch) if ch.is_uppercase() => self.modifiers - KeyModifiers::SHIFT,
87            _ => self.modifiers,
88        };
89
90        if modifiers.is_empty() {
91            write!(f, "{code}")
92        } else {
93            write!(f, "{}-{code}", modifiers.to_string().to_ascii_lowercase())
94        }
95    }
96}
97
98#[derive(Clone, Debug, Eq, Hash, PartialEq)]
99pub enum Key {
100    Single(Keystroke),
101    Chord(Vec<Keystroke>),
102}
103
104impl fmt::Display for Key {
105    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
106        match self {
107            Key::Single(key) => key.fmt(f),
108            Key::Chord(keys) => keys.iter().try_for_each(|key| key.fmt(f)),
109        }
110    }
111}
112
113impl Key {
114    pub const CTRL_C: Key = Key::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
115
116    pub const fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
117        Key::Single(Keystroke::new(code, modifiers))
118    }
119
120    pub fn chord(iter: impl IntoIterator<Item = Keystroke>) -> Self {
121        let mut keystrokes: Vec<Keystroke> = iter.into_iter().collect();
122        match keystrokes.len() {
123            1 => Key::Single(keystrokes.remove(0)),
124            _ => Key::Chord(keystrokes),
125        }
126    }
127
128    fn keystrokes(&self) -> &[Keystroke] {
129        match self {
130            Key::Single(keystroke) => slice::from_ref(keystroke),
131            Key::Chord(keystrokes) => keystrokes,
132        }
133    }
134}
135
136/// The prefix key that `<leader>` stands for in key bindings.
137#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize)]
138#[serde(transparent)]
139pub struct Leader(Key);
140
141impl Default for Leader {
142    fn default() -> Self {
143        Self(Key::from(' '))
144    }
145}
146
147impl From<Key> for Leader {
148    fn from(key: Key) -> Self {
149        Self(key)
150    }
151}
152
153/// One element of a binding as written in config. `Leader` stands for however
154/// many keystrokes the configured leader holds, so an element is not
155/// necessarily a single key.
156#[derive(Clone, Debug, Eq, Hash, PartialEq)]
157enum Keys {
158    Keystroke(Keystroke),
159    Leader,
160}
161
162/// A binding as written in config, where `<leader>` still stands for the
163/// configured [`Leader`]. Resolving it against one yields a concrete [`Key`].
164#[derive(Clone, Debug, Eq, Hash, PartialEq)]
165pub struct KeySpec(Vec<Keys>);
166
167impl KeySpec {
168    /// Expands every `<leader>` into the keystrokes of the configured leader.
169    pub fn resolve(&self, leader: &Leader) -> Key {
170        self.0
171            .iter()
172            .flat_map(|keys| match keys {
173                Keys::Keystroke(keystroke) => slice::from_ref(keystroke),
174                Keys::Leader => leader.0.keystrokes(),
175            })
176            .cloned()
177            .collect()
178    }
179
180    /// The concrete key, or `None` when the spec still contains `<leader>`.
181    fn literal(&self) -> Option<Key> {
182        self.0
183            .iter()
184            .map(|keys| match keys {
185                Keys::Keystroke(keystroke) => Some(keystroke.clone()),
186                Keys::Leader => None,
187            })
188            .collect()
189    }
190}
191
192impl From<Key> for KeySpec {
193    fn from(key: Key) -> Self {
194        Self(
195            key.keystrokes()
196                .iter()
197                .cloned()
198                .map(Keys::Keystroke)
199                .collect(),
200        )
201    }
202}
203
204impl From<KeyEvent> for Key {
205    fn from(value: KeyEvent) -> Self {
206        Self::Single(Keystroke::from(value))
207    }
208}
209
210impl From<KeyCode> for Key {
211    fn from(value: KeyCode) -> Self {
212        Self::Single(Keystroke::from(value))
213    }
214}
215
216impl From<(KeyCode, KeyModifiers)> for Key {
217    fn from(value: (KeyCode, KeyModifiers)) -> Self {
218        Self::Single(Keystroke::from(value))
219    }
220}
221
222impl From<char> for Key {
223    fn from(value: char) -> Self {
224        Self::from(KeyCode::Char(value))
225    }
226}
227
228impl From<(char, KeyModifiers)> for Key {
229    fn from(value: (char, KeyModifiers)) -> Self {
230        Self::Single(Keystroke::from(value))
231    }
232}
233
234impl From<Keystroke> for Key {
235    fn from(value: Keystroke) -> Self {
236        Self::Single(value)
237    }
238}
239
240impl FromIterator<Keystroke> for Key {
241    fn from_iter<T: IntoIterator<Item = Keystroke>>(iter: T) -> Self {
242        Key::chord(iter)
243    }
244}
245
246impl From<Vec<Keystroke>> for Key {
247    fn from(value: Vec<Keystroke>) -> Self {
248        Key::from_iter(value)
249    }
250}
251
252impl<'de> Deserialize<'de> for KeySpec {
253    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
254    where
255        D: Deserializer<'de>,
256    {
257        deserializer.deserialize_str(KeySpecVisitor)
258    }
259}
260
261/// Only a literal key is accepted — `<leader>` is what a [`Leader`] defines,
262/// so it cannot stand for itself.
263impl<'de> Deserialize<'de> for Key {
264    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
265    where
266        D: Deserializer<'de>,
267    {
268        KeySpec::deserialize(deserializer)?
269            .literal()
270            .ok_or_else(|| de::Error::custom("`<leader>` is not allowed here"))
271    }
272}
273
274struct KeySpecVisitor;
275
276impl Visitor<'_> for KeySpecVisitor {
277    type Value = KeySpec;
278
279    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
280        formatter.write_str("a single key (\"a\"), named key (\"esc\"), modified key (\"ctrl+x\"), or key sequence (\"gg\", \"<leader>f\")")
281    }
282
283    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
284    where
285        E: de::Error,
286    {
287        parse_keys(value).map(KeySpec).map_err(de::Error::custom)
288    }
289}
290
291/// Tokenizes a binding string into its elements, treating `<...>` as a grouping
292/// delimiter. The inside of a group is parsed exactly like a bare standalone
293/// key, so `<space>f` isolates `space` as a named key instead of splitting
294/// `spacef` into per-character keystrokes.
295fn parse_keys(input: &str) -> Result<Vec<Keys>, ConfigError> {
296    let mut keys = Vec::new();
297    let mut rest = input;
298
299    while !rest.is_empty() {
300        match rest.strip_prefix('<') {
301            // A `<...>` group denotes exactly one named/modified key.
302            Some(after) => {
303                let (name, tail) = after.split_once('>').ok_or_else(|| {
304                    ConfigError::InvalidKeybinding(format!("unterminated `<` in {input:?}"))
305                })?;
306                match name {
307                    "leader" => keys.push(Keys::Leader),
308                    _ => match parse_segment(name)?.as_slice() {
309                        [single] if !name.is_empty() => keys.push(Keys::Keystroke(single.clone())),
310                        _ => {
311                            return Err(ConfigError::InvalidKeybinding(format!(
312                                "`<{name}>` is not a single key"
313                            )))
314                        }
315                    },
316                }
317                rest = tail;
318            }
319            None => {
320                let bare = rest.split('<').next().unwrap_or(rest);
321                keys.extend(parse_segment(bare)?.into_iter().map(Keys::Keystroke));
322                rest = &rest[bare.len()..];
323            }
324        }
325    }
326
327    Ok(keys)
328}
329
330/// Parses one segment — a bare run or the contents of a `<...>` group — into
331/// its keystrokes, splitting `+` into modifiers and a trailing key code.
332fn parse_segment(segment: &str) -> Result<Vec<Keystroke>, ConfigError> {
333    let mut parts = segment.split('+');
334    let code = parts
335        .next_back()
336        .ok_or_else(|| ConfigError::UnknownKeyCode(segment.to_string()))?;
337
338    let modifiers = parts.try_fold(KeyModifiers::NONE, |modifiers, part| {
339        parse_modifiers(&part.to_lowercase()).map(|parsed| modifiers | parsed)
340    })?;
341
342    Ok(parse_key(code, modifiers)?.keystrokes().to_vec())
343}
344
345fn parse_key(code: &str, modifiers: KeyModifiers) -> Result<Key, ConfigError> {
346    if code.is_empty() {
347        return Ok(Key::from((KeyCode::Null, modifiers)));
348    }
349
350    let key_code = match code {
351        "esc" => KeyCode::Esc,
352        "space" => KeyCode::Char(' '),
353        "backspace" => KeyCode::Backspace,
354        "backtab" => KeyCode::BackTab,
355        "delete" => KeyCode::Delete,
356        "down" => KeyCode::Down,
357        "end" => KeyCode::End,
358        "enter" => KeyCode::Enter,
359        "home" => KeyCode::Home,
360        "insert" => KeyCode::Insert,
361        // `<` and `>` are reserved for group syntax — name them to bind literals
362        "lt" => KeyCode::Char('<'),
363        "gt" => KeyCode::Char('>'),
364        "left" => KeyCode::Left,
365        "page_down" => KeyCode::PageDown,
366        "page_up" => KeyCode::PageUp,
367        "right" => KeyCode::Right,
368        "tab" => KeyCode::Tab,
369        "up" => KeyCode::Up,
370        // Single char — uppercase SHIFT is handled by Keystroke::from
371        c if c.chars().count() == 1 => c
372            .chars()
373            .next()
374            .map(KeyCode::Char)
375            .ok_or_else(|| ConfigError::UnknownKeyCode(c.to_string()))?,
376        // F-n keys
377        c if c.starts_with('f') => c[1..]
378            .parse::<u8>()
379            .map(KeyCode::F)
380            .map_err(|_| ConfigError::UnknownKeyCode(c.to_string()))?,
381        // Multi-char sequence like "gG" or "ciw" — uppercase SHIFT via Keystroke::from
382        c => {
383            return Ok(Key::chord(
384                c.chars().map(KeyCode::Char).map(Keystroke::from),
385            ))
386        }
387    };
388
389    Ok(Key::from((key_code, modifiers)))
390}
391
392fn parse_modifiers(modifiers: &str) -> Result<KeyModifiers, ConfigError> {
393    if modifiers.is_empty() {
394        return Ok(KeyModifiers::NONE);
395    }
396
397    match modifiers {
398        "alt" => Ok(KeyModifiers::ALT),
399        "ctrl" | "control" => Ok(KeyModifiers::CONTROL),
400        "hyper" => Ok(KeyModifiers::HYPER),
401        "meta" => Ok(KeyModifiers::META),
402        "shift" => Ok(KeyModifiers::SHIFT),
403        "super" => Ok(KeyModifiers::SUPER),
404        _ => Err(ConfigError::UnknownKeyModifiers(modifiers.to_string())),
405    }
406}
407
408impl de::Error for ConfigError {
409    fn custom<T>(msg: T) -> Self
410    where
411        T: fmt::Display,
412    {
413        ConfigError::InvalidKeybinding(msg.to_string())
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use ratatui::crossterm::event::{KeyCode, KeyModifiers};
420    use serde::de::IntoDeserializer;
421
422    use super::*;
423
424    fn key_from_str(s: &str) -> Result<Key, ConfigError> {
425        Key::deserialize(s.into_deserializer())
426    }
427
428    fn spec_from_str(s: &str) -> Result<KeySpec, ConfigError> {
429        KeySpec::deserialize(s.into_deserializer())
430    }
431
432    #[test]
433    fn test_named_keys() {
434        let cases = [
435            ("esc", Key::from(KeyCode::Esc)),
436            ("enter", Key::from(KeyCode::Enter)),
437            ("space", Key::from(KeyCode::Char(' '))),
438            ("backspace", Key::from(KeyCode::Backspace)),
439            ("backtab", Key::from(KeyCode::BackTab)),
440            ("delete", Key::from(KeyCode::Delete)),
441            ("tab", Key::from(KeyCode::Tab)),
442            ("up", Key::from(KeyCode::Up)),
443            ("down", Key::from(KeyCode::Down)),
444            ("left", Key::from(KeyCode::Left)),
445            ("right", Key::from(KeyCode::Right)),
446            ("home", Key::from(KeyCode::Home)),
447            ("end", Key::from(KeyCode::End)),
448            ("page_up", Key::from(KeyCode::PageUp)),
449            ("page_down", Key::from(KeyCode::PageDown)),
450            ("insert", Key::from(KeyCode::Insert)),
451        ];
452
453        cases.into_iter().for_each(|(input, expected)| {
454            assert_eq!(key_from_str(input).unwrap(), expected, "input: {input:?}");
455        });
456    }
457
458    #[test]
459    fn test_single_char_keys() {
460        let cases = [
461            ("a", Key::from('a')),
462            ("z", Key::from('z')),
463            ("A", Key::from('A')),
464            ("0", Key::from('0')),
465            ("?", Key::from('?')),
466            ("/", Key::from('/')),
467            (":", Key::from(':')),
468        ];
469
470        cases.into_iter().for_each(|(input, expected)| {
471            assert_eq!(key_from_str(input).unwrap(), expected, "input: {input:?}");
472        });
473    }
474
475    #[test]
476    fn test_function_keys() {
477        let cases = [
478            ("f1", Key::from(KeyCode::F(1))),
479            ("f5", Key::from(KeyCode::F(5))),
480            ("f12", Key::from(KeyCode::F(12))),
481        ];
482
483        cases.into_iter().for_each(|(input, expected)| {
484            assert_eq!(key_from_str(input).unwrap(), expected, "input: {input:?}");
485        });
486    }
487
488    #[test]
489    fn test_modified_keys() {
490        let cases = [
491            ("ctrl+c", Key::from(('c', KeyModifiers::CONTROL))),
492            ("control+c", Key::from(('c', KeyModifiers::CONTROL))),
493            ("alt+x", Key::from(('x', KeyModifiers::ALT))),
494            ("shift+a", Key::from(('a', KeyModifiers::SHIFT))),
495            (
496                "ctrl+shift+k",
497                Key::from((
498                    KeyCode::Char('k'),
499                    KeyModifiers::CONTROL | KeyModifiers::SHIFT,
500                )),
501            ),
502            (
503                "ctrl+enter",
504                Key::from((KeyCode::Enter, KeyModifiers::CONTROL)),
505            ),
506            ("alt+esc", Key::from((KeyCode::Esc, KeyModifiers::ALT))),
507            ("ctrl+f5", Key::from((KeyCode::F(5), KeyModifiers::CONTROL))),
508        ];
509
510        cases.into_iter().for_each(|(input, expected)| {
511            assert_eq!(key_from_str(input).unwrap(), expected, "input: {input:?}");
512        });
513    }
514
515    #[test]
516    fn test_key_sequences() {
517        let cases: &[(&str, &[Keystroke])] = &[
518            (
519                "gg",
520                &[
521                    Keystroke::from(KeyCode::Char('g')),
522                    Keystroke::from(KeyCode::Char('g')),
523                ],
524            ),
525            (
526                "gG",
527                &[
528                    Keystroke::from(KeyCode::Char('g')),
529                    Keystroke::from(KeyCode::Char('G')),
530                ],
531            ),
532            (
533                "crn",
534                &[
535                    Keystroke::from(KeyCode::Char('c')),
536                    Keystroke::from(KeyCode::Char('r')),
537                    Keystroke::from(KeyCode::Char('n')),
538                ],
539            ),
540        ];
541
542        cases.iter().for_each(|(input, expected_keys)| {
543            let key = key_from_str(input).unwrap();
544            match key {
545                Key::Chord(keys) => assert_eq!(keys, *expected_keys, "input: {input:?}"),
546                Key::Single(_) => panic!("Expected sequence for {input:?}, got plain key"),
547            }
548        });
549    }
550
551    #[test]
552    fn test_named_key_sequences() {
553        let space = Keystroke::from(KeyCode::Char(' '));
554        let cases: &[(&str, &[Keystroke])] = &[
555            (
556                "<space>f",
557                &[space.clone(), Keystroke::from(KeyCode::Char('f'))],
558            ),
559            ("<space><space>", &[space.clone(), space.clone()]),
560            (
561                "<enter>x",
562                &[
563                    Keystroke::from(KeyCode::Enter),
564                    Keystroke::from(KeyCode::Char('x')),
565                ],
566            ),
567            (
568                "g<space>",
569                &[Keystroke::from(KeyCode::Char('g')), space.clone()],
570            ),
571            (
572                "<ctrl+space>f",
573                &[
574                    Keystroke::from((KeyCode::Char(' '), KeyModifiers::CONTROL)),
575                    Keystroke::from(KeyCode::Char('f')),
576                ],
577            ),
578        ];
579
580        cases.iter().for_each(
581            |(input, expected_keys)| match key_from_str(input).unwrap() {
582                Key::Chord(keys) => assert_eq!(keys, *expected_keys, "input: {input:?}"),
583                Key::Single(_) => panic!("Expected sequence for {input:?}, got plain key"),
584            },
585        );
586    }
587
588    #[test]
589    fn test_named_group_single_key() {
590        // A lone group is equivalent to the bare named key
591        assert_eq!(
592            key_from_str("<space>").unwrap(),
593            key_from_str("space").unwrap()
594        );
595        assert_eq!(key_from_str("<esc>").unwrap(), key_from_str("esc").unwrap());
596        // `<` and `>` literals remain bindable via lt/gt
597        assert_eq!(key_from_str("<lt>").unwrap(), Key::from('<'));
598        assert_eq!(key_from_str("<gt>").unwrap(), Key::from('>'));
599    }
600
601    #[test]
602    fn test_invalid_keys() {
603        let cases = [
604            "unknown_modifier+c",
605            "badmod+x",
606            "f999",
607            "<space",
608            "<>",
609            "<unknown_key>",
610        ];
611
612        cases.into_iter().for_each(|input| {
613            assert!(key_from_str(input).is_err(), "Expected error for {input:?}");
614        });
615    }
616
617    #[test]
618    fn test_keystroke_display() {
619        let cases = [
620            (Keystroke::new(KeyCode::Char('a'), KeyModifiers::NONE), "a"),
621            (
622                Keystroke::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
623                "control-c",
624            ),
625            // Uppercase char: SHIFT is implicit, not shown in display
626            (Keystroke::from(KeyCode::Char('G')), "G"),
627            // Uppercase char with additional modifier
628            (
629                Keystroke::from((KeyCode::Char('G'), KeyModifiers::CONTROL)),
630                "control-G",
631            ),
632        ];
633
634        cases.into_iter().for_each(|(key, expected)| {
635            assert_eq!(key.to_string(), expected, "key: {key:?}");
636        });
637    }
638
639    #[test]
640    fn test_key_sequence_display() {
641        let keys = [
642            Keystroke::from(KeyCode::Char('g')),
643            Keystroke::from(KeyCode::Char('G')),
644        ];
645
646        assert_eq!(Key::chord(keys).to_string(), "gG");
647    }
648
649    #[test]
650    fn test_leader_expands_to_the_configured_key() {
651        let leader = Leader::from(Key::from(','));
652        let cases = [
653            ("<leader>", ","),
654            ("<leader>f", ",f"),
655            ("<leader><leader>", ",,"),
656            ("g<leader>", "g,"),
657            ("<leader><space>", ",<space>"),
658        ];
659
660        cases.into_iter().for_each(|(input, expected)| {
661            assert_eq!(
662                spec_from_str(input).unwrap().resolve(&leader),
663                key_from_str(expected).unwrap(),
664                "input: {input:?}"
665            );
666        });
667    }
668
669    #[test]
670    fn test_leader_defaults_to_space() {
671        assert_eq!(
672            spec_from_str("<leader>f")
673                .unwrap()
674                .resolve(&Leader::default()),
675            key_from_str("<space>f").unwrap()
676        );
677    }
678
679    #[test]
680    fn test_leader_can_be_a_sequence() {
681        let leader = Leader::from(key_from_str("gs").unwrap());
682
683        assert_eq!(
684            spec_from_str("<leader>f").unwrap().resolve(&leader),
685            key_from_str("gsf").unwrap()
686        );
687    }
688
689    #[test]
690    fn test_leader_is_not_a_literal_key() {
691        // The leader definition itself cannot refer to `<leader>`
692        assert!(key_from_str("<leader>").is_err());
693        assert!(key_from_str("<leader>f").is_err());
694    }
695
696    #[test]
697    fn test_uppercase_implies_shift() {
698        // Parsing "G" should give the same result as "shift+g" would — SHIFT in modifiers
699        let upper = key_from_str("G").unwrap();
700        assert_eq!(
701            upper,
702            Key::Single(Keystroke::new(KeyCode::Char('G'), KeyModifiers::SHIFT))
703        );
704
705        // Sequence "gG" — second key carries SHIFT
706        let seq = key_from_str("gG").unwrap();
707        assert_eq!(
708            seq,
709            Key::chord([
710                Keystroke::new(KeyCode::Char('g'), KeyModifiers::NONE),
711                Keystroke::new(KeyCode::Char('G'), KeyModifiers::SHIFT),
712            ])
713        );
714    }
715}