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
202pub fn from_planned(planned: crate::types::Input) -> Option<Input> {
214 use crate::types::{Input as PlannedInput, SpecialKey};
215 let (key, mods) = match planned {
216 PlannedInput::Char(c, m) => (Key::Char(c), m),
217 PlannedInput::Key(k, m) => {
218 let key = match k {
219 SpecialKey::Esc => Key::Esc,
220 SpecialKey::Enter => Key::Enter,
221 SpecialKey::Backspace => Key::Backspace,
222 SpecialKey::Tab => Key::Tab,
223 SpecialKey::BackTab => Key::Tab,
227 SpecialKey::Up => Key::Up,
228 SpecialKey::Down => Key::Down,
229 SpecialKey::Left => Key::Left,
230 SpecialKey::Right => Key::Right,
231 SpecialKey::Home => Key::Home,
232 SpecialKey::End => Key::End,
233 SpecialKey::PageUp => Key::PageUp,
234 SpecialKey::PageDown => Key::PageDown,
235 SpecialKey::Insert => Key::Null,
239 SpecialKey::Delete => Key::Delete,
240 SpecialKey::F(_) => Key::Null,
241 };
242 let m = if matches!(k, SpecialKey::BackTab) {
243 crate::types::Modifiers { shift: true, ..m }
244 } else {
245 m
246 };
247 (key, m)
248 }
249 PlannedInput::Mouse(_)
251 | PlannedInput::Paste(_)
252 | PlannedInput::FocusGained
253 | PlannedInput::FocusLost
254 | PlannedInput::Resize(_, _) => return None,
255 };
256 if key == Key::Null {
257 return None;
258 }
259 Some(Input {
260 key,
261 ctrl: mods.ctrl,
262 alt: mods.alt,
263 shift: mods.shift,
264 })
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn roundtrip_simple_chars() {
273 let keys = vec![
274 Input {
275 key: Key::Char('h'),
276 ..Input::default()
277 },
278 Input {
279 key: Key::Char('i'),
280 ..Input::default()
281 },
282 ];
283 let text = encode_macro(&keys);
284 assert_eq!(text, "hi");
285 assert_eq!(decode_macro(&text), keys);
286 }
287
288 #[test]
289 fn roundtrip_with_special_keys_and_ctrl() {
290 let keys = vec![
291 Input {
292 key: Key::Char('i'),
293 ..Input::default()
294 },
295 Input {
296 key: Key::Char('X'),
297 ..Input::default()
298 },
299 Input {
300 key: Key::Esc,
301 ..Input::default()
302 },
303 Input {
304 key: Key::Char('d'),
305 ctrl: true,
306 ..Input::default()
307 },
308 ];
309 let text = encode_macro(&keys);
310 assert_eq!(text, "iX<Esc><C-d>");
311 assert_eq!(decode_macro(&text), keys);
312 }
313
314 #[test]
315 fn roundtrip_literal_lt() {
316 let keys = vec![Input {
317 key: Key::Char('<'),
318 ..Input::default()
319 }];
320 let text = encode_macro(&keys);
321 assert_eq!(text, "<lt>");
322 assert_eq!(decode_macro(&text), keys);
323 }
324}