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
125const ANSI: &[(char, u16)] = &[
126 ('a', 0),
127 ('s', 1),
128 ('d', 2),
129 ('f', 3),
130 ('h', 4),
131 ('g', 5),
132 ('z', 6),
133 ('x', 7),
134 ('c', 8),
135 ('v', 9),
136 ('b', 11),
137 ('q', 12),
138 ('w', 13),
139 ('e', 14),
140 ('r', 15),
141 ('y', 16),
142 ('t', 17),
143 ('o', 31),
144 ('u', 32),
145 ('i', 34),
146 ('p', 35),
147 ('l', 37),
148 ('j', 38),
149 ('k', 40),
150 ('n', 45),
151 ('m', 46),
152 ('1', 18),
153 ('2', 19),
154 ('3', 20),
155 ('4', 21),
156 ('6', 22),
157 ('5', 23),
158 ('=', 24),
159 ('9', 25),
160 ('7', 26),
161 ('-', 27),
162 ('8', 28),
163 ('0', 29),
164 (']', 30),
165 ('[', 33),
166 ('\'', 39),
167 (';', 41),
168 ('\\', 42),
169 (',', 43),
170 ('/', 44),
171 ('.', 47),
172 ('`', 50),
173];
174
175pub fn ansi_char(code: u16) -> Option<char> {
177 ANSI.iter().find(|(_, k)| *k == code).map(|(l, _)| *l)
178}
179
180fn letter(s: &str) -> Option<u16> {
181 let c = s.chars().next().filter(|_| s.chars().count() == 1)?;
182 ANSI.iter().find(|(l, _)| *l == c).map(|(_, k)| *k)
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 #[test]
189 fn parses_chords() {
190 assert_eq!(chord("cmd+a"), Some((0, FLAG_CMD)));
191 assert_eq!(chord("shift+tab"), Some((0x30, FLAG_SHIFT)));
192 assert_eq!(chord("nope"), None);
193 assert_eq!(chord("cmd+,"), Some((0x2B, FLAG_CMD)));
194 assert_eq!(chord("cmd+comma"), Some((0x2B, FLAG_CMD)));
195 assert_eq!(chord("cmd+shift+/"), Some((0x2C, FLAG_CMD | FLAG_SHIFT)));
196 assert_eq!(chord("cmd+["), Some((0x21, FLAG_CMD)));
197 assert_eq!(chord("cmd+1"), Some((18, FLAG_CMD)));
198 assert_eq!(chord("cmd++"), Some((0x18, FLAG_CMD | FLAG_SHIFT)));
199 assert_eq!(chord("f5"), Some((0x60, 0)));
200 assert_eq!(chord("cmd+."), Some((0x2F, FLAG_CMD)));
201 }
202 #[test]
203 fn function_keys_carry_appkit_characters() {
204 let arrow = FLAG_FUNCTION | FLAG_NUMPAD;
205 assert_eq!(function_char("right"), Some(('\u{F703}', arrow)));
206 assert_eq!(function_char("Up"), Some(('\u{F700}', arrow)));
207 assert_eq!(function_char("home"), Some(('\u{F729}', FLAG_FUNCTION)));
208 assert_eq!(function_char("pagedown"), Some(('\u{F72D}', FLAG_FUNCTION)));
209 assert_eq!(
210 function_char("forwarddelete"),
211 Some(('\u{F728}', FLAG_FUNCTION))
212 );
213 assert_eq!(function_char("f1"), Some(('\u{F704}', FLAG_FUNCTION)));
214 assert_eq!(function_char("f12"), Some(('\u{F70F}', FLAG_FUNCTION)));
215 assert_eq!(function_char("f13"), None);
216 assert_eq!(function_char("a"), None);
217 assert_eq!(named_char("comma"), Some(','));
218 }
219}