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> {
75 readline_binding::key_sequence(input)
76 .map_err(|_err| error::BindingParseError::Unknown(input.to_owned()))
77}
78
79pub 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
97pub 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
115pub 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 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 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}