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().peekable();
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 _ => continue,
196 };
197 out.push(input);
198 }
199 out
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn roundtrip_simple_chars() {
208 let keys = vec![
209 Input {
210 key: Key::Char('h'),
211 ..Input::default()
212 },
213 Input {
214 key: Key::Char('i'),
215 ..Input::default()
216 },
217 ];
218 let text = encode_macro(&keys);
219 assert_eq!(text, "hi");
220 assert_eq!(decode_macro(&text), keys);
221 }
222
223 #[test]
224 fn roundtrip_with_special_keys_and_ctrl() {
225 let keys = vec![
226 Input {
227 key: Key::Char('i'),
228 ..Input::default()
229 },
230 Input {
231 key: Key::Char('X'),
232 ..Input::default()
233 },
234 Input {
235 key: Key::Esc,
236 ..Input::default()
237 },
238 Input {
239 key: Key::Char('d'),
240 ctrl: true,
241 ..Input::default()
242 },
243 ];
244 let text = encode_macro(&keys);
245 assert_eq!(text, "iX<Esc><C-d>");
246 assert_eq!(decode_macro(&text), keys);
247 }
248
249 #[test]
250 fn roundtrip_literal_lt() {
251 let keys = vec![Input {
252 key: Key::Char('<'),
253 ..Input::default()
254 }];
255 let text = encode_macro(&keys);
256 assert_eq!(text, "<lt>");
257 assert_eq!(decode_macro(&text), keys);
258 }
259}