1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
12pub enum Key {
13 Char(char),
14 Backspace,
15 Enter,
16 Left,
17 Right,
18 Up,
19 Down,
20 Tab,
21 Delete,
22 Home,
23 End,
24 PageUp,
25 PageDown,
26 Esc,
27 #[default]
28 Null,
29}
30
31#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
34pub struct Input {
35 pub key: Key,
36 pub ctrl: bool,
37 pub alt: bool,
38 pub shift: bool,
39}
40
41pub fn encode_macro(inputs: &[Input]) -> String {
45 let mut out = String::new();
46 for input in inputs {
47 match input.key {
48 Key::Char(c) if input.ctrl => {
49 out.push_str("<C-");
50 out.push(c);
51 out.push('>');
52 }
53 Key::Char(c) if input.alt => {
54 out.push_str("<M-");
55 out.push(c);
56 out.push('>');
57 }
58 Key::Char('<') => out.push_str("<lt>"),
59 Key::Char(c) => out.push(c),
60 Key::Esc => out.push_str("<Esc>"),
61 Key::Enter => out.push_str("<CR>"),
62 Key::Backspace => out.push_str("<BS>"),
63 Key::Tab => out.push_str("<Tab>"),
64 Key::Up => out.push_str("<Up>"),
65 Key::Down => out.push_str("<Down>"),
66 Key::Left => out.push_str("<Left>"),
67 Key::Right => out.push_str("<Right>"),
68 Key::Delete => out.push_str("<Del>"),
69 Key::Home => out.push_str("<Home>"),
70 Key::End => out.push_str("<End>"),
71 Key::PageUp => out.push_str("<PageUp>"),
72 Key::PageDown => out.push_str("<PageDown>"),
73 Key::Null => {}
74 }
75 }
76 out
77}
78
79const LONGEST_TAG: usize = "PageDown".len();
83
84pub fn decode_macro(s: &str) -> Vec<Input> {
91 let chars: Vec<char> = s.chars().collect();
92 let mut out = Vec::new();
93 let mut i = 0;
94 while i < chars.len() {
95 let c = chars[i];
96 i += 1;
97 if c == '<' {
98 let rest = &chars[i..];
99 let close = rest.iter().take(LONGEST_TAG + 1).position(|&ch| ch == '>');
100 if let Some(len) = close
101 && let Some(input) = key_from_tag(&rest[..len].iter().collect::<String>())
102 {
103 out.push(input);
104 i += len + 1;
105 continue;
106 }
107 }
108 out.push(Input {
109 key: Key::Char(c),
110 ..Input::default()
111 });
112 }
113 out
114}
115
116fn key_from_tag(tag: &str) -> Option<Input> {
118 let key = match tag {
119 "Esc" => Key::Esc,
120 "CR" => Key::Enter,
121 "BS" => Key::Backspace,
122 "Tab" => Key::Tab,
123 "Space" => Key::Char(' '),
124 "Up" => Key::Up,
125 "Down" => Key::Down,
126 "Left" => Key::Left,
127 "Right" => Key::Right,
128 "Del" => Key::Delete,
129 "Home" => Key::Home,
130 "End" => Key::End,
131 "PageUp" => Key::PageUp,
132 "PageDown" => Key::PageDown,
133 "lt" => Key::Char('<'),
134 t => {
135 let mut parts = t.chars();
136 let (modifier, dash, ch) = (parts.next()?, parts.next()?, parts.next()?);
137 if dash != '-' || parts.next().is_some() {
138 return None;
139 }
140 return match modifier {
141 'C' => Some(Input {
142 key: Key::Char(ch),
143 ctrl: true,
144 ..Input::default()
145 }),
146 'M' => Some(Input {
147 key: Key::Char(ch),
148 alt: true,
149 ..Input::default()
150 }),
151 _ => None,
152 };
153 }
154 };
155 Some(Input {
156 key,
157 ..Input::default()
158 })
159}
160
161pub fn from_planned(planned: crate::types::Input) -> Option<Input> {
173 use crate::types::{Input as PlannedInput, SpecialKey};
174 let (key, mods) = match planned {
175 PlannedInput::Char(c, m) => (Key::Char(c), m),
176 PlannedInput::Key(k, m) => {
177 let key = match k {
178 SpecialKey::Esc => Key::Esc,
179 SpecialKey::Enter => Key::Enter,
180 SpecialKey::Backspace => Key::Backspace,
181 SpecialKey::Tab => Key::Tab,
182 SpecialKey::BackTab => Key::Tab,
186 SpecialKey::Up => Key::Up,
187 SpecialKey::Down => Key::Down,
188 SpecialKey::Left => Key::Left,
189 SpecialKey::Right => Key::Right,
190 SpecialKey::Home => Key::Home,
191 SpecialKey::End => Key::End,
192 SpecialKey::PageUp => Key::PageUp,
193 SpecialKey::PageDown => Key::PageDown,
194 SpecialKey::Insert => Key::Null,
198 SpecialKey::Delete => Key::Delete,
199 SpecialKey::F(_) => Key::Null,
200 };
201 let m = if matches!(k, SpecialKey::BackTab) {
202 crate::types::Modifiers { shift: true, ..m }
203 } else {
204 m
205 };
206 (key, m)
207 }
208 PlannedInput::Mouse(_)
210 | PlannedInput::Paste(_)
211 | PlannedInput::FocusGained
212 | PlannedInput::FocusLost
213 | PlannedInput::Resize(_, _) => return None,
214 };
215 if key == Key::Null {
216 return None;
217 }
218 Some(Input {
219 key,
220 ctrl: mods.ctrl,
221 alt: mods.alt,
222 shift: mods.shift,
223 })
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn roundtrip_simple_chars() {
232 let keys = vec![
233 Input {
234 key: Key::Char('h'),
235 ..Input::default()
236 },
237 Input {
238 key: Key::Char('i'),
239 ..Input::default()
240 },
241 ];
242 let text = encode_macro(&keys);
243 assert_eq!(text, "hi");
244 assert_eq!(decode_macro(&text), keys);
245 }
246
247 #[test]
248 fn roundtrip_with_special_keys_and_ctrl() {
249 let keys = vec![
250 Input {
251 key: Key::Char('i'),
252 ..Input::default()
253 },
254 Input {
255 key: Key::Char('X'),
256 ..Input::default()
257 },
258 Input {
259 key: Key::Esc,
260 ..Input::default()
261 },
262 Input {
263 key: Key::Char('d'),
264 ctrl: true,
265 ..Input::default()
266 },
267 ];
268 let text = encode_macro(&keys);
269 assert_eq!(text, "iX<Esc><C-d>");
270 assert_eq!(decode_macro(&text), keys);
271 }
272
273 #[test]
274 fn roundtrip_literal_lt() {
275 let keys = vec![Input {
276 key: Key::Char('<'),
277 ..Input::default()
278 }];
279 let text = encode_macro(&keys);
280 assert_eq!(text, "<lt>");
281 assert_eq!(decode_macro(&text), keys);
282 }
283
284 #[test]
288 fn unknown_tag_replays_literally() {
289 fn ch(c: char) -> Input {
290 Input {
291 key: Key::Char(c),
292 ..Input::default()
293 }
294 }
295 let decoded = decode_macro("<xyz>ok");
296 let expected = vec![
297 ch('<'),
298 ch('x'),
299 ch('y'),
300 ch('z'),
301 ch('>'),
302 ch('o'),
303 ch('k'),
304 ];
305 assert_eq!(decoded, expected);
306 }
307
308 #[test]
310 fn space_tag_decodes_to_a_space() {
311 let space = Input {
312 key: Key::Char(' '),
313 ..Input::default()
314 };
315 assert_eq!(decode_macro("<Space>"), vec![space]);
316 }
317
318 #[test]
322 fn literal_lt_rescans_for_a_nested_tag() {
323 fn ch(c: char) -> Input {
324 Input {
325 key: Key::Char(c),
326 ..Input::default()
327 }
328 }
329 let esc = Input {
330 key: Key::Esc,
331 ..Input::default()
332 };
333 assert_eq!(
334 decode_macro("<<$<Esc>"),
335 vec![ch('<'), ch('<'), ch('$'), esc]
336 );
337 assert_eq!(decode_macro("<<Esc>"), vec![ch('<'), esc]);
338 assert_eq!(decode_macro("<ab"), vec![ch('<'), ch('a'), ch('b')]);
339 assert_eq!(
340 decode_macro("<C-ab>"),
341 "<C-ab>".chars().map(ch).collect::<Vec<_>>()
342 );
343 }
344
345 #[test]
347 fn roundtrip_still_holds_after_unknown_tag_fix() {
348 let keys = vec![
349 Input {
350 key: Key::Char('d'),
351 ctrl: true,
352 ..Input::default()
353 },
354 Input {
355 key: Key::Esc,
356 ..Input::default()
357 },
358 ];
359 let text = encode_macro(&keys);
360 assert_eq!(decode_macro(&text), keys);
361 }
362}