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
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}