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>
193 lines
7.3 KiB
Rust
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
|
|
}
|