//! Scancode mapping table — maps JavaScript `KeyboardEvent.code` strings to //! RDP hardware scancodes (Set 1 / XT scan codes). //! //! Extended keys (those with a 0xE0 prefix on the wire) have the high byte set //! to 0xE0. Use [`is_extended`] and [`scancode_value`] to decompose them. //! //! Ported from `wraith/internal/rdp/input.go`. use std::collections::HashMap; use std::sync::LazyLock; /// RDP mouse event flags — these match the MS-RDPBCGR specification. pub mod mouse_flags { pub const MOVE: u32 = 0x0800; pub const BUTTON1: u32 = 0x1000; // Left pub const BUTTON2: u32 = 0x2000; // Right pub const BUTTON3: u32 = 0x4000; // Middle pub const DOWN: u32 = 0x8000; // Button pressed (absence = released) pub const WHEEL: u32 = 0x0200; // Vertical wheel rotation pub const WHEEL_NEG: u32 = 0x0100; // Negative wheel direction (scroll down) pub const HWHEEL: u32 = 0x0400; // Horizontal wheel rotation } /// Lazily-initialized map from JS `KeyboardEvent.code` to RDP hardware scancode. pub static SCANCODE_MAP: LazyLock> = LazyLock::new(|| { let mut m = HashMap::new(); // ── Row 0: Escape + Function keys ────────────────────────────── m.insert("Escape", 0x0001u32); m.insert("F1", 0x003B); m.insert("F2", 0x003C); m.insert("F3", 0x003D); m.insert("F4", 0x003E); m.insert("F5", 0x003F); m.insert("F6", 0x0040); m.insert("F7", 0x0041); m.insert("F8", 0x0042); m.insert("F9", 0x0043); m.insert("F10", 0x0044); m.insert("F11", 0x0057); m.insert("F12", 0x0058); // ── Row 1: Number row ────────────────────────────────────────── m.insert("Backquote", 0x0029); m.insert("Digit1", 0x0002); m.insert("Digit2", 0x0003); m.insert("Digit3", 0x0004); m.insert("Digit4", 0x0005); m.insert("Digit5", 0x0006); m.insert("Digit6", 0x0007); m.insert("Digit7", 0x0008); m.insert("Digit8", 0x0009); m.insert("Digit9", 0x000A); m.insert("Digit0", 0x000B); m.insert("Minus", 0x000C); m.insert("Equal", 0x000D); m.insert("Backspace", 0x000E); // ── Row 2: QWERTY row ───────────────────────────────────────── m.insert("Tab", 0x000F); m.insert("KeyQ", 0x0010); m.insert("KeyW", 0x0011); m.insert("KeyE", 0x0012); m.insert("KeyR", 0x0013); m.insert("KeyT", 0x0014); m.insert("KeyY", 0x0015); m.insert("KeyU", 0x0016); m.insert("KeyI", 0x0017); m.insert("KeyO", 0x0018); m.insert("KeyP", 0x0019); m.insert("BracketLeft", 0x001A); m.insert("BracketRight", 0x001B); m.insert("Backslash", 0x002B); // ── Row 3: Home row ─────────────────────────────────────────── m.insert("CapsLock", 0x003A); m.insert("KeyA", 0x001E); m.insert("KeyS", 0x001F); m.insert("KeyD", 0x0020); m.insert("KeyF", 0x0021); m.insert("KeyG", 0x0022); m.insert("KeyH", 0x0023); m.insert("KeyJ", 0x0024); m.insert("KeyK", 0x0025); m.insert("KeyL", 0x0026); m.insert("Semicolon", 0x0027); m.insert("Quote", 0x0028); m.insert("Enter", 0x001C); // ── Row 4: Bottom row ───────────────────────────────────────── m.insert("ShiftLeft", 0x002A); m.insert("KeyZ", 0x002C); m.insert("KeyX", 0x002D); m.insert("KeyC", 0x002E); m.insert("KeyV", 0x002F); m.insert("KeyB", 0x0030); m.insert("KeyN", 0x0031); m.insert("KeyM", 0x0032); m.insert("Comma", 0x0033); m.insert("Period", 0x0034); m.insert("Slash", 0x0035); m.insert("ShiftRight", 0x0036); // ── Row 5: Bottom modifiers + space ─────────────────────────── m.insert("ControlLeft", 0x001D); m.insert("MetaLeft", 0xE05B); m.insert("AltLeft", 0x0038); m.insert("Space", 0x0039); m.insert("AltRight", 0xE038); m.insert("MetaRight", 0xE05C); m.insert("ContextMenu", 0xE05D); m.insert("ControlRight", 0xE01D); // ── Navigation cluster ──────────────────────────────────────── m.insert("PrintScreen", 0xE037); m.insert("ScrollLock", 0x0046); m.insert("Pause", 0x0045); m.insert("Insert", 0xE052); m.insert("Home", 0xE047); m.insert("PageUp", 0xE049); m.insert("Delete", 0xE053); m.insert("End", 0xE04F); m.insert("PageDown", 0xE051); // ── Arrow keys ──────────────────────────────────────────────── m.insert("ArrowUp", 0xE048); m.insert("ArrowLeft", 0xE04B); m.insert("ArrowDown", 0xE050); m.insert("ArrowRight", 0xE04D); // ── Numpad ──────────────────────────────────────────────────── m.insert("NumLock", 0x0045); m.insert("NumpadDivide", 0xE035); m.insert("NumpadMultiply", 0x0037); m.insert("NumpadSubtract", 0x004A); m.insert("Numpad7", 0x0047); m.insert("Numpad8", 0x0048); m.insert("Numpad9", 0x0049); m.insert("NumpadAdd", 0x004E); m.insert("Numpad4", 0x004B); m.insert("Numpad5", 0x004C); m.insert("Numpad6", 0x004D); m.insert("Numpad1", 0x004F); m.insert("Numpad2", 0x0050); m.insert("Numpad3", 0x0051); m.insert("NumpadEnter", 0xE01C); m.insert("Numpad0", 0x0052); m.insert("NumpadDecimal", 0x0053); // ── Multimedia / browser keys ───────────────────────────────── m.insert("BrowserBack", 0xE06A); m.insert("BrowserForward", 0xE069); m.insert("BrowserRefresh", 0xE067); m.insert("BrowserStop", 0xE068); m.insert("BrowserSearch", 0xE065); m.insert("BrowserFavorites", 0xE066); m.insert("BrowserHome", 0xE032); m.insert("VolumeMute", 0xE020); m.insert("VolumeDown", 0xE02E); m.insert("VolumeUp", 0xE030); m.insert("MediaTrackNext", 0xE019); m.insert("MediaTrackPrevious", 0xE010); m.insert("MediaStop", 0xE024); m.insert("MediaPlayPause", 0xE022); m.insert("LaunchMail", 0xE06C); m.insert("LaunchApp1", 0xE06B); m.insert("LaunchApp2", 0xE021); // ── International keys ──────────────────────────────────────── m.insert("IntlBackslash", 0x0056); m.insert("IntlYen", 0x007D); m.insert("IntlRo", 0x0073); m }); /// Look up the RDP hardware scancode for a JS `KeyboardEvent.code` string. /// /// Returns `None` for unmapped keys. pub fn js_key_to_scancode(js_code: &str) -> Option { SCANCODE_MAP.get(js_code).copied() } /// Returns `true` if the scancode has the 0xE0 extended prefix. pub fn is_extended(scancode: u32) -> bool { (scancode & 0xFF00) == 0xE000 } /// Returns the low byte of the scancode (the actual value without the prefix). pub fn scancode_value(scancode: u32) -> u8 { (scancode & 0xFF) as u8 }