brush_parser/
readline_binding.rs1use crate::error;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7#[cfg_attr(test, derive(serde::Serialize))]
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))]
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))]
28pub enum ReadlineTarget {
29 Function(String),
31 Command(String),
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37#[cfg_attr(test, derive(serde::Serialize))]
38pub struct KeySequence(pub Vec<KeySequenceItem>);
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42#[cfg_attr(test, derive(serde::Serialize))]
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))]
55pub struct KeyStroke {
56 pub meta: bool,
58 pub control: bool,
60 pub key_code: Vec<u8>,
62}
63
64pub 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
77pub 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
90pub 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 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}