brush_parser/
readline_binding.rs

1//! Implements a parser for readline binding syntax.
2
3use crate::error;
4
5/// Represents a key-sequence-to-shell-command binding.
6#[derive(Debug, Clone, PartialEq, Eq)]
7#[cfg_attr(test, derive(serde::Serialize))]
8pub struct KeySequenceShellCommandBinding {
9    /// Key sequence to bind
10    pub seq: KeySequence,
11    /// Shell command to bind to the sequence
12    pub shell_cmd: String,
13}
14
15/// Represents a key-sequence-to-readline-command binding.
16#[derive(Debug, Clone, PartialEq, Eq)]
17#[cfg_attr(test, derive(serde::Serialize))]
18pub struct KeySequenceReadlineBinding {
19    /// Key sequence to bind
20    pub seq: KeySequence,
21    /// Readline target to bind to the sequence
22    pub target: ReadlineTarget,
23}
24
25/// Represents a readline target.
26#[derive(Debug, Clone, PartialEq, Eq)]
27#[cfg_attr(test, derive(serde::Serialize))]
28pub enum ReadlineTarget {
29    /// A named readline function.
30    Function(String),
31    /// A readline command.
32    Command(String),
33}
34
35/// Represents a key sequence.
36#[derive(Debug, Clone, PartialEq, Eq)]
37#[cfg_attr(test, derive(serde::Serialize))]
38pub struct KeySequence(pub Vec<KeySequenceItem>);
39
40/// Represents an element of a key sequence.
41#[derive(Debug, Clone, PartialEq, Eq)]
42#[cfg_attr(test, derive(serde::Serialize))]
43pub enum KeySequenceItem {
44    /// Control
45    Control,
46    /// Meta
47    Meta,
48    /// Regular character
49    Byte(u8),
50}
51
52/// Represents a single key stroke.
53#[derive(Debug, Default, Clone, PartialEq, Eq)]
54#[cfg_attr(test, derive(serde::Serialize))]
55pub struct KeyStroke {
56    /// Meta key is held down
57    pub meta: bool,
58    /// Control key is held down
59    pub control: bool,
60    /// Primary key code
61    pub key_code: Vec<u8>,
62}
63
64/// Parses a binding specification that maps a key sequence
65/// to a shell command.
66///
67/// # Arguments
68///
69/// * `input` - The input string to parse
70pub fn parse_key_sequence_shell_cmd_binding(
71    input: &str,
72) -> Result<KeySequenceShellCommandBinding, error::BindingParseError> {
73    readline_binding::key_sequence_shell_cmd_binding(input)
74        .map_err(|_err| error::BindingParseError::Unknown(input.to_owned()))
75}
76
77/// Parses a binding specification that maps a key sequence
78/// to a readline target.
79///
80/// # Arguments
81///
82/// * `input` - The input string to parse
83pub fn parse_key_sequence_readline_binding(
84    input: &str,
85) -> Result<KeySequenceReadlineBinding, error::BindingParseError> {
86    readline_binding::key_sequence_readline_binding(input)
87        .map_err(|_err| error::BindingParseError::Unknown(input.to_owned()))
88}
89
90/// Converts a `KeySequence` to a vector of `KeyStroke`.
91///
92/// # Arguments
93///
94/// * `seq` - The key sequence to convert
95pub fn key_sequence_to_strokes(
96    seq: &KeySequence,
97) -> Result<Vec<KeyStroke>, error::BindingParseError> {
98    let mut strokes = vec![];
99    let mut current_stroke = KeyStroke::default();
100
101    for item in &seq.0 {
102        if matches!(item, KeySequenceItem::Control | KeySequenceItem::Meta)
103            && !current_stroke.key_code.is_empty()
104        {
105            strokes.push(current_stroke);
106            current_stroke = KeyStroke::default();
107        }
108
109        match item {
110            KeySequenceItem::Control => current_stroke.control = true,
111            KeySequenceItem::Meta => current_stroke.meta = true,
112            KeySequenceItem::Byte(b) => current_stroke.key_code.push(*b),
113        }
114    }
115
116    if current_stroke.key_code.is_empty() {
117        if current_stroke.control || current_stroke.meta {
118            return Err(error::BindingParseError::MissingKeyCode);
119        }
120    } else {
121        strokes.push(current_stroke);
122    }
123
124    Ok(strokes)
125}
126
127peg::parser! {
128    grammar readline_binding() for str {
129        rule _() = [' ' | '\t' | '\n']*
130
131        pub rule key_sequence_shell_cmd_binding() -> KeySequenceShellCommandBinding =
132            _ "\"" seq:key_sequence() "\"" _ ":" _ cmd:shell_cmd() _ { KeySequenceShellCommandBinding { seq, shell_cmd: cmd } }
133
134        pub rule key_sequence_readline_binding() -> KeySequenceReadlineBinding =
135            _ "\"" seq:key_sequence() "\"" _ ":" _ "\"" cmd:readline_cmd() "\"" _ {
136                KeySequenceReadlineBinding { seq, target: ReadlineTarget::Command(cmd) }
137            } /
138            _ "\"" seq:key_sequence() "\"" _ ":" _ func:readline_function() _ {
139                KeySequenceReadlineBinding { seq, target: ReadlineTarget::Function(func) }
140            }
141
142        rule readline_cmd() -> String = s:$([^'"']*) { s.to_string() }
143        rule shell_cmd() -> String = s:$([_]*) { s.to_string() }
144        rule readline_function() -> String = s:$([_]*) { s.to_string() }
145
146        // Main rule for parsing a key sequence
147        rule key_sequence() -> KeySequence =
148            items:key_sequence_item()* { KeySequence(items) }
149
150        rule key_sequence_item() -> KeySequenceItem =
151            "\\C-" { KeySequenceItem::Control } /
152            "\\M-" { KeySequenceItem::Meta } /
153            "\\e" { KeySequenceItem::Byte(b'\x1b') } /
154            "\\\\" { KeySequenceItem::Byte(b'\\') } /
155            "\\\"" { KeySequenceItem::Byte(b'"') } /
156            "\\'" { KeySequenceItem::Byte(b'\'') } /
157            "\\a" { KeySequenceItem::Byte(b'\x07') } /
158            "\\b" { KeySequenceItem::Byte(b'\x08') } /
159            "\\d" { KeySequenceItem::Byte(b'\x7f') } /
160            "\\f" { KeySequenceItem::Byte(b'\x0c') } /
161            "\\n" { KeySequenceItem::Byte(b'\n') } /
162            "\\r" { KeySequenceItem::Byte(b'\r') } /
163            "\\t" { KeySequenceItem::Byte(b'\t') } /
164            "\\v" { KeySequenceItem::Byte(b'\x0b') } /
165            "\\" n:octal_number() { KeySequenceItem::Byte(n) } /
166            "\\" n:hex_number() { KeySequenceItem::Byte(n) } /
167            [c if c != '"'] { KeySequenceItem::Byte(c as u8) }
168
169        rule octal_number() -> u8 =
170            s:$(['0'..='7']*<1,3>) {? u8::from_str_radix(s, 8).or(Err("invalid octal number")) }
171
172        rule hex_number() -> u8 =
173            s:$(['0'..='9' | 'a'..='f' | 'A'..='F']*<1,2>) {? u8::from_str_radix(s, 16).or(Err("invalid hex number")) }
174    }
175}
176
177#[cfg(test)]
178#[expect(clippy::panic_in_result_fn)]
179mod tests {
180    use super::*;
181    use anyhow::Result;
182
183    #[test]
184    fn test_basic_shell_cmd_binding_parse() -> Result<()> {
185        let binding = parse_key_sequence_shell_cmd_binding(r#""\C-k": xyz"#)?;
186        assert_eq!(
187            binding.seq.0,
188            [KeySequenceItem::Control, KeySequenceItem::Byte(b'k')]
189        );
190        assert_eq!(binding.shell_cmd, "xyz");
191
192        Ok(())
193    }
194
195    #[test]
196    fn test_basic_readline_func_binding_parse() -> Result<()> {
197        let binding = parse_key_sequence_readline_binding(r#""\M-x": some-function"#)?;
198        assert_eq!(
199            binding.seq.0,
200            [KeySequenceItem::Meta, KeySequenceItem::Byte(b'x')]
201        );
202        assert_eq!(
203            binding.target,
204            ReadlineTarget::Function("some-function".to_string())
205        );
206
207        Ok(())
208    }
209
210    #[test]
211    fn test_basic_readline_cmd_binding_parse() -> Result<()> {
212        let binding = parse_key_sequence_readline_binding(r#""\C-k": "xyz""#)?;
213        assert_eq!(
214            binding.seq.0,
215            [KeySequenceItem::Control, KeySequenceItem::Byte(b'k')]
216        );
217        assert_eq!(binding.target, ReadlineTarget::Command(String::from("xyz")));
218
219        Ok(())
220    }
221}