rightkit-control 0.1.8

Background native input, accessibility reads, and a WebDriver bridge for driving real desktop apps without stealing focus.
Documentation
//! US-layout virtual keycodes for named keys (typed text uses Unicode events).

pub fn named(name: &str) -> Option<u16> {
    Some(match name.to_ascii_lowercase().as_str() {
        "return" | "enter" => 0x24,
        "tab" => 0x30,
        "space" => 0x31,
        "delete" | "backspace" => 0x33,
        "escape" | "esc" => 0x35,
        "left" => 0x7B,
        "right" => 0x7C,
        "down" => 0x7D,
        "up" => 0x7E,
        "home" => 0x73,
        "end" => 0x77,
        "pageup" => 0x74,
        "pagedown" => 0x79,
        "forwarddelete" => 0x75,
        "comma" => 0x2B,
        "period" | "dot" => 0x2F,
        "slash" => 0x2C,
        "backslash" => 0x2A,
        "semicolon" => 0x29,
        "quote" | "apostrophe" => 0x27,
        "minus" => 0x1B,
        "equal" | "equals" => 0x18,
        "leftbracket" => 0x21,
        "rightbracket" => 0x1E,
        "grave" | "backtick" => 0x32,
        "f1" => 0x7A,
        "f2" => 0x78,
        "f3" => 0x63,
        "f4" => 0x76,
        "f5" => 0x60,
        "f6" => 0x61,
        "f7" => 0x62,
        "f8" => 0x64,
        "f9" => 0x65,
        "f10" => 0x6D,
        "f11" => 0x67,
        "f12" => 0x6F,
        _ => return None,
    })
}

/// AppKit `NSEventModifierFlagNumericPad` / `NSEventModifierFlagFunction`.
pub const FLAG_NUMPAD: u64 = 0x200000;
pub const FLAG_FUNCTION: u64 = 0x800000;

/// AppKit delivers navigation and F-keys as private-use function characters with
/// `NSEventModifierFlagFunction` (arrows also with `NumericPad`). WebKit derives
/// `KeyboardEvent.key` from those characters; without them it reports "Unidentified".
pub fn function_char(name: &str) -> Option<(char, u64)> {
    let arrow = FLAG_FUNCTION | FLAG_NUMPAD;
    let (c, f) = match name.to_ascii_lowercase().as_str() {
        "up" => ('\u{F700}', arrow),
        "down" => ('\u{F701}', arrow),
        "left" => ('\u{F702}', arrow),
        "right" => ('\u{F703}', arrow),
        "forwarddelete" => ('\u{F728}', FLAG_FUNCTION),
        "home" => ('\u{F729}', FLAG_FUNCTION),
        "end" => ('\u{F72B}', FLAG_FUNCTION),
        "pageup" => ('\u{F72C}', FLAG_FUNCTION),
        "pagedown" => ('\u{F72D}', FLAG_FUNCTION),
        f if f.len() >= 2 && f.starts_with('f') => {
            let n: u32 = f[1..].parse().ok().filter(|n| (1..=12).contains(n))?;
            (char::from_u32(0xF703 + n)?, FLAG_FUNCTION)
        }
        _ => return None,
    };
    Some((c, f))
}

/// The printed character of a named punctuation key (`comma` -> `,`).
pub fn named_char(name: &str) -> Option<char> {
    Some(match name.to_ascii_lowercase().as_str() {
        "comma" => ',',
        "period" | "dot" => '.',
        "slash" => '/',
        "backslash" => '\\',
        "semicolon" => ';',
        "quote" | "apostrophe" => '\'',
        "minus" => '-',
        "equal" | "equals" => '=',
        "leftbracket" => '[',
        "rightbracket" => ']',
        "grave" | "backtick" => '`',
        _ => return None,
    })
}

pub const FLAG_SHIFT: u64 = 0x20000;
pub const FLAG_CTRL: u64 = 0x40000;
pub const FLAG_ALT: u64 = 0x80000;
pub const FLAG_CMD: u64 = 0x100000;

/// Parse "cmd+shift+return" into (keycode, flags). Single letters, digits and
/// punctuation (`cmd+,`, `cmd+/`, `cmd+[`) use ANSI codes; a trailing `+` is the
/// plus key (`cmd++` = cmd+shift+equal).
pub fn chord(spec: &str) -> Option<(u16, u64)> {
    let mut flags = 0;
    let mut key = None;
    let spec = match spec.strip_suffix("++") {
        Some(rest) => {
            flags |= FLAG_SHIFT;
            key = Some(0x18);
            rest
        }
        None if spec == "+" => return Some((0x18, FLAG_SHIFT)),
        None => spec,
    };
    let plus_key = key.is_some();
    for part in spec.split('+').filter(|p| !p.is_empty() || !plus_key) {
        match part.to_ascii_lowercase().as_str() {
            "cmd" | "command" => flags |= FLAG_CMD,
            "shift" => flags |= FLAG_SHIFT,
            "ctrl" | "control" => flags |= FLAG_CTRL,
            "alt" | "option" => flags |= FLAG_ALT,
            other => key = Some(named(other).or_else(|| letter(other))?),
        }
    }
    key.map(|k| (k, flags))
}

const ANSI: &[(char, u16)] = &[
    ('a', 0),
    ('s', 1),
    ('d', 2),
    ('f', 3),
    ('h', 4),
    ('g', 5),
    ('z', 6),
    ('x', 7),
    ('c', 8),
    ('v', 9),
    ('b', 11),
    ('q', 12),
    ('w', 13),
    ('e', 14),
    ('r', 15),
    ('y', 16),
    ('t', 17),
    ('o', 31),
    ('u', 32),
    ('i', 34),
    ('p', 35),
    ('l', 37),
    ('j', 38),
    ('k', 40),
    ('n', 45),
    ('m', 46),
    ('1', 18),
    ('2', 19),
    ('3', 20),
    ('4', 21),
    ('6', 22),
    ('5', 23),
    ('=', 24),
    ('9', 25),
    ('7', 26),
    ('-', 27),
    ('8', 28),
    ('0', 29),
    (']', 30),
    ('[', 33),
    ('\'', 39),
    (';', 41),
    ('\\', 42),
    (',', 43),
    ('/', 44),
    ('.', 47),
    ('`', 50),
];

/// The unshifted character a US-layout ANSI keycode produces (letters, digits, punctuation).
pub fn ansi_char(code: u16) -> Option<char> {
    ANSI.iter().find(|(_, k)| *k == code).map(|(l, _)| *l)
}

fn letter(s: &str) -> Option<u16> {
    let c = s.chars().next().filter(|_| s.chars().count() == 1)?;
    ANSI.iter().find(|(l, _)| *l == c).map(|(_, k)| *k)
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn parses_chords() {
        assert_eq!(chord("cmd+a"), Some((0, FLAG_CMD)));
        assert_eq!(chord("shift+tab"), Some((0x30, FLAG_SHIFT)));
        assert_eq!(chord("nope"), None);
        assert_eq!(chord("cmd+,"), Some((0x2B, FLAG_CMD)));
        assert_eq!(chord("cmd+comma"), Some((0x2B, FLAG_CMD)));
        assert_eq!(chord("cmd+shift+/"), Some((0x2C, FLAG_CMD | FLAG_SHIFT)));
        assert_eq!(chord("cmd+["), Some((0x21, FLAG_CMD)));
        assert_eq!(chord("cmd+1"), Some((18, FLAG_CMD)));
        assert_eq!(chord("cmd++"), Some((0x18, FLAG_CMD | FLAG_SHIFT)));
        assert_eq!(chord("f5"), Some((0x60, 0)));
        assert_eq!(chord("cmd+."), Some((0x2F, FLAG_CMD)));
    }
    #[test]
    fn function_keys_carry_appkit_characters() {
        let arrow = FLAG_FUNCTION | FLAG_NUMPAD;
        assert_eq!(function_char("right"), Some(('\u{F703}', arrow)));
        assert_eq!(function_char("Up"), Some(('\u{F700}', arrow)));
        assert_eq!(function_char("home"), Some(('\u{F729}', FLAG_FUNCTION)));
        assert_eq!(function_char("pagedown"), Some(('\u{F72D}', FLAG_FUNCTION)));
        assert_eq!(
            function_char("forwarddelete"),
            Some(('\u{F728}', FLAG_FUNCTION))
        );
        assert_eq!(function_char("f1"), Some(('\u{F704}', FLAG_FUNCTION)));
        assert_eq!(function_char("f12"), Some(('\u{F70F}', FLAG_FUNCTION)));
        assert_eq!(function_char("f13"), None);
        assert_eq!(function_char("a"), None);
        assert_eq!(named_char("comma"), Some(','));
    }
}