wraith/src-tauri/src/rdp/input.rs
Vantz Stockwell c75da74ecd feat: Phase 4 complete — RDP via ironrdp
Rust RDP service: ironrdp client with full connection handshake
(TCP -> TLS -> CredSSP -> NLA), pixel buffer frame delivery,
mouse/keyboard input via scancode mapping, graceful disconnect.
Runs in dedicated thread with own tokio runtime to avoid Send
lifetime issues with ironrdp trait objects.

Vue frontend: RdpView canvas renderer with 30fps polling,
mouse/keyboard capture, RdpToolbar with Ctrl+Alt+Del and
clipboard. SessionContainer handles both SSH and RDP tabs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 16:05:11 -04:00

193 lines
7.3 KiB
Rust

//! 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<HashMap<&'static str, u32>> = 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<u32> {
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
}