1pub fn named(name: &str) -> Option<u16> {
4 Some(match name.to_ascii_lowercase().as_str() {
5 "return" | "enter" => 0x24,
6 "tab" => 0x30,
7 "space" => 0x31,
8 "delete" | "backspace" => 0x33,
9 "escape" | "esc" => 0x35,
10 "left" => 0x7B,
11 "right" => 0x7C,
12 "down" => 0x7D,
13 "up" => 0x7E,
14 "home" => 0x73,
15 "end" => 0x77,
16 "pageup" => 0x74,
17 "pagedown" => 0x79,
18 "forwarddelete" => 0x75,
19 "comma" => 0x2B,
20 "period" | "dot" => 0x2F,
21 "slash" => 0x2C,
22 "backslash" => 0x2A,
23 "semicolon" => 0x29,
24 "quote" | "apostrophe" => 0x27,
25 "minus" => 0x1B,
26 "equal" | "equals" => 0x18,
27 "leftbracket" => 0x21,
28 "rightbracket" => 0x1E,
29 "grave" | "backtick" => 0x32,
30 "f1" => 0x7A,
31 "f2" => 0x78,
32 "f3" => 0x63,
33 "f4" => 0x76,
34 "f5" => 0x60,
35 "f6" => 0x61,
36 "f7" => 0x62,
37 "f8" => 0x64,
38 "f9" => 0x65,
39 "f10" => 0x6D,
40 "f11" => 0x67,
41 "f12" => 0x6F,
42 _ => return None,
43 })
44}
45
46pub const FLAG_NUMPAD: u64 = 0x200000;
48pub const FLAG_FUNCTION: u64 = 0x800000;
49
50pub fn function_char(name: &str) -> Option<(char, u64)> {
54 let arrow = FLAG_FUNCTION | FLAG_NUMPAD;
55 let (c, f) = match name.to_ascii_lowercase().as_str() {
56 "up" => ('\u{F700}', arrow),
57 "down" => ('\u{F701}', arrow),
58 "left" => ('\u{F702}', arrow),
59 "right" => ('\u{F703}', arrow),
60 "forwarddelete" => ('\u{F728}', FLAG_FUNCTION),
61 "home" => ('\u{F729}', FLAG_FUNCTION),
62 "end" => ('\u{F72B}', FLAG_FUNCTION),
63 "pageup" => ('\u{F72C}', FLAG_FUNCTION),
64 "pagedown" => ('\u{F72D}', FLAG_FUNCTION),
65 f if f.len() >= 2 && f.starts_with('f') => {
66 let n: u32 = f[1..].parse().ok().filter(|n| (1..=12).contains(n))?;
67 (char::from_u32(0xF703 + n)?, FLAG_FUNCTION)
68 }
69 _ => return None,
70 };
71 Some((c, f))
72}
73
74pub fn named_char(name: &str) -> Option<char> {
76 Some(match name.to_ascii_lowercase().as_str() {
77 "comma" => ',',
78 "period" | "dot" => '.',
79 "slash" => '/',
80 "backslash" => '\\',
81 "semicolon" => ';',
82 "quote" | "apostrophe" => '\'',
83 "minus" => '-',
84 "equal" | "equals" => '=',
85 "leftbracket" => '[',
86 "rightbracket" => ']',
87 "grave" | "backtick" => '`',
88 _ => return None,
89 })
90}
91
92pub const FLAG_SHIFT: u64 = 0x20000;
93pub const FLAG_CTRL: u64 = 0x40000;
94pub const FLAG_ALT: u64 = 0x80000;
95pub const FLAG_CMD: u64 = 0x100000;
96
97pub fn chord(spec: &str) -> Option<(u16, u64)> {
101 let mut flags = 0;
102 let mut key = None;
103 let spec = match spec.strip_suffix("++") {
104 Some(rest) => {
105 flags |= FLAG_SHIFT;
106 key = Some(0x18);
107 rest
108 }
109 None if spec == "+" => return Some((0x18, FLAG_SHIFT)),
110 None => spec,
111 };
112 let plus_key = key.is_some();
113 for part in spec.split('+').filter(|p| !p.is_empty() || !plus_key) {
114 match part.to_ascii_lowercase().as_str() {
115 "cmd" | "command" => flags |= FLAG_CMD,
116 "shift" => flags |= FLAG_SHIFT,
117 "ctrl" | "control" => flags |= FLAG_CTRL,
118 "alt" | "option" => flags |= FLAG_ALT,
119 other => key = Some(named(other).or_else(|| letter(other))?),
120 }
121 }
122 key.map(|k| (k, flags))
123}
124
125fn letter(s: &str) -> Option<u16> {
126 const ANSI: &[(char, u16)] = &[
127 ('a', 0),
128 ('s', 1),
129 ('d', 2),
130 ('f', 3),
131 ('h', 4),
132 ('g', 5),
133 ('z', 6),
134 ('x', 7),
135 ('c', 8),
136 ('v', 9),
137 ('b', 11),
138 ('q', 12),
139 ('w', 13),
140 ('e', 14),
141 ('r', 15),
142 ('y', 16),
143 ('t', 17),
144 ('o', 31),
145 ('u', 32),
146 ('i', 34),
147 ('p', 35),
148 ('l', 37),
149 ('j', 38),
150 ('k', 40),
151 ('n', 45),
152 ('m', 46),
153 ('1', 18),
154 ('2', 19),
155 ('3', 20),
156 ('4', 21),
157 ('6', 22),
158 ('5', 23),
159 ('=', 24),
160 ('9', 25),
161 ('7', 26),
162 ('-', 27),
163 ('8', 28),
164 ('0', 29),
165 (']', 30),
166 ('[', 33),
167 ('\'', 39),
168 (';', 41),
169 ('\\', 42),
170 (',', 43),
171 ('/', 44),
172 ('.', 47),
173 ('`', 50),
174 ];
175 let c = s.chars().next().filter(|_| s.chars().count() == 1)?;
176 ANSI.iter().find(|(l, _)| *l == c).map(|(_, k)| *k)
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 #[test]
183 fn parses_chords() {
184 assert_eq!(chord("cmd+a"), Some((0, FLAG_CMD)));
185 assert_eq!(chord("shift+tab"), Some((0x30, FLAG_SHIFT)));
186 assert_eq!(chord("nope"), None);
187 assert_eq!(chord("cmd+,"), Some((0x2B, FLAG_CMD)));
188 assert_eq!(chord("cmd+comma"), Some((0x2B, FLAG_CMD)));
189 assert_eq!(chord("cmd+shift+/"), Some((0x2C, FLAG_CMD | FLAG_SHIFT)));
190 assert_eq!(chord("cmd+["), Some((0x21, FLAG_CMD)));
191 assert_eq!(chord("cmd+1"), Some((18, FLAG_CMD)));
192 assert_eq!(chord("cmd++"), Some((0x18, FLAG_CMD | FLAG_SHIFT)));
193 assert_eq!(chord("f5"), Some((0x60, 0)));
194 assert_eq!(chord("cmd+."), Some((0x2F, FLAG_CMD)));
195 }
196 #[test]
197 fn function_keys_carry_appkit_characters() {
198 let arrow = FLAG_FUNCTION | FLAG_NUMPAD;
199 assert_eq!(function_char("right"), Some(('\u{F703}', arrow)));
200 assert_eq!(function_char("Up"), Some(('\u{F700}', arrow)));
201 assert_eq!(function_char("home"), Some(('\u{F729}', FLAG_FUNCTION)));
202 assert_eq!(function_char("pagedown"), Some(('\u{F72D}', FLAG_FUNCTION)));
203 assert_eq!(
204 function_char("forwarddelete"),
205 Some(('\u{F728}', FLAG_FUNCTION))
206 );
207 assert_eq!(function_char("f1"), Some(('\u{F704}', FLAG_FUNCTION)));
208 assert_eq!(function_char("f12"), Some(('\u{F70F}', FLAG_FUNCTION)));
209 assert_eq!(function_char("f13"), None);
210 assert_eq!(function_char("a"), None);
211 assert_eq!(named_char("comma"), Some(','));
212 }
213}