1use anyhow::{bail, Result};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct KeyDef {
18 pub key: &'static str,
20 pub code: &'static str,
22 pub vk: u32,
24 pub text: Option<&'static str>,
31 pub bidi: &'static str,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Modifier {
40 Alt = 1,
41 Control = 2,
42 Meta = 4,
43 Shift = 8,
44}
45
46impl Modifier {
47 pub fn def(self) -> KeyDef {
48 match self {
49 Modifier::Alt => KeyDef {
50 key: "Alt",
51 code: "AltLeft",
52 vk: 0x12,
53 text: None,
54 bidi: "\u{E00A}",
55 },
56 Modifier::Control => KeyDef {
57 key: "Control",
58 code: "ControlLeft",
59 vk: 0x11,
60 text: None,
61 bidi: "\u{E009}",
62 },
63 Modifier::Meta => KeyDef {
64 key: "Meta",
65 code: "MetaLeft",
66 vk: 0x5B,
67 text: None,
68 bidi: "\u{E03D}",
69 },
70 Modifier::Shift => KeyDef {
71 key: "Shift",
72 code: "ShiftLeft",
73 vk: 0x10,
74 text: None,
75 bidi: "\u{E008}",
76 },
77 }
78 }
79
80 fn parse(name: &str) -> Option<Modifier> {
81 match name.to_ascii_lowercase().as_str() {
82 "alt" | "option" => Some(Modifier::Alt),
83 "control" | "ctrl" => Some(Modifier::Control),
84 "meta" | "cmd" | "command" | "super" => Some(Modifier::Meta),
85 "shift" => Some(Modifier::Shift),
86 _ => None,
87 }
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct Chord {
94 pub modifiers: Vec<Modifier>,
95 pub key: KeyDef,
96}
97
98impl Chord {
99 pub fn plain(key: KeyDef) -> Chord {
100 Chord {
101 modifiers: Vec::new(),
102 key,
103 }
104 }
105
106 pub fn mask(&self) -> u32 {
108 self.modifiers.iter().fold(0, |m, k| m | (*k as u32))
109 }
110}
111
112const NAMED: &[KeyDef] = &[
113 KeyDef {
114 key: "Enter",
115 code: "Enter",
116 vk: 0x0D,
117 text: Some("\r"),
118 bidi: "\u{E007}",
119 },
120 KeyDef {
121 key: "Tab",
122 code: "Tab",
123 vk: 0x09,
124 text: Some("\t"),
125 bidi: "\u{E004}",
126 },
127 KeyDef {
128 key: "Escape",
129 code: "Escape",
130 vk: 0x1B,
131 text: None,
132 bidi: "\u{E00C}",
133 },
134 KeyDef {
135 key: "Backspace",
136 code: "Backspace",
137 vk: 0x08,
138 text: None,
139 bidi: "\u{E003}",
140 },
141 KeyDef {
142 key: "Delete",
143 code: "Delete",
144 vk: 0x2E,
145 text: None,
146 bidi: "\u{E017}",
147 },
148 KeyDef {
149 key: "Insert",
150 code: "Insert",
151 vk: 0x2D,
152 text: None,
153 bidi: "\u{E016}",
154 },
155 KeyDef {
156 key: " ",
157 code: "Space",
158 vk: 0x20,
159 text: Some(" "),
160 bidi: "\u{E00D}",
161 },
162 KeyDef {
163 key: "Home",
164 code: "Home",
165 vk: 0x24,
166 text: None,
167 bidi: "\u{E011}",
168 },
169 KeyDef {
170 key: "End",
171 code: "End",
172 vk: 0x23,
173 text: None,
174 bidi: "\u{E010}",
175 },
176 KeyDef {
177 key: "PageUp",
178 code: "PageUp",
179 vk: 0x21,
180 text: None,
181 bidi: "\u{E00E}",
182 },
183 KeyDef {
184 key: "PageDown",
185 code: "PageDown",
186 vk: 0x22,
187 text: None,
188 bidi: "\u{E00F}",
189 },
190 KeyDef {
191 key: "ArrowUp",
192 code: "ArrowUp",
193 vk: 0x26,
194 text: None,
195 bidi: "\u{E013}",
196 },
197 KeyDef {
198 key: "ArrowDown",
199 code: "ArrowDown",
200 vk: 0x28,
201 text: None,
202 bidi: "\u{E015}",
203 },
204 KeyDef {
205 key: "ArrowLeft",
206 code: "ArrowLeft",
207 vk: 0x25,
208 text: None,
209 bidi: "\u{E012}",
210 },
211 KeyDef {
212 key: "ArrowRight",
213 code: "ArrowRight",
214 vk: 0x27,
215 text: None,
216 bidi: "\u{E014}",
217 },
218 KeyDef {
219 key: "F1",
220 code: "F1",
221 vk: 0x70,
222 text: None,
223 bidi: "\u{E031}",
224 },
225 KeyDef {
226 key: "F2",
227 code: "F2",
228 vk: 0x71,
229 text: None,
230 bidi: "\u{E032}",
231 },
232 KeyDef {
233 key: "F3",
234 code: "F3",
235 vk: 0x72,
236 text: None,
237 bidi: "\u{E033}",
238 },
239 KeyDef {
240 key: "F4",
241 code: "F4",
242 vk: 0x73,
243 text: None,
244 bidi: "\u{E034}",
245 },
246 KeyDef {
247 key: "F5",
248 code: "F5",
249 vk: 0x74,
250 text: None,
251 bidi: "\u{E035}",
252 },
253 KeyDef {
254 key: "F6",
255 code: "F6",
256 vk: 0x75,
257 text: None,
258 bidi: "\u{E036}",
259 },
260 KeyDef {
261 key: "F7",
262 code: "F7",
263 vk: 0x76,
264 text: None,
265 bidi: "\u{E037}",
266 },
267 KeyDef {
268 key: "F8",
269 code: "F8",
270 vk: 0x77,
271 text: None,
272 bidi: "\u{E038}",
273 },
274 KeyDef {
275 key: "F9",
276 code: "F9",
277 vk: 0x78,
278 text: None,
279 bidi: "\u{E039}",
280 },
281 KeyDef {
282 key: "F10",
283 code: "F10",
284 vk: 0x79,
285 text: None,
286 bidi: "\u{E03A}",
287 },
288 KeyDef {
289 key: "F11",
290 code: "F11",
291 vk: 0x7A,
292 text: None,
293 bidi: "\u{E03B}",
294 },
295 KeyDef {
296 key: "F12",
297 code: "F12",
298 vk: 0x7B,
299 text: None,
300 bidi: "\u{E03C}",
301 },
302];
303
304pub const ENTER: KeyDef = NAMED[0];
306
307fn alias(name: &str) -> Option<&'static str> {
310 match name.to_ascii_lowercase().as_str() {
311 "space" | "spacebar" => Some(" "),
312 "esc" => Some("Escape"),
313 "return" => Some("Enter"),
314 "del" => Some("Delete"),
315 "up" => Some("ArrowUp"),
316 "down" => Some("ArrowDown"),
317 "left" => Some("ArrowLeft"),
318 "right" => Some("ArrowRight"),
319 "pgup" => Some("PageUp"),
320 "pgdn" | "pgdown" => Some("PageDown"),
321 _ => None,
322 }
323}
324
325pub fn lookup(name: &str) -> Option<KeyDef> {
328 let wanted = alias(name).unwrap_or(name);
329 if let Some(def) = NAMED
330 .iter()
331 .find(|d| d.key.eq_ignore_ascii_case(wanted) && !wanted.is_empty())
332 {
333 return Some(*def);
334 }
335 let mut chars = wanted.chars();
336 match (chars.next(), chars.next()) {
337 (Some(c), None) if c.is_ascii_graphic() => Some(printable(c)),
338 _ => None,
339 }
340}
341
342fn printable(c: char) -> KeyDef {
348 let upper = c.to_ascii_uppercase();
349 let code: &'static str = match upper {
350 'A'..='Z' => Box::leak(format!("Key{upper}").into_boxed_str()),
351 '0'..='9' => Box::leak(format!("Digit{upper}").into_boxed_str()),
352 _ => "",
353 };
354 KeyDef {
355 key: Box::leak(c.to_string().into_boxed_str()),
356 code,
357 vk: if upper.is_ascii_alphanumeric() {
359 upper as u32
360 } else {
361 0
362 },
363 text: Some(Box::leak(c.to_string().into_boxed_str())),
364 bidi: Box::leak(c.to_string().into_boxed_str()),
365 }
366}
367
368pub fn parse_chord(spec: &str) -> Result<Chord> {
371 if spec.is_empty() {
372 bail!("empty key");
373 }
374 let (mod_spec, key_name) = if let Some(head) = spec.strip_suffix('+') {
378 (head.trim_end_matches('+'), "+")
379 } else {
380 match spec.rsplit_once('+') {
381 Some((head, key)) => (head, key),
382 None => ("", spec),
383 }
384 };
385
386 let mut modifiers = Vec::new();
387 for m in mod_spec.split('+').filter(|s| !s.is_empty()) {
388 match Modifier::parse(m) {
389 Some(modifier) => modifiers.push(modifier),
390 None => bail!(
391 "unknown modifier `{m}` in `{spec}`. Known: Control/Ctrl, Shift, Alt/Option, Meta/Cmd"
392 ),
393 }
394 }
395
396 let key = lookup(key_name).ok_or_else(|| {
397 anyhow::anyhow!(
398 "unknown key `{key_name}` in `{spec}`.{}",
399 suggestion(key_name)
400 )
401 })?;
402 Ok(Chord { modifiers, key })
403}
404
405fn suggestion(name: &str) -> String {
408 let lower = name.to_ascii_lowercase();
409 let near: Vec<&str> = NAMED
410 .iter()
411 .map(|d| d.key)
412 .filter(|k| {
413 let k = k.to_ascii_lowercase();
414 k.starts_with(&lower) || lower.starts_with(&k)
415 })
416 .take(3)
417 .collect();
418 if near.is_empty() {
419 " Named keys are e.g. Enter, Tab, Escape, ArrowDown, F5; \
420 a single character presses that character."
421 .to_string()
422 } else {
423 format!(" Did you mean {}?", near.join(", "))
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430
431 #[test]
432 fn plain_named_key() {
433 let c = parse_chord("Enter").unwrap();
434 assert!(c.modifiers.is_empty());
435 assert_eq!(c.key.key, "Enter");
436 assert_eq!(c.mask(), 0);
437 }
438
439 #[test]
440 fn named_keys_are_case_insensitive() {
441 assert_eq!(parse_chord("escape").unwrap().key.key, "Escape");
442 assert_eq!(parse_chord("ARROWDOWN").unwrap().key.key, "ArrowDown");
443 }
444
445 #[test]
446 fn single_characters_are_case_sensitive() {
447 assert_eq!(parse_chord("a").unwrap().key.key, "a");
448 assert_eq!(parse_chord("A").unwrap().key.key, "A");
449 }
450
451 #[test]
452 fn modifiers_combine_into_the_cdp_mask() {
453 let c = parse_chord("Control+A").unwrap();
454 assert_eq!(c.modifiers, vec![Modifier::Control]);
455 assert_eq!(c.mask(), 2);
456 assert_eq!(parse_chord("Control+Shift+K").unwrap().mask(), 2 | 8);
457 assert_eq!(parse_chord("Alt+Meta+x").unwrap().mask(), 1 | 4);
458 }
459
460 #[test]
461 fn modifier_aliases() {
462 assert_eq!(parse_chord("Ctrl+a").unwrap().mask(), 2);
463 assert_eq!(parse_chord("Cmd+a").unwrap().mask(), 4);
464 assert_eq!(parse_chord("Option+a").unwrap().mask(), 1);
465 }
466
467 #[test]
468 fn trailing_plus_is_the_plus_key() {
469 let c = parse_chord("Control++").unwrap();
471 assert_eq!(c.modifiers, vec![Modifier::Control]);
472 assert_eq!(c.key.key, "+");
473
474 let bare = parse_chord("+").unwrap();
475 assert!(bare.modifiers.is_empty());
476 assert_eq!(bare.key.key, "+");
477
478 let two = parse_chord("Control+Shift++").unwrap();
479 assert_eq!(two.modifiers, vec![Modifier::Control, Modifier::Shift]);
480 assert_eq!(two.key.key, "+");
481 }
482
483 #[test]
484 fn space_is_reachable_by_name() {
485 assert_eq!(parse_chord("Space").unwrap().key.code, "Space");
487 }
488
489 #[test]
490 fn named_keys_that_insert_nothing_carry_no_text() {
491 for name in ["ArrowDown", "Escape", "F5", "Home", "Backspace"] {
493 assert_eq!(lookup(name).unwrap().text, None, "{name} must not insert");
494 }
495 }
496
497 #[test]
498 fn enter_still_carries_the_carriage_return() {
499 assert_eq!(ENTER.text, Some("\r"));
501 assert_eq!(ENTER.vk, 13);
502 }
503
504 #[test]
505 fn printable_keys_insert_themselves() {
506 let a = lookup("a").unwrap();
507 assert_eq!(a.text, Some("a"));
508 assert_eq!(a.code, "KeyA");
509 assert_eq!(a.vk, 'A' as u32);
510 }
511
512 #[test]
513 fn unknown_key_names_the_closest_match() {
514 let err = parse_chord("Ente").unwrap_err().to_string();
515 assert!(err.contains("Enter"), "{err}");
516 }
517
518 #[test]
519 fn unknown_modifier_is_rejected_rather_than_ignored() {
520 let err = parse_chord("Hyper+a").unwrap_err().to_string();
521 assert!(err.contains("Hyper") && err.contains("Control"), "{err}");
522 }
523
524 #[test]
525 fn empty_input_is_an_error() {
526 assert!(parse_chord("").is_err());
527 }
528
529 #[test]
530 fn bidi_values_are_the_webdriver_namespace_not_the_key_name() {
531 assert_eq!(lookup("ArrowDown").unwrap().bidi, "\u{E015}");
532 assert_eq!(ENTER.bidi, "\u{E007}");
533 }
534}