Skip to main content

shrub_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, serde::Deserialize))]
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, serde::Deserialize))]
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, serde::Deserialize))]
28pub enum ReadlineTarget {
29    /// A named readline function.
30    Function(String),
31    /// A readline command macro.
32    Macro(String),
33}
34
35/// Represents a key sequence.
36#[derive(Debug, Clone, PartialEq, Eq)]
37#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
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, serde::Deserialize))]
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, serde::Deserialize))]
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 key sequence.
65///
66/// # Arguments
67///
68/// * `input` - The input string to parse
69pub fn parse_key_sequence(input: &str) -> Result<KeySequence, error::BindingParseError> {
70    readline_binding::key_sequence(input)
71        .map_err(|_err| error::BindingParseError::Unknown(input.to_owned()))
72}
73
74/// Parses a binding specification that maps a key sequence
75/// to a shell command.
76///
77/// # Arguments
78///
79/// * `input` - The input string to parse
80pub fn parse_key_sequence_shell_cmd_binding(
81    input: &str,
82) -> Result<KeySequenceShellCommandBinding, error::BindingParseError> {
83    readline_binding::key_sequence_shell_cmd_binding(input)
84        .map_err(|_err| error::BindingParseError::Unknown(input.to_owned()))
85}
86
87/// Parses a binding specification that maps a key sequence
88/// to a readline target.
89///
90/// # Arguments
91///
92/// * `input` - The input string to parse
93pub fn parse_key_sequence_readline_binding(
94    input: &str,
95) -> Result<KeySequenceReadlineBinding, error::BindingParseError> {
96    readline_binding::key_sequence_readline_binding(input)
97        .map_err(|_err| error::BindingParseError::Unknown(input.to_owned()))
98}
99
100/// Converts a `KeySequence` to a vector of `KeyStroke`.
101///
102/// # Arguments
103///
104/// * `seq` - The key sequence to convert
105pub fn key_sequence_to_strokes(
106    seq: &KeySequence,
107) -> Result<Vec<KeyStroke>, error::BindingParseError> {
108    let mut strokes = vec![];
109    let mut current_stroke = KeyStroke::default();
110
111    for item in &seq.0 {
112        if matches!(
113            item,
114            KeySequenceItem::Control | KeySequenceItem::Meta | KeySequenceItem::Byte(b'\x1b')
115        ) && !current_stroke.key_code.is_empty()
116        {
117            strokes.push(current_stroke);
118            current_stroke = KeyStroke::default();
119        }
120
121        match item {
122            KeySequenceItem::Control => current_stroke.control = true,
123            KeySequenceItem::Meta => current_stroke.meta = true,
124            KeySequenceItem::Byte(b) => {
125                current_stroke.key_code.push(*b);
126                // If this is a control or meta stroke, the modifier only
127                // applies to this one byte, so we need to push
128                // the stroke and start fresh for subsequent bytes.
129                if current_stroke.control || current_stroke.meta {
130                    strokes.push(current_stroke);
131                    current_stroke = KeyStroke::default();
132                }
133            }
134        }
135    }
136
137    if current_stroke.key_code.is_empty() {
138        if current_stroke.control || current_stroke.meta {
139            return Err(error::BindingParseError::MissingKeyCode);
140        }
141    } else {
142        strokes.push(current_stroke);
143    }
144
145    Ok(strokes)
146}
147
148peg::parser! {
149    grammar readline_binding() for str {
150        rule _() = [' ' | '\t' | '\n']*
151
152        pub rule key_sequence_shell_cmd_binding() -> KeySequenceShellCommandBinding =
153            _ "\"" seq:key_sequence() "\"" _ ":" _ cmd:shell_cmd() _ { KeySequenceShellCommandBinding { seq, shell_cmd: cmd } }
154
155        pub rule key_sequence_readline_binding() -> KeySequenceReadlineBinding =
156            _ "\"" seq:key_sequence() "\"" _ ":" _ "\"" cmd:readline_cmd() "\"" _ {
157                KeySequenceReadlineBinding { seq, target: ReadlineTarget::Macro(cmd) }
158            } /
159            _ "\"" seq:key_sequence() "\"" _ ":" _ func:readline_function() _ {
160                KeySequenceReadlineBinding { seq, target: ReadlineTarget::Function(func) }
161            }
162
163        rule readline_cmd() -> String = s:$([^'"']*) { s.to_string() }
164        rule shell_cmd() -> String = s:$([_]*) { s.to_string() }
165        rule readline_function() -> String = s:$([_]*) { s.to_string() }
166
167        // Main rule for parsing a key sequence
168        pub rule key_sequence() -> KeySequence =
169            items:key_sequence_item()* { KeySequence(items) }
170
171        rule key_sequence_item() -> KeySequenceItem =
172            "\\C-" { KeySequenceItem::Control } /
173            "\\M-" { KeySequenceItem::Meta } /
174            "\\e" { KeySequenceItem::Byte(b'\x1b') } /
175            "\\\\" { KeySequenceItem::Byte(b'\\') } /
176            "\\\"" { KeySequenceItem::Byte(b'"') } /
177            "\\'" { KeySequenceItem::Byte(b'\'') } /
178            "\\a" { KeySequenceItem::Byte(b'\x07') } /
179            "\\b" { KeySequenceItem::Byte(b'\x08') } /
180            "\\d" { KeySequenceItem::Byte(b'\x7f') } /
181            "\\f" { KeySequenceItem::Byte(b'\x0c') } /
182            "\\n" { KeySequenceItem::Byte(b'\n') } /
183            "\\r" { KeySequenceItem::Byte(b'\r') } /
184            "\\t" { KeySequenceItem::Byte(b'\t') } /
185            "\\v" { KeySequenceItem::Byte(b'\x0b') } /
186            "\\" n:octal_number() { KeySequenceItem::Byte(n) } /
187            "\\" n:hex_number() { KeySequenceItem::Byte(n) } /
188            [c if c != '"'] { KeySequenceItem::Byte(c as u8) }
189
190        rule octal_number() -> u8 =
191            s:$(['0'..='7']*<1,3>) {? u8::from_str_radix(s, 8).or(Err("invalid octal number")) }
192
193        rule hex_number() -> u8 =
194            s:$(['0'..='9' | 'a'..='f' | 'A'..='F']*<1,2>) {? u8::from_str_radix(s, 16).or(Err("invalid hex number")) }
195    }
196}
197
198#[cfg(test)]
199#[expect(clippy::panic_in_result_fn)]
200mod tests {
201    use anyhow::Result;
202
203    use super::*;
204
205    #[test]
206    fn test_basic_shell_cmd_binding_parse() -> Result<()> {
207        let binding = parse_key_sequence_shell_cmd_binding(r#""\C-k": xyz"#)?;
208        assert_eq!(
209            binding.seq.0,
210            [KeySequenceItem::Control, KeySequenceItem::Byte(b'k')]
211        );
212        assert_eq!(binding.shell_cmd, "xyz");
213
214        Ok(())
215    }
216
217    #[test]
218    fn test_basic_readline_func_binding_parse() -> Result<()> {
219        let binding = parse_key_sequence_readline_binding(r#""\M-x": some-function"#)?;
220        assert_eq!(
221            binding.seq.0,
222            [KeySequenceItem::Meta, KeySequenceItem::Byte(b'x')]
223        );
224        assert_eq!(
225            binding.target,
226            ReadlineTarget::Function("some-function".to_string())
227        );
228
229        Ok(())
230    }
231
232    #[test]
233    fn test_basic_readline_cmd_binding_parse() -> Result<()> {
234        let binding = parse_key_sequence_readline_binding(r#""\C-k": "xyz""#)?;
235        assert_eq!(
236            binding.seq.0,
237            [KeySequenceItem::Control, KeySequenceItem::Byte(b'k')]
238        );
239        assert_eq!(binding.target, ReadlineTarget::Macro(String::from("xyz")));
240
241        Ok(())
242    }
243}