shrub_parser/
readline_binding.rs1use crate::error;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
8pub struct KeySequenceShellCommandBinding {
9 pub seq: KeySequence,
11 pub shell_cmd: String,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
18pub struct KeySequenceReadlineBinding {
19 pub seq: KeySequence,
21 pub target: ReadlineTarget,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
28pub enum ReadlineTarget {
29 Function(String),
31 Macro(String),
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
38pub struct KeySequence(pub Vec<KeySequenceItem>);
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
43pub enum KeySequenceItem {
44 Control,
46 Meta,
48 Byte(u8),
50}
51
52#[derive(Debug, Default, Clone, PartialEq, Eq)]
54#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
55pub struct KeyStroke {
56 pub meta: bool,
58 pub control: bool,
60 pub key_code: Vec<u8>,
62}
63
64pub 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
74pub 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
87pub 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
100pub 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 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 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}