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
79pub fn decode_macro(s: &str) -> Vec<Input> {
84 let mut out = Vec::new();
85 let mut chars = s.chars();
86 while let Some(c) = chars.next() {
87 if c != '<' {
88 out.push(Input {
89 key: Key::Char(c),
90 ..Input::default()
91 });
92 continue;
93 }
94 let mut tag = String::new();
95 let mut closed = false;
96 for ch in chars.by_ref() {
97 if ch == '>' {
98 closed = true;
99 break;
100 }
101 tag.push(ch);
102 }
103 if !closed {
104 out.push(Input {
107 key: Key::Char('<'),
108 ..Input::default()
109 });
110 for ch in tag.chars() {
111 out.push(Input {
112 key: Key::Char(ch),
113 ..Input::default()
114 });
115 }
116 continue;
117 }
118 let input = match tag.as_str() {
119 "Esc" => Input {
120 key: Key::Esc,
121 ..Input::default()
122 },
123 "CR" => Input {
124 key: Key::Enter,
125 ..Input::default()
126 },
127 "BS" => Input {
128 key: Key::Backspace,
129 ..Input::default()
130 },
131 "Tab" => Input {
132 key: Key::Tab,
133 ..Input::default()
134 },
135 "Up" => Input {
136 key: Key::Up,
137 ..Input::default()
138 },
139 "Down" => Input {
140 key: Key::Down,
141 ..Input::default()
142 },
143 "Left" => Input {
144 key: Key::Left,
145 ..Input::default()
146 },
147 "Right" => Input {
148 key: Key::Right,
149 ..Input::default()
150 },
151 "Del" => Input {
152 key: Key::Delete,
153 ..Input::default()
154 },
155 "Home" => Input {
156 key: Key::Home,
157 ..Input::default()
158 },
159 "End" => Input {
160 key: Key::End,
161 ..Input::default()
162 },
163 "PageUp" => Input {
164 key: Key::PageUp,
165 ..Input::default()
166 },
167 "PageDown" => Input {
168 key: Key::PageDown,
169 ..Input::default()
170 },
171 "lt" => Input {
172 key: Key::Char('<'),
173 ..Input::default()
174 },
175 t if t.starts_with("C-") => {
176 let Some(ch) = t.chars().nth(2) else {
177 continue;
178 };
179 Input {
180 key: Key::Char(ch),
181 ctrl: true,
182 ..Input::default()
183 }
184 }
185 t if t.starts_with("M-") => {
186 let Some(ch) = t.chars().nth(2) else {
187 continue;
188 };
189 Input {
190 key: Key::Char(ch),
191 alt: true,
192 ..Input::default()
193 }
194 }
195 _ => {
196 out.push(Input {
200 key: Key::Char('<'),
201 ..Input::default()
202 });
203 for ch in tag.chars() {
204 out.push(Input {
205 key: Key::Char(ch),
206 ..Input::default()
207 });
208 }
209 out.push(Input {
210 key: Key::Char('>'),
211 ..Input::default()
212 });
213 continue;
214 }
215 };
216 out.push(input);
217 }
218 out
219}
220
221pub fn from_planned(planned: crate::types::Input) -> Option<Input> {
233 use crate::types::{Input as PlannedInput, SpecialKey};
234 let (key, mods) = match planned {
235 PlannedInput::Char(c, m) => (Key::Char(c), m),
236 PlannedInput::Key(k, m) => {
237 let key = match k {
238 SpecialKey::Esc => Key::Esc,
239 SpecialKey::Enter => Key::Enter,
240 SpecialKey::Backspace => Key::Backspace,
241 SpecialKey::Tab => Key::Tab,
242 SpecialKey::BackTab => Key::Tab,
246 SpecialKey::Up => Key::Up,
247 SpecialKey::Down => Key::Down,
248 SpecialKey::Left => Key::Left,
249 SpecialKey::Right => Key::Right,
250 SpecialKey::Home => Key::Home,
251 SpecialKey::End => Key::End,
252 SpecialKey::PageUp => Key::PageUp,
253 SpecialKey::PageDown => Key::PageDown,
254 SpecialKey::Insert => Key::Null,
258 SpecialKey::Delete => Key::Delete,
259 SpecialKey::F(_) => Key::Null,
260 };
261 let m = if matches!(k, SpecialKey::BackTab) {
262 crate::types::Modifiers { shift: true, ..m }
263 } else {
264 m
265 };
266 (key, m)
267 }
268 PlannedInput::Mouse(_)
270 | PlannedInput::Paste(_)
271 | PlannedInput::FocusGained
272 | PlannedInput::FocusLost
273 | PlannedInput::Resize(_, _) => return None,
274 };
275 if key == Key::Null {
276 return None;
277 }
278 Some(Input {
279 key,
280 ctrl: mods.ctrl,
281 alt: mods.alt,
282 shift: mods.shift,
283 })
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn roundtrip_simple_chars() {
292 let keys = vec![
293 Input {
294 key: Key::Char('h'),
295 ..Input::default()
296 },
297 Input {
298 key: Key::Char('i'),
299 ..Input::default()
300 },
301 ];
302 let text = encode_macro(&keys);
303 assert_eq!(text, "hi");
304 assert_eq!(decode_macro(&text), keys);
305 }
306
307 #[test]
308 fn roundtrip_with_special_keys_and_ctrl() {
309 let keys = vec![
310 Input {
311 key: Key::Char('i'),
312 ..Input::default()
313 },
314 Input {
315 key: Key::Char('X'),
316 ..Input::default()
317 },
318 Input {
319 key: Key::Esc,
320 ..Input::default()
321 },
322 Input {
323 key: Key::Char('d'),
324 ctrl: true,
325 ..Input::default()
326 },
327 ];
328 let text = encode_macro(&keys);
329 assert_eq!(text, "iX<Esc><C-d>");
330 assert_eq!(decode_macro(&text), keys);
331 }
332
333 #[test]
334 fn roundtrip_literal_lt() {
335 let keys = vec![Input {
336 key: Key::Char('<'),
337 ..Input::default()
338 }];
339 let text = encode_macro(&keys);
340 assert_eq!(text, "<lt>");
341 assert_eq!(decode_macro(&text), keys);
342 }
343
344 #[test]
348 fn unknown_tag_replays_literally() {
349 fn ch(c: char) -> Input {
350 Input {
351 key: Key::Char(c),
352 ..Input::default()
353 }
354 }
355 let decoded = decode_macro("<xyz>ok");
356 let expected = vec![
357 ch('<'),
358 ch('x'),
359 ch('y'),
360 ch('z'),
361 ch('>'),
362 ch('o'),
363 ch('k'),
364 ];
365 assert_eq!(decoded, expected);
366 }
367
368 #[test]
370 fn roundtrip_still_holds_after_unknown_tag_fix() {
371 let keys = vec![
372 Input {
373 key: Key::Char('d'),
374 ctrl: true,
375 ..Input::default()
376 },
377 Input {
378 key: Key::Esc,
379 ..Input::default()
380 },
381 ];
382 let text = encode_macro(&keys);
383 assert_eq!(decode_macro(&text), keys);
384 }
385}