Skip to main content

rightkit_control/
keys.rs

1//! US-layout virtual keycodes for named keys (typed text uses Unicode events).
2
3pub 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
46/// AppKit `NSEventModifierFlagNumericPad` / `NSEventModifierFlagFunction`.
47pub const FLAG_NUMPAD: u64 = 0x200000;
48pub const FLAG_FUNCTION: u64 = 0x800000;
49
50/// AppKit delivers navigation and F-keys as private-use function characters with
51/// `NSEventModifierFlagFunction` (arrows also with `NumericPad`). WebKit derives
52/// `KeyboardEvent.key` from those characters; without them it reports "Unidentified".
53pub 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
74/// The printed character of a named punctuation key (`comma` -> `,`).
75pub 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
97/// Parse "cmd+shift+return" into (keycode, flags). Single letters, digits and
98/// punctuation (`cmd+,`, `cmd+/`, `cmd+[`) use ANSI codes; a trailing `+` is the
99/// plus key (`cmd++` = cmd+shift+equal).
100pub 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
175/// The unshifted character a US-layout ANSI keycode produces (letters, digits, punctuation).
176pub 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}