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