brush_parser/
readline_binding.rs

1//! Implements a parser for readline binding syntax.
2
3use crate::error;
4
5/// Represents a readline key-sequence binding.
6#[derive(Debug, Clone, PartialEq, Eq)]
7#[cfg_attr(test, derive(serde::Serialize))]
8pub struct KeySequenceBinding {
9    /// Key sequence to bind
10    pub seq: KeySequence,
11    /// Command to bind to the sequence
12    pub command: String,
13}
14
15/// Represents a key sequence.
16#[derive(Debug, Clone, PartialEq, Eq)]
17#[cfg_attr(test, derive(serde::Serialize))]
18pub struct KeySequence(pub Vec<KeySequenceItem>);
19
20/// Represents an element of a key sequence.
21#[derive(Debug, Clone, PartialEq, Eq)]
22#[cfg_attr(test, derive(serde::Serialize))]
23pub enum KeySequenceItem {
24    /// Control
25    Control,
26    /// Meta
27    Meta,
28    /// Regular character
29    Byte(u8),
30}
31
32/// Represents a single key stroke.
33#[derive(Debug, Default, Clone, PartialEq, Eq)]
34#[cfg_attr(test, derive(serde::Serialize))]
35pub struct KeyStroke {
36    /// Meta key is held down
37    pub meta: bool,
38    /// Control key is held down
39    pub control: bool,
40    /// Primary key code
41    pub key_code: Vec<u8>,
42}
43
44/// Parses a key-sequence binding specification.
45///
46/// # Arguments
47///
48/// * `input` - The input string to parse
49pub fn parse_key_sequence_binding(
50    input: &str,
51) -> Result<KeySequenceBinding, error::BindingParseError> {
52    readline_binding::key_sequence_binding(input)
53        .map_err(|_err| error::BindingParseError::Unknown(input.to_owned()))
54}
55
56/// Converts a `KeySequence` to a vector of `KeyStroke`.
57///
58/// # Arguments
59///
60/// * `seq` - The key sequence to convert
61pub fn key_sequence_to_strokes(
62    seq: &KeySequence,
63) -> Result<Vec<KeyStroke>, error::BindingParseError> {
64    let mut strokes = vec![];
65    let mut current_stroke = KeyStroke::default();
66
67    for item in &seq.0 {
68        if matches!(item, KeySequenceItem::Control | KeySequenceItem::Meta)
69            && !current_stroke.key_code.is_empty()
70        {
71            strokes.push(current_stroke);
72            current_stroke = KeyStroke::default();
73        }
74
75        match item {
76            KeySequenceItem::Control => current_stroke.control = true,
77            KeySequenceItem::Meta => current_stroke.meta = true,
78            KeySequenceItem::Byte(b) => current_stroke.key_code.push(*b),
79        }
80    }
81
82    if current_stroke.key_code.is_empty() {
83        if current_stroke.control || current_stroke.meta {
84            return Err(error::BindingParseError::MissingKeyCode);
85        }
86    } else {
87        strokes.push(current_stroke);
88    }
89
90    Ok(strokes)
91}
92
93peg::parser! {
94    grammar readline_binding() for str {
95        rule _() = [' ' | '\t' | '\n']*
96
97        pub rule key_sequence_binding() -> KeySequenceBinding =
98            _ "\"" seq:key_sequence() "\"" _ ":" _ command:cmd() _ { KeySequenceBinding { seq, command } }
99
100        rule cmd() -> String = s:$([_]*) { s.to_string() }
101
102        // Main rule for parsing a key sequence
103        rule key_sequence() -> KeySequence =
104            items:key_sequence_item()* { KeySequence(items) }
105
106        rule key_sequence_item() -> KeySequenceItem =
107            "\\C-" { KeySequenceItem::Control } /
108            "\\M-" { KeySequenceItem::Meta } /
109            "\\e" { KeySequenceItem::Byte(b'\x1b') } /
110            "\\\\" { KeySequenceItem::Byte(b'\\') } /
111            "\\\"" { KeySequenceItem::Byte(b'"') } /
112            "\\'" { KeySequenceItem::Byte(b'\'') } /
113            "\\a" { KeySequenceItem::Byte(b'\x07') } /
114            "\\b" { KeySequenceItem::Byte(b'\x08') } /
115            "\\d" { KeySequenceItem::Byte(b'\x7f') } /
116            "\\f" { KeySequenceItem::Byte(b'\x0c') } /
117            "\\n" { KeySequenceItem::Byte(b'\n') } /
118            "\\r" { KeySequenceItem::Byte(b'\r') } /
119            "\\t" { KeySequenceItem::Byte(b'\t') } /
120            "\\v" { KeySequenceItem::Byte(b'\x0b') } /
121            "\\" n:octal_number() { KeySequenceItem::Byte(n) } /
122            "\\" n:hex_number() { KeySequenceItem::Byte(n) } /
123            [c if c != '"'] { KeySequenceItem::Byte(c as u8) }
124
125        rule octal_number() -> u8 =
126            s:$(['0'..='7']*<1,3>) {? u8::from_str_radix(s, 8).or(Err("invalid octal number")) }
127
128        rule hex_number() -> u8 =
129            s:$(['0'..='9' | 'a'..='f' | 'A'..='F']*<1,2>) {? u8::from_str_radix(s, 16).or(Err("invalid hex number")) }
130    }
131}
132
133#[cfg(test)]
134#[expect(clippy::panic_in_result_fn)]
135mod tests {
136    use super::*;
137    use anyhow::Result;
138
139    #[test]
140    fn test_basic_parse() -> Result<()> {
141        let binding = parse_key_sequence_binding(r#""\C-k": xyz"#)?;
142        assert_eq!(
143            binding.seq.0,
144            [KeySequenceItem::Control, KeySequenceItem::Byte(b'k')]
145        );
146        assert_eq!(binding.command, "xyz");
147
148        Ok(())
149    }
150}