bern_protocol/
shortcut.rs1pub const SHORTCUT_KEYS: &[&str] = &[
11 "space", "escape", "return", "tab", "delete", "up", "down", "left", "right", "home", "end",
12 "pageup", "pagedown",
13];
14
15pub fn parse_shortcut(text: &str) -> Result<String, String> {
20 let parts: Vec<String> = text
21 .split('+')
22 .map(|part| part.trim().to_lowercase())
23 .collect();
24 let (key, modifiers) = parts.split_last().ok_or("an empty shortcut")?;
25 let mut ordered = Vec::new();
26 for wanted in ["cmd", "ctrl", "alt", "shift"] {
27 match modifiers.iter().filter(|m| *m == wanted).count() {
28 0 => {}
29 1 => ordered.push(wanted.to_string()),
30 _ => return Err(format!("`{wanted}` is written twice")),
31 }
32 }
33 if let Some(other) = modifiers
34 .iter()
35 .find(|m| !["cmd", "ctrl", "alt", "shift"].contains(&m.as_str()))
36 {
37 return Err(format!(
38 "`{other}` is not a modifier; use cmd, ctrl, alt or shift"
39 ));
40 }
41 let printable = key.chars().count() == 1
42 && key
43 .chars()
44 .all(|c| c.is_ascii_graphic() && !c.is_ascii_uppercase());
45 if !printable && !SHORTCUT_KEYS.contains(&key.as_str()) {
46 return Err(format!(
47 "`{key}` is not a key; use one character or one of {}",
48 SHORTCUT_KEYS.join(", ")
49 ));
50 }
51 ordered.push(key.clone());
52 Ok(ordered.join("+"))
53}
54
55pub fn same_keys(a: &str, b: &str, apple: bool) -> bool {
60 let pressed = |text: &str| -> Option<String> {
61 let normal = parse_shortcut(text).ok()?;
62 if apple {
63 return Some(normal);
64 }
65 let mut parts: Vec<&str> = normal.split('+').collect();
66 let key = parts.pop()?;
67 let control = parts.iter().any(|part| *part == "cmd" || *part == "ctrl");
68 let mut keys: Vec<&str> = Vec::new();
69 if control {
70 keys.push("cmd");
71 }
72 keys.extend(
73 parts
74 .iter()
75 .filter(|part| **part == "alt" || **part == "shift"),
76 );
77 keys.push(key);
78 Some(keys.join("+"))
79 };
80 matches!((pressed(a), pressed(b)), (Some(a), Some(b)) if a == b)
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn shortcuts_normalize_their_modifiers_and_key() {
89 assert_eq!(parse_shortcut("Shift + Cmd + K").unwrap(), "cmd+shift+k");
90 assert_eq!(parse_shortcut("space").unwrap(), "space");
91 assert_eq!(parse_shortcut("alt+ctrl+left").unwrap(), "ctrl+alt+left");
92 assert_eq!(parse_shortcut("cmd+,").unwrap(), "cmd+,");
93 assert!(parse_shortcut("cmd++").is_err());
95 assert!(parse_shortcut("").is_err());
96 assert!(parse_shortcut("cmd+cmd+k").is_err());
97 assert!(parse_shortcut("hyper+k").is_err());
98 assert!(parse_shortcut("cmd+f13").is_err());
99 }
100
101 #[test]
102 fn control_and_command_are_one_key_away_from_apple() {
103 assert!(same_keys("cmd+k", "ctrl+k", false));
104 assert!(!same_keys("cmd+k", "ctrl+k", true));
105 assert!(same_keys("shift+cmd+k", "cmd+shift+k", true));
106 assert!(!same_keys("", "", true));
107 assert!(!same_keys("cmd+k", "cmd+j", false));
108 }
109}