mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 01:27:11 +00:00
fix(rdclient): align web keyboard encoding with RustDesk native contract
Add keyboard-encoder.js for Legacy chr+modifiers and full Map scancode path so Shift, Caps Lock, and symbol keys work reliably on relay sessions including Hyper-V.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# RdClient Web — keyboard troubleshooting
|
||||
|
||||
Symptoms such as wrong characters after some time, broken shortcuts, or Hyper-V / VM Connect typing issues often involve **sticky remote modifiers** or **nested remote keyboard translation**.
|
||||
Symptoms such as wrong characters after some time, broken Shift/Caps Lock, or Hyper-V / VM Connect typing issues often involve **sticky remote modifiers**, **Legacy vs Map encoding**, or **nested remote keyboard translation**.
|
||||
|
||||
## Quick recovery (operator)
|
||||
|
||||
@@ -12,28 +12,33 @@ Symptoms such as wrong characters after some time, broken shortcuts, or Hyper-V
|
||||
|
||||
1. Connect via **Settings → Remote** (rdclient web).
|
||||
2. Hold **Shift** or **Ctrl**, then switch to another browser tab or click the address bar **without** releasing the modifier.
|
||||
3. Return to the remote tab and type — if characters are wrong, the issue matches missing keyup sync (fixed in recent builds via blur/tab release).
|
||||
3. Return to the remote tab and type — if characters are wrong, the issue matches missing keyup sync (fixed via blur/tab release).
|
||||
4. Click **Reset Keyboard State** — typing should return to normal.
|
||||
|
||||
## Compare with native RustDesk client
|
||||
|
||||
| Step | Native RustDesk | rdclient web |
|
||||
|------|-----------------|--------------|
|
||||
| Same host, same task (e.g. Hyper-V Manager) | Baseline | If only web fails → web pipeline |
|
||||
| After tab switch with modifier held | May also stick | Should auto-release after fix |
|
||||
| Hyper-V VM Connect nested in session | Known fragile | Use **Keyboard Mode → Map** or connect directly to guest |
|
||||
| Same host, same task (e.g. Hyper-V Manager) | Baseline | Should match after parity encoder |
|
||||
| Shift + letter | Uppercase | Same (Map scancode + Shift controlKey, or Legacy `a` + Shift modifier) |
|
||||
| After tab switch with modifier held | May also stick | Auto-releases tracked keys on blur |
|
||||
| Hyper-V VM Connect nested in session | Known fragile | Prefer **Auto** or **Map**; connect to guest when possible |
|
||||
|
||||
**Interpretation:** If native RustDesk works but rdclient web fails on the same host, focus on web client settings (Keyboard Mode, Reset Keyboard). If both fail inside VM Connect, prefer **Enhanced Session**, direct RDP to the guest, or RustDesk installed on the VM.
|
||||
**Interpretation:** If native RustDesk works but rdclient web fails on the same host, try **Reset Keyboard State**, then **Legacy** mode for accented typing (ą, ü). If both fail inside VM Connect, prefer **Enhanced Session**, direct RDP to the guest, or RustDesk on the VM.
|
||||
|
||||
## Keyboard modes (Display settings)
|
||||
|
||||
| Mode | When to use |
|
||||
|------|-------------|
|
||||
| **Auto** (default) | Map (scancode) for Windows peers; Legacy elsewhere |
|
||||
| **Legacy** | Normal text entry, accented characters (ą, ü, …) |
|
||||
| **Map** | Hyper-V, VM console, BIOS, apps needing physical key positions |
|
||||
| **Auto** (default) | **Map (scancode) for all keys** — same default as the RustDesk desktop client. Best for Windows, Hyper-V, VM console, Shift+symbols. |
|
||||
| **Legacy** | Layout-specific characters (ą, ü, …), AltGr, or when Map misbehaves on a specific app |
|
||||
| **Map** | Explicit scancode mode (same wire format as Auto) |
|
||||
|
||||
Map mode sends RustDesk `KeyEvent` with `mode: Map` and scancode in `chr` (same contract as the desktop client).
|
||||
### Wire contract (matches RustDesk native)
|
||||
|
||||
- **Auto / Map:** printable keys → `KeyEvent.mode = Map`, scancode in `chr`; Shift/Ctrl/Alt sent as separate Legacy `controlKey` events; `modifiers` on Map chr events carry **CapsLock/NumLock only**.
|
||||
- **Legacy:** printable keys → lowercase `chr` + `modifiers` (Shift/Ctrl/Alt as needed); server applies case via `need_to_uppercase()`.
|
||||
- **Caps Lock / Num Lock / Scroll Lock keys:** no wire event (state synced via `modifiers` on following letter/numpad keys).
|
||||
|
||||
## Hyper-V / nested remote
|
||||
|
||||
@@ -41,10 +46,11 @@ Chain: **Browser → rdclient → RustDesk on host → Hyper-V / VM Connect →
|
||||
|
||||
- Avoid configuring VMs through double/triple nested remote when possible.
|
||||
- Connect rdclient **directly to the guest** if it runs RustDesk.
|
||||
- For host-only Hyper-V Manager work, **Map** or **Auto** mode on Windows peers improves scancode fidelity.
|
||||
- **Auto** (Map) is the recommended default for host Hyper-V Manager and VM Connect.
|
||||
|
||||
## Related code
|
||||
|
||||
- [`web-nodejs/public/js/rdclient/input.js`](../../web-nodejs/public/js/rdclient/input.js) — capture, release, Legacy/Map
|
||||
- [`web-nodejs/public/js/rdclient/keyboard-encoder.js`](../../web-nodejs/public/js/rdclient/keyboard-encoder.js) — RustDesk-compatible KeyEvent encoder
|
||||
- [`web-nodejs/public/js/rdclient/input.js`](../../web-nodejs/public/js/rdclient/input.js) — DOM capture, blur/stop release
|
||||
- [`web-nodejs/public/js/rdclient/keyboard-scancode.js`](../../web-nodejs/public/js/rdclient/keyboard-scancode.js) — DOM `code` → scancode tables
|
||||
- [`web-nodejs/tests/rdclient.input.test.js`](../../web-nodejs/tests/rdclient.input.test.js) — regression tests
|
||||
- [`web-nodejs/tests/rdclient.input.test.js`](../../web-nodejs/tests/rdclient.input.test.js) — parity regression tests
|
||||
|
||||
@@ -1194,7 +1194,7 @@
|
||||
"reset_keyboard": "إعادة تعيين حالة لوحة المفاتيح",
|
||||
"reset_keyboard_done": "تمت إعادة تعيين لوحة المفاتيح على الجهاز البعيد",
|
||||
"keyboard_mode": "وضع لوحة المفاتيح",
|
||||
"keyboard_mode_auto": "تلقائي (Map على Windows)",
|
||||
"keyboard_mode_auto": "تلقائي (Map، مثل RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (حرف)",
|
||||
"keyboard_mode_map": "Map (رمز مسح فعلي)"
|
||||
},
|
||||
|
||||
@@ -1092,7 +1092,7 @@
|
||||
"reset_keyboard": "Reset stavu klávesnice",
|
||||
"reset_keyboard_done": "Stav klávesnice na vzdáleném PC byl resetován",
|
||||
"keyboard_mode": "Režim klávesnice",
|
||||
"keyboard_mode_auto": "Auto (Map ve Windows)",
|
||||
"keyboard_mode_auto": "Auto (Map, jako RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (znak)",
|
||||
"keyboard_mode_map": "Map (fyzický scancode)",
|
||||
"display_settings": "Nastavení zobrazení",
|
||||
|
||||
@@ -1098,7 +1098,7 @@
|
||||
"reset_keyboard": "Nulstil tastaturtilstand",
|
||||
"reset_keyboard_done": "Tastaturtilstand nulstillet på fjernen",
|
||||
"keyboard_mode": "Tastaturtilstand",
|
||||
"keyboard_mode_auto": "Auto (Map på Windows)",
|
||||
"keyboard_mode_auto": "Auto (Map, som RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (tegn)",
|
||||
"keyboard_mode_map": "Map (fysisk scancode)",
|
||||
"display_settings": "Skærmindstillinger",
|
||||
|
||||
@@ -1092,7 +1092,7 @@
|
||||
"reset_keyboard": "Tastaturzustand zurücksetzen",
|
||||
"reset_keyboard_done": "Tastaturzustand auf dem Remote-PC zurückgesetzt",
|
||||
"keyboard_mode": "Tastaturmodus",
|
||||
"keyboard_mode_auto": "Auto (Map unter Windows)",
|
||||
"keyboard_mode_auto": "Auto (Map, wie RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (Zeichen)",
|
||||
"keyboard_mode_map": "Map (physischer Scancode)",
|
||||
"display_settings": "Anzeigeeinstellungen",
|
||||
|
||||
@@ -1117,7 +1117,7 @@
|
||||
"reset_keyboard": "Reset Keyboard State",
|
||||
"reset_keyboard_done": "Keyboard state reset on remote",
|
||||
"keyboard_mode": "Keyboard Mode",
|
||||
"keyboard_mode_auto": "Auto (Map on Windows)",
|
||||
"keyboard_mode_auto": "Auto (Map, like RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (character)",
|
||||
"keyboard_mode_map": "Map (physical scancode)",
|
||||
"display_settings": "Display Settings",
|
||||
|
||||
@@ -1092,7 +1092,7 @@
|
||||
"reset_keyboard": "Restablecer estado del teclado",
|
||||
"reset_keyboard_done": "Estado del teclado restablecido en el remoto",
|
||||
"keyboard_mode": "Modo de teclado",
|
||||
"keyboard_mode_auto": "Auto (Map en Windows)",
|
||||
"keyboard_mode_auto": "Auto (Map, como RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (carácter)",
|
||||
"keyboard_mode_map": "Map (escaneo físico)",
|
||||
"display_settings": "Ajustes de pantalla",
|
||||
|
||||
@@ -1098,7 +1098,7 @@
|
||||
"reset_keyboard": "Nollaa näppäimistön tila",
|
||||
"reset_keyboard_done": "Näppäimistön tila nollattu etäkoneella",
|
||||
"keyboard_mode": "Näppäimistötila",
|
||||
"keyboard_mode_auto": "Auto (Map Windowsissa)",
|
||||
"keyboard_mode_auto": "Auto (Map, kuten RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (merkki)",
|
||||
"keyboard_mode_map": "Map (fyysinen skannauskoodi)",
|
||||
"display_settings": "Näyttöasetukset",
|
||||
|
||||
@@ -1113,7 +1113,7 @@
|
||||
"reset_keyboard": "Réinitialiser le clavier",
|
||||
"reset_keyboard_done": "État du clavier réinitialisé à distance",
|
||||
"keyboard_mode": "Mode clavier",
|
||||
"keyboard_mode_auto": "Auto (Map sous Windows)",
|
||||
"keyboard_mode_auto": "Auto (Map, comme RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (caractère)",
|
||||
"keyboard_mode_map": "Map (scancode physique)",
|
||||
"display_settings": "Paramètres d'affichage",
|
||||
|
||||
@@ -1098,7 +1098,7 @@
|
||||
"reset_keyboard": "कीबोर्ड स्थिति रीसेट करें",
|
||||
"reset_keyboard_done": "रिमोट पर कीबोर्ड स्थिति रीसेट की गई",
|
||||
"keyboard_mode": "कीबोर्ड मोड",
|
||||
"keyboard_mode_auto": "ऑटो (Windows पर Map)",
|
||||
"keyboard_mode_auto": "ऑटो (Map, RustDesk जैसा)",
|
||||
"keyboard_mode_legacy": "Legacy (अक्षर)",
|
||||
"keyboard_mode_map": "Map (भौतिक scancode)",
|
||||
"display_settings": "प्रदर्शन सेटिंग्स",
|
||||
|
||||
@@ -1098,7 +1098,7 @@
|
||||
"reset_keyboard": "Billentyűzet állapot visszaállítása",
|
||||
"reset_keyboard_done": "A távoli billentyűzet állapota visszaállítva",
|
||||
"keyboard_mode": "Billentyűzet mód",
|
||||
"keyboard_mode_auto": "Auto (Map Windows-on)",
|
||||
"keyboard_mode_auto": "Auto (Map, mint a RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (karakter)",
|
||||
"keyboard_mode_map": "Map (fizikai scancode)",
|
||||
"display_settings": "Megjelenítési beállítások",
|
||||
|
||||
@@ -1098,7 +1098,7 @@
|
||||
"reset_keyboard": "Reset status keyboard",
|
||||
"reset_keyboard_done": "Status keyboard di remote direset",
|
||||
"keyboard_mode": "Mode keyboard",
|
||||
"keyboard_mode_auto": "Otomatis (Map di Windows)",
|
||||
"keyboard_mode_auto": "Otomatis (Map, seperti RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (karakter)",
|
||||
"keyboard_mode_map": "Map (scancode fisik)",
|
||||
"display_settings": "Pengaturan Tampilan",
|
||||
|
||||
@@ -1092,7 +1092,7 @@
|
||||
"reset_keyboard": "Reimposta stato tastiera",
|
||||
"reset_keyboard_done": "Stato tastiera reimpostato sul remoto",
|
||||
"keyboard_mode": "Modalità tastiera",
|
||||
"keyboard_mode_auto": "Auto (Map su Windows)",
|
||||
"keyboard_mode_auto": "Auto (Map, come RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (carattere)",
|
||||
"keyboard_mode_map": "Map (scancode fisico)",
|
||||
"display_settings": "Impostazioni schermo",
|
||||
|
||||
@@ -1092,7 +1092,7 @@
|
||||
"reset_keyboard": "キーボード状態をリセット",
|
||||
"reset_keyboard_done": "リモート側のキーボード状態をリセットしました",
|
||||
"keyboard_mode": "キーボードモード",
|
||||
"keyboard_mode_auto": "自動(Windows では Map)",
|
||||
"keyboard_mode_auto": "自動(Map、RustDesk と同様)",
|
||||
"keyboard_mode_legacy": "Legacy(文字)",
|
||||
"keyboard_mode_map": "Map(物理スキャンコード)",
|
||||
"display_settings": "表示設定",
|
||||
|
||||
@@ -1092,7 +1092,7 @@
|
||||
"reset_keyboard": "키보드 상태 재설정",
|
||||
"reset_keyboard_done": "원격 키보드 상태가 재설정되었습니다",
|
||||
"keyboard_mode": "키보드 모드",
|
||||
"keyboard_mode_auto": "자동(Windows에서 Map)",
|
||||
"keyboard_mode_auto": "자동(Map, RustDesk와 동일)",
|
||||
"keyboard_mode_legacy": "Legacy(문자)",
|
||||
"keyboard_mode_map": "Map(물리 스캔코드)",
|
||||
"display_settings": "디스플레이 설정",
|
||||
|
||||
@@ -1098,7 +1098,7 @@
|
||||
"reset_keyboard": "Tilbakestill tastaturtilstand",
|
||||
"reset_keyboard_done": "Tastaturtilstand tilbakestilt på ekstern maskin",
|
||||
"keyboard_mode": "Tastaturmodus",
|
||||
"keyboard_mode_auto": "Auto (Map på Windows)",
|
||||
"keyboard_mode_auto": "Auto (Map, som RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (tegn)",
|
||||
"keyboard_mode_map": "Map (fysisk scancode)",
|
||||
"display_settings": "Skjerminnstillinger",
|
||||
|
||||
@@ -1092,7 +1092,7 @@
|
||||
"reset_keyboard": "Toetsenbordstatus resetten",
|
||||
"reset_keyboard_done": "Toetsenbordstatus op remote gereset",
|
||||
"keyboard_mode": "Toetsenbordmodus",
|
||||
"keyboard_mode_auto": "Auto (Map op Windows)",
|
||||
"keyboard_mode_auto": "Auto (Map, zoals RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (teken)",
|
||||
"keyboard_mode_map": "Map (fysieke scancode)",
|
||||
"display_settings": "Scherminstellingen",
|
||||
|
||||
@@ -1109,7 +1109,7 @@
|
||||
"reset_keyboard": "Resetuj stan klawiatury",
|
||||
"reset_keyboard_done": "Stan klawiatury zresetowany na hoście zdalnym",
|
||||
"keyboard_mode": "Tryb klawiatury",
|
||||
"keyboard_mode_auto": "Auto (Map w Windows)",
|
||||
"keyboard_mode_auto": "Auto (Map, jak RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (znak)",
|
||||
"keyboard_mode_map": "Map (skan fizyczny)",
|
||||
"display_settings": "Ustawienia wyświetlania",
|
||||
|
||||
@@ -1092,7 +1092,7 @@
|
||||
"reset_keyboard": "Repor estado do teclado",
|
||||
"reset_keyboard_done": "Estado do teclado reposto no remoto",
|
||||
"keyboard_mode": "Modo de teclado",
|
||||
"keyboard_mode_auto": "Auto (Map no Windows)",
|
||||
"keyboard_mode_auto": "Auto (Map, como RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (carácter)",
|
||||
"keyboard_mode_map": "Map (scancode físico)",
|
||||
"display_settings": "Definições do ecrã",
|
||||
|
||||
@@ -1098,7 +1098,7 @@
|
||||
"reset_keyboard": "Resetează starea tastaturii",
|
||||
"reset_keyboard_done": "Starea tastaturii a fost resetată la distanță",
|
||||
"keyboard_mode": "Mod tastatură",
|
||||
"keyboard_mode_auto": "Auto (Map pe Windows)",
|
||||
"keyboard_mode_auto": "Auto (Map, ca RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (caracter)",
|
||||
"keyboard_mode_map": "Map (scancode fizic)",
|
||||
"display_settings": "Setări de afișare",
|
||||
|
||||
@@ -1098,7 +1098,7 @@
|
||||
"reset_keyboard": "Återställ tangentbordsläge",
|
||||
"reset_keyboard_done": "Tangentbordsläge återställt på fjärrdatorn",
|
||||
"keyboard_mode": "Tangentbordsläge",
|
||||
"keyboard_mode_auto": "Auto (Map i Windows)",
|
||||
"keyboard_mode_auto": "Auto (Map, som RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (tecken)",
|
||||
"keyboard_mode_map": "Map (fysisk scancode)",
|
||||
"display_settings": "Displayinställningar",
|
||||
|
||||
@@ -1098,7 +1098,7 @@
|
||||
"reset_keyboard": "รีเซ็ตสถานะคีย์บอร์ด",
|
||||
"reset_keyboard_done": "รีเซ็ตสถานะคีย์บอร์ดบนเครื่องระยะไกลแล้ว",
|
||||
"keyboard_mode": "โหมดคีย์บอร์ด",
|
||||
"keyboard_mode_auto": "อัตโนมัติ (Map บน Windows)",
|
||||
"keyboard_mode_auto": "อัตโนมัติ (Map เหมือน RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (ตัวอักษร)",
|
||||
"keyboard_mode_map": "Map (scancode ทางกายภาพ)",
|
||||
"display_settings": "การตั้งค่าการแสดงผล",
|
||||
|
||||
@@ -1098,7 +1098,7 @@
|
||||
"reset_keyboard": "Klavye durumunu sıfırla",
|
||||
"reset_keyboard_done": "Uzak klavye durumu sıfırlandı",
|
||||
"keyboard_mode": "Klavye modu",
|
||||
"keyboard_mode_auto": "Otomatik (Windows'ta Map)",
|
||||
"keyboard_mode_auto": "Otomatik (Map, RustDesk gibi)",
|
||||
"keyboard_mode_legacy": "Legacy (karakter)",
|
||||
"keyboard_mode_map": "Map (fiziksel scancode)",
|
||||
"display_settings": "Ekran Ayarları",
|
||||
|
||||
@@ -1098,7 +1098,7 @@
|
||||
"reset_keyboard": "Скинути стан клавіатури",
|
||||
"reset_keyboard_done": "Стан клавіатури на віддаленому ПК скинуто",
|
||||
"keyboard_mode": "Режим клавіатури",
|
||||
"keyboard_mode_auto": "Авто (Map у Windows)",
|
||||
"keyboard_mode_auto": "Авто (Map, як RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (символ)",
|
||||
"keyboard_mode_map": "Map (фізичний scancode)",
|
||||
"display_settings": "Налаштування дисплея",
|
||||
|
||||
@@ -1098,7 +1098,7 @@
|
||||
"reset_keyboard": "Đặt lại trạng thái bàn phím",
|
||||
"reset_keyboard_done": "Đã đặt lại bàn phím trên máy remote",
|
||||
"keyboard_mode": "Chế độ bàn phím",
|
||||
"keyboard_mode_auto": "Tự động (Map trên Windows)",
|
||||
"keyboard_mode_auto": "Tự động (Map, giống RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (ký tự)",
|
||||
"keyboard_mode_map": "Map (scancode vật lý)",
|
||||
"display_settings": "Cài đặt hiển thị",
|
||||
|
||||
@@ -1113,7 +1113,7 @@
|
||||
"reset_keyboard": "重設鍵盤狀態",
|
||||
"reset_keyboard_done": "已在遠端重設鍵盤狀態",
|
||||
"keyboard_mode": "鍵盤模式",
|
||||
"keyboard_mode_auto": "自動(Windows 使用 Map)",
|
||||
"keyboard_mode_auto": "自動(Map,與 RustDesk 相同)",
|
||||
"keyboard_mode_legacy": "Legacy(字元)",
|
||||
"keyboard_mode_map": "Map(實體掃描碼)",
|
||||
"display_settings": "顯示設置",
|
||||
|
||||
@@ -1094,7 +1094,7 @@
|
||||
"reset_keyboard": "重置键盘状态",
|
||||
"reset_keyboard_done": "已在远程端重置键盘状态",
|
||||
"keyboard_mode": "键盘模式",
|
||||
"keyboard_mode_auto": "自动(Windows 使用 Map)",
|
||||
"keyboard_mode_auto": "自动(Map,与 RustDesk 相同)",
|
||||
"keyboard_mode_legacy": "Legacy(字符)",
|
||||
"keyboard_mode_map": "Map(物理扫描码)",
|
||||
"display_settings": "显示设置",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Captures keyboard and mouse events and converts them to RustDesk protocol messages
|
||||
*/
|
||||
|
||||
/* global RDProtocol, RDKeyboardScancode */
|
||||
/* global RDProtocol, RDKeyboardScancode, RDKeyboardEncoder */
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
class RDInput {
|
||||
@@ -33,16 +33,14 @@ class RDInput {
|
||||
this.peerPlatform = '';
|
||||
/**
|
||||
* Keyboard wire mode: 'Legacy' | 'Map' | 'Auto'.
|
||||
* Auto uses Map for Windows peers (Hyper-V / VM console), Legacy otherwise.
|
||||
* Auto resolves to Map (RustDesk native default).
|
||||
* @type {'Legacy'|'Map'|'Auto'}
|
||||
*/
|
||||
this.keyboardMode = 'Auto';
|
||||
|
||||
// Mouse move throttling (~60 Hz for smoother remote control)
|
||||
this._lastMouseSendTime = 0;
|
||||
this._mouseThrottleMs = 16;
|
||||
|
||||
// Bound event handlers (for removal)
|
||||
this._onMouseMove = this._handleMouseMove.bind(this);
|
||||
this._onMouseDown = this._handleMouseDown.bind(this);
|
||||
this._onMouseUp = this._handleMouseUp.bind(this);
|
||||
@@ -77,9 +75,6 @@ class RDInput {
|
||||
this._releaseAllKeys(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start capturing input events
|
||||
*/
|
||||
start() {
|
||||
if (this.enabled) return;
|
||||
|
||||
@@ -90,31 +85,21 @@ class RDInput {
|
||||
c.addEventListener('wheel', this._onWheel, { passive: false });
|
||||
c.addEventListener('contextmenu', this._onContextMenu);
|
||||
|
||||
// Keyboard events on document (canvas needs focus for key events)
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
document.addEventListener('keyup', this._onKeyUp);
|
||||
|
||||
// Pointer Lock change detection
|
||||
document.addEventListener('pointerlockchange', this._onPointerLockChange);
|
||||
|
||||
// Release remote keys when the operator loses focus (prevents sticky modifiers)
|
||||
window.addEventListener('blur', this._onWindowBlur);
|
||||
document.addEventListener('visibilitychange', this._onVisibilityChange);
|
||||
|
||||
// Make canvas focusable
|
||||
c.tabIndex = 0;
|
||||
c.focus();
|
||||
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop capturing input events
|
||||
*/
|
||||
stop() {
|
||||
if (!this.enabled) return;
|
||||
|
||||
// Notify remote before detaching listeners (tracked keys only)
|
||||
this._releaseAllKeys(false);
|
||||
|
||||
const c = this.canvas;
|
||||
@@ -139,48 +124,16 @@ class RDInput {
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request pointer lock for better mouse capture
|
||||
*/
|
||||
requestPointerLock() {
|
||||
this.canvas.requestPointerLock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Release pointer lock
|
||||
*/
|
||||
exitPointerLock() {
|
||||
if (this.pointerLocked) {
|
||||
document.exitPointerLock();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Mouse Event Handlers ----
|
||||
|
||||
/**
|
||||
* RustDesk mouse event mask encoding:
|
||||
* mask = EVENT_TYPE | (BUTTON << 3)
|
||||
*
|
||||
* Event types (bits 0-2):
|
||||
* 0 = move, 1 = down, 2 = up, 3 = wheel
|
||||
*
|
||||
* Button IDs (bits 3+):
|
||||
* 1 = left, 2 = right, 4 = middle, 8 = back, 16 = forward
|
||||
*
|
||||
* Wheel events (type 3): x/y carry scroll delta, not cursor position.
|
||||
* Match RustDesk Flutter client — normalized to -1 or 1 per axis.
|
||||
*
|
||||
* Examples:
|
||||
* move = 0
|
||||
* left down = 1 | (1 << 3) = 9
|
||||
* left up = 2 | (1 << 3) = 10
|
||||
* right down = 1 | (2 << 3) = 17
|
||||
* right up = 2 | (2 << 3) = 18
|
||||
* middle down = 1 | (4 << 3) = 33
|
||||
* middle up = 2 | (4 << 3) = 34
|
||||
* scroll up = 3, x=0, y=1
|
||||
* scroll down = 3, x=0, y=-1
|
||||
*/
|
||||
static MOUSE_TYPE_DOWN = 1;
|
||||
static MOUSE_TYPE_UP = 2;
|
||||
static MOUSE_TYPE_WHEEL = 3;
|
||||
@@ -189,28 +142,25 @@ class RDInput {
|
||||
static MOUSE_BUTTON_RIGHT = 2;
|
||||
static MOUSE_BUTTON_MIDDLE = 4;
|
||||
|
||||
/** ControlKey enum values from message.proto (lock + modifier sync). */
|
||||
static MOD_ALT = 1;
|
||||
static MOD_CAPS_LOCK = 3;
|
||||
static MOD_CTRL = 4;
|
||||
static MOD_META = 23;
|
||||
static MOD_SHIFT = 29;
|
||||
static MOD_NUM_LOCK = 63;
|
||||
|
||||
/** @param {string} code */
|
||||
static isLetterCode(code) {
|
||||
return /^Key[A-Z]$/.test(code);
|
||||
_encoder() {
|
||||
if (typeof RDKeyboardEncoder !== 'undefined') return RDKeyboardEncoder;
|
||||
if (typeof window !== 'undefined' && window.RDKeyboardEncoder) {
|
||||
return window.RDKeyboardEncoder;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @param {string} code */
|
||||
static isNumpadCode(code) {
|
||||
return /^Numpad/.test(code);
|
||||
_getScancodeLib() {
|
||||
if (typeof RDKeyboardScancode !== 'undefined') return RDKeyboardScancode;
|
||||
if (typeof window !== 'undefined' && window.RDKeyboardScancode) {
|
||||
return window.RDKeyboardScancode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_handleMouseMove(e) {
|
||||
if (!this.enabled) return;
|
||||
|
||||
// Throttle mouse moves to reduce bandwidth and improve responsiveness
|
||||
const now = performance.now();
|
||||
if (now - this._lastMouseSendTime < this._mouseThrottleMs) return;
|
||||
this._lastMouseSendTime = now;
|
||||
@@ -218,13 +168,12 @@ class RDInput {
|
||||
const pos = this._getRemotePosition(e);
|
||||
if (!pos) return;
|
||||
|
||||
// Mouse move = mask 0
|
||||
this.sendMessage({
|
||||
mouseEvent: {
|
||||
mask: 0,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
modifiers: this._getModifiers(e)
|
||||
modifiers: this._getMouseModifiers(e)
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -232,8 +181,6 @@ class RDInput {
|
||||
_handleMouseDown(e) {
|
||||
if (!this.enabled) return;
|
||||
e.preventDefault();
|
||||
|
||||
// Focus canvas for keyboard events
|
||||
this.canvas.focus();
|
||||
|
||||
const pos = this._getRemotePosition(e);
|
||||
@@ -254,7 +201,7 @@ class RDInput {
|
||||
mask: mask,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
modifiers: this._getModifiers(e)
|
||||
modifiers: this._getMouseModifiers(e)
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -282,7 +229,7 @@ class RDInput {
|
||||
mask: mask,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
modifiers: this._getModifiers(e)
|
||||
modifiers: this._getMouseModifiers(e)
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -300,75 +247,19 @@ class RDInput {
|
||||
mask: RDInput.MOUSE_TYPE_WHEEL,
|
||||
x: delta.dx,
|
||||
y: delta.dy,
|
||||
modifiers: this._getModifiers(e)
|
||||
modifiers: this._getMouseModifiers(e)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Keyboard Event Handlers ----
|
||||
|
||||
/**
|
||||
* Map browser key code to RustDesk ControlKey enum value
|
||||
*/
|
||||
static KEY_MAP = {
|
||||
'Escape': 'Escape',
|
||||
'Backspace': 'Backspace',
|
||||
'Tab': 'Tab',
|
||||
'Enter': 'Return',
|
||||
'ShiftLeft': 'Shift',
|
||||
'ShiftRight': 'RShift',
|
||||
'ControlLeft': 'Control',
|
||||
'ControlRight': 'RControl',
|
||||
'AltLeft': 'Alt',
|
||||
'AltRight': 'RAlt',
|
||||
'MetaLeft': 'Meta',
|
||||
'MetaRight': 'RWin',
|
||||
'Pause': 'Pause',
|
||||
'CapsLock': 'CapsLock',
|
||||
'Space': 'Space',
|
||||
'PageUp': 'PageUp',
|
||||
'PageDown': 'PageDown',
|
||||
'End': 'End',
|
||||
'Home': 'Home',
|
||||
'ArrowLeft': 'LeftArrow',
|
||||
'ArrowUp': 'UpArrow',
|
||||
'ArrowRight': 'RightArrow',
|
||||
'ArrowDown': 'DownArrow',
|
||||
'PrintScreen': 'Snapshot',
|
||||
'Insert': 'Insert',
|
||||
'Delete': 'Delete',
|
||||
'ScrollLock': 'Scroll',
|
||||
'NumLock': 'NumLock',
|
||||
'F1': 'F1', 'F2': 'F2', 'F3': 'F3', 'F4': 'F4',
|
||||
'F5': 'F5', 'F6': 'F6', 'F7': 'F7', 'F8': 'F8',
|
||||
'F9': 'F9', 'F10': 'F10', 'F11': 'F11', 'F12': 'F12',
|
||||
'Numpad0': 'Numpad0', 'Numpad1': 'Numpad1', 'Numpad2': 'Numpad2',
|
||||
'Numpad3': 'Numpad3', 'Numpad4': 'Numpad4', 'Numpad5': 'Numpad5',
|
||||
'Numpad6': 'Numpad6', 'Numpad7': 'Numpad7', 'Numpad8': 'Numpad8',
|
||||
'Numpad9': 'Numpad9',
|
||||
'NumpadMultiply': 'Multiply',
|
||||
'NumpadAdd': 'Add',
|
||||
'NumpadSubtract': 'Subtract',
|
||||
'NumpadDecimal': 'Decimal',
|
||||
'NumpadDivide': 'Divide',
|
||||
'NumpadEnter': 'NumpadEnter',
|
||||
'ContextMenu': 'Apps',
|
||||
'AudioVolumeMute': 'VolumeMute',
|
||||
'AudioVolumeDown': 'VolumeDown',
|
||||
'AudioVolumeUp': 'VolumeUp'
|
||||
};
|
||||
|
||||
_handleKeyDown(e) {
|
||||
if (!this.enabled) return;
|
||||
// Don't capture if focus is on an input element
|
||||
if (this._isInputFocused()) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const keyCode = e.code;
|
||||
// RustDesk legacy mode: keydown => down, key repeat => press (click).
|
||||
// Repeat is reported either via e.repeat or our own pressedKeys guard.
|
||||
const isRepeat = e.repeat || this.pressedKeys.has(keyCode);
|
||||
this.pressedKeys.set(keyCode, { key: e.key });
|
||||
|
||||
@@ -403,15 +294,11 @@ class RDInput {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send keyup for every locally tracked key.
|
||||
* @param {boolean} [forceAllModifiers=false] - release all modifiers (toolbar recovery only)
|
||||
*/
|
||||
_releaseAllKeys(forceAllModifiers) {
|
||||
const codes = [...this.pressedKeys.keys()];
|
||||
for (const code of codes) {
|
||||
const meta = this.pressedKeys.get(code);
|
||||
this._sendKeyForCode(code, meta?.key || '', false, false, []);
|
||||
this._sendKeyForCode(code, meta?.key || '', false, false, null);
|
||||
}
|
||||
this.pressedKeys.clear();
|
||||
|
||||
@@ -420,188 +307,51 @@ class RDInput {
|
||||
}
|
||||
}
|
||||
|
||||
/** Release Shift/Ctrl/Alt/Meta on the remote side even if keyup was missed. */
|
||||
_releaseAllModifiers() {
|
||||
const mode = this._resolveKeyboardMode();
|
||||
const modCodes = this._getScancodeLib()?.MODIFIER_CODES ||
|
||||
['ShiftLeft', 'ShiftRight', 'ControlLeft', 'ControlRight',
|
||||
'AltLeft', 'AltRight', 'MetaLeft', 'MetaRight'];
|
||||
|
||||
for (const code of modCodes) {
|
||||
if (mode === 'Map') {
|
||||
const scLib = this._getScancodeLib();
|
||||
const sc = scLib?.codeToScancode(code, this.peerPlatform);
|
||||
if (sc != null) {
|
||||
this.sendMessage({
|
||||
keyEvent: {
|
||||
chr: sc,
|
||||
down: false,
|
||||
press: false,
|
||||
modifiers: [],
|
||||
mode: 'Map'
|
||||
}
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const controlKey = RDInput.KEY_MAP[code];
|
||||
if (controlKey) {
|
||||
this.sendMessage({
|
||||
keyEvent: {
|
||||
controlKey: controlKey,
|
||||
down: false,
|
||||
press: false,
|
||||
modifiers: [],
|
||||
mode: 'Legacy'
|
||||
}
|
||||
});
|
||||
}
|
||||
const enc = this._encoder();
|
||||
if (!enc) return;
|
||||
const events = enc.buildModifierReleaseEvents(this._getScancodeLib());
|
||||
for (const keyEvent of events) {
|
||||
this.sendMessage({ keyEvent });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {typeof RDKeyboardScancode|null}
|
||||
*/
|
||||
_getScancodeLib() {
|
||||
if (typeof RDKeyboardScancode !== 'undefined') return RDKeyboardScancode;
|
||||
if (typeof window !== 'undefined' && window.RDKeyboardScancode) {
|
||||
return window.RDKeyboardScancode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map (scancode) is only used for letter keys — digits and punctuation use
|
||||
* Legacy chr so Shift+7, AltGr, PL layout symbols resolve correctly.
|
||||
* @param {string} code
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_shouldUseMapScancode(code) {
|
||||
return this._resolveKeyboardMode() === 'Map' && RDInput.isLetterCode(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy chr already encodes Shift/Caps; strip them unless Ctrl/Alt/Meta hotkey.
|
||||
* @param {number[]} mods
|
||||
* @param {KeyboardEvent} e
|
||||
* @returns {number[]}
|
||||
*/
|
||||
_stripResolvedCharModifiers(mods, e) {
|
||||
if (e.ctrlKey || e.altKey || e.metaKey) return mods;
|
||||
return mods.filter((m) =>
|
||||
m !== RDInput.MOD_SHIFT && m !== RDInput.MOD_CAPS_LOCK
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {'Legacy'|'Map'}
|
||||
*/
|
||||
_resolveKeyboardMode() {
|
||||
if (this.keyboardMode === 'Map') return 'Map';
|
||||
if (this.keyboardMode === 'Legacy') return 'Legacy';
|
||||
// Auto: Map for Windows peers (Hyper-V / VM console scancode path)
|
||||
const platform = (this.peerPlatform || '').toLowerCase();
|
||||
if (platform === 'windows') return 'Map';
|
||||
return 'Legacy';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and dispatch a single RustDesk key event from a KeyboardEvent.
|
||||
* @param {KeyboardEvent} e
|
||||
* @param {boolean} down - key is pressed down
|
||||
* @param {boolean} press - key click (down+up), used for repeats
|
||||
*/
|
||||
_sendKey(e, down, press) {
|
||||
const isChar = !RDInput.KEY_MAP[e.code];
|
||||
let mods = this._getKeyModifiers(e, isChar);
|
||||
let key = e.key;
|
||||
const useMap = isChar && this._shouldUseMapScancode(e.code);
|
||||
|
||||
if (isChar && !useMap) {
|
||||
if (RDInput.isLetterCode(e.code)) {
|
||||
key = this._resolveLegacyLetterCase(e);
|
||||
}
|
||||
mods = this._stripResolvedCharModifiers(mods, e);
|
||||
}
|
||||
|
||||
this._sendKeyForCode(e.code, key, down, press, mods, useMap);
|
||||
this._sendKeyForCode(e.code, e.key, down, press, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and dispatch a key event from code/key strings (used for synthetic keyup).
|
||||
* @param {string} code
|
||||
* @param {string} key
|
||||
* @param {boolean} down
|
||||
* @param {boolean} press
|
||||
* @param {number[]} modifiers
|
||||
* @param {boolean} [useMap=false]
|
||||
* @param {KeyboardEvent|null} e
|
||||
*/
|
||||
_sendKeyForCode(code, key, down, press, modifiers, useMap) {
|
||||
const controlKey = RDInput.KEY_MAP[code];
|
||||
_sendKeyForCode(code, key, down, press, e) {
|
||||
const enc = this._encoder();
|
||||
if (!enc) return;
|
||||
|
||||
// Navigation, modifiers, and lock keys always use Legacy controlKey —
|
||||
// Map scancode path breaks reliable Shift/Caps/NumLock handling on Windows.
|
||||
if (controlKey) {
|
||||
this.sendMessage({
|
||||
keyEvent: {
|
||||
controlKey: controlKey,
|
||||
down: down,
|
||||
press: press,
|
||||
modifiers: modifiers,
|
||||
mode: 'Legacy'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (useMap) {
|
||||
const scLib = this._getScancodeLib();
|
||||
const sc = scLib?.codeToScancode(code, this.peerPlatform);
|
||||
if (sc != null) {
|
||||
this.sendMessage({
|
||||
keyEvent: {
|
||||
chr: sc,
|
||||
down: down,
|
||||
press: press,
|
||||
modifiers: modifiers,
|
||||
mode: 'Map'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Character key — forward the produced character so layout-specific and
|
||||
// accented glyphs (ą, ę, ü, etc.) resolve correctly on the remote side.
|
||||
if (!key || key.length === 0 || key === 'Unidentified' || key === 'Dead') return;
|
||||
|
||||
const codePoint = key.codePointAt(0);
|
||||
if (codePoint === undefined) return;
|
||||
|
||||
this.sendMessage({
|
||||
keyEvent: {
|
||||
chr: codePoint,
|
||||
down: down,
|
||||
press: press,
|
||||
modifiers: modifiers,
|
||||
mode: 'Legacy'
|
||||
}
|
||||
const keyEvent = enc.encodeKeyEvent({
|
||||
code,
|
||||
key,
|
||||
down,
|
||||
press,
|
||||
e,
|
||||
keyboardMode: this.keyboardMode,
|
||||
peerPlatform: this.peerPlatform,
|
||||
pressedCodes: this.pressedKeys.keys(),
|
||||
scancodeLib: this._getScancodeLib(),
|
||||
});
|
||||
|
||||
if (keyEvent) {
|
||||
this.sendMessage({ keyEvent });
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
/**
|
||||
* Convert a browser WheelEvent into RustDesk scroll deltas.
|
||||
* Peers interpret x/y as scroll amount when mask type is wheel (3).
|
||||
* @param {WheelEvent} e
|
||||
* @returns {{ dx: number, dy: number }|null}
|
||||
*/
|
||||
_normalizeWheelDelta(e) {
|
||||
let rawDx = e.deltaX;
|
||||
let rawDy = e.deltaY;
|
||||
|
||||
// Shift+wheel often maps vertical motion to horizontal scrolling.
|
||||
if (e.shiftKey && rawDy !== 0 && rawDx === 0) {
|
||||
rawDx = rawDy;
|
||||
rawDy = 0;
|
||||
@@ -625,11 +375,6 @@ class RDInput {
|
||||
this.pointerLocked = document.pointerLockElement === this.canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remote coordinates from mouse event
|
||||
* @param {MouseEvent} e
|
||||
* @returns {{ x: number, y: number }|null}
|
||||
*/
|
||||
_getRemotePosition(e) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const canvasX = e.clientX - rect.left;
|
||||
@@ -637,7 +382,6 @@ class RDInput {
|
||||
|
||||
const pos = this.renderer.canvasToRemote(canvasX, canvasY);
|
||||
|
||||
// Clamp to remote display bounds
|
||||
if (pos.x < 0 || pos.y < 0 ||
|
||||
pos.x > this.renderer.remoteWidth ||
|
||||
pos.y > this.renderer.remoteHeight) {
|
||||
@@ -647,99 +391,26 @@ class RDInput {
|
||||
return pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mouse modifier flags
|
||||
* @param {MouseEvent} e
|
||||
* @returns {number[]}
|
||||
*/
|
||||
_getModifiers(e) {
|
||||
_getMouseModifiers(e) {
|
||||
const enc = this._encoder();
|
||||
if (!enc) return [];
|
||||
const mods = [];
|
||||
if (e.shiftKey) mods.push(RDInput.MOD_SHIFT);
|
||||
if (e.ctrlKey) mods.push(RDInput.MOD_CTRL);
|
||||
if (e.altKey) mods.push(RDInput.MOD_ALT);
|
||||
if (e.metaKey) mods.push(RDInput.MOD_META);
|
||||
if (e.shiftKey) mods.push(enc.MOD_SHIFT);
|
||||
if (e.ctrlKey) mods.push(enc.MOD_CTRL);
|
||||
if (e.altKey) mods.push(enc.MOD_ALT);
|
||||
if (e.metaKey) mods.push(enc.MOD_META);
|
||||
return mods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply Shift XOR CapsLock case for Legacy ASCII letters (matches RustDesk client).
|
||||
* @param {KeyboardEvent} e
|
||||
* @returns {string}
|
||||
*/
|
||||
_resolveLegacyLetterCase(e) {
|
||||
const key = e.key;
|
||||
if (!key || key.length !== 1 || !RDInput.isLetterCode(e.code)) return key;
|
||||
const cp = key.codePointAt(0);
|
||||
if (cp === undefined || cp < 0x41 || cp > 0x7A) return key;
|
||||
if (cp > 0x5A && cp < 0x61) return key;
|
||||
|
||||
const caps = typeof e.getModifierState === 'function' &&
|
||||
e.getModifierState('CapsLock');
|
||||
const upper = e.shiftKey !== caps;
|
||||
if (upper) {
|
||||
return String.fromCodePoint(cp <= 0x5A ? cp : cp - 32);
|
||||
}
|
||||
return String.fromCodePoint(cp >= 0x61 ? cp : cp + 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get keyboard modifier flags
|
||||
* @param {KeyboardEvent} e
|
||||
* @param {boolean} [isChar=false] - true for printable-character events
|
||||
* @returns {number[]}
|
||||
*/
|
||||
_getKeyModifiers(e, isChar) {
|
||||
// Values must match ControlKey enum in message.proto:
|
||||
// Alt=1, CapsLock=3, Control=4, Meta=23, Shift=29, NumLock=63
|
||||
const mods = [];
|
||||
if (e.shiftKey) mods.push(RDInput.MOD_SHIFT);
|
||||
|
||||
// AltGr (Right Alt) is reported on Windows as a phantom Ctrl+Alt
|
||||
// combination. When AltGr produced a printable character the produced
|
||||
// glyph already encodes it, so forwarding Ctrl+Alt makes the remote
|
||||
// side treat it as a hotkey instead of typing the character.
|
||||
const altGraph = typeof e.getModifierState === 'function' &&
|
||||
e.getModifierState('AltGraph');
|
||||
if (isChar && altGraph) {
|
||||
return mods;
|
||||
}
|
||||
|
||||
if (e.ctrlKey) mods.push(RDInput.MOD_CTRL);
|
||||
if (e.altKey) mods.push(RDInput.MOD_ALT);
|
||||
if (e.metaKey) mods.push(RDInput.MOD_META);
|
||||
|
||||
// RustDesk LockModesHandler: sync remote Caps/Num lock from modifier flags
|
||||
// on letter / numpad events (required for Map scancode path and chr fallback).
|
||||
if (typeof e.getModifierState === 'function') {
|
||||
if (isChar && RDInput.isLetterCode(e.code) &&
|
||||
e.getModifierState('CapsLock')) {
|
||||
mods.push(RDInput.MOD_CAPS_LOCK);
|
||||
}
|
||||
if (RDInput.isNumpadCode(e.code) && e.getModifierState('NumLock')) {
|
||||
mods.push(RDInput.MOD_NUM_LOCK);
|
||||
}
|
||||
}
|
||||
|
||||
return mods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a visible input-like element has focus (not the remote canvas)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isInputFocused() {
|
||||
const el = document.activeElement;
|
||||
if (!el) return false;
|
||||
const tag = el.tagName?.toLowerCase();
|
||||
if (tag !== 'input' && tag !== 'textarea' && tag !== 'select') return false;
|
||||
// Ignore hidden inputs (e.g. password field after login)
|
||||
if (el.type === 'hidden' || el.offsetParent === null) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close and release all resources
|
||||
*/
|
||||
close() {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* RustDesk-compatible KeyEvent encoder (mirrors upstream keyboard.rs client path).
|
||||
*
|
||||
* Legacy: lowercase chr + legacy_modifiers + lock_modes on chr/control events.
|
||||
* Map: scancode in chr; modifiers on chr events are lock_modes only (Caps/Num).
|
||||
* Modifier / navigation keys always use Legacy controlKey.
|
||||
*/
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* global RDKeyboardScancode */
|
||||
|
||||
const RDKeyboardEncoder = {
|
||||
MOD_ALT: 1,
|
||||
MOD_CAPS_LOCK: 3,
|
||||
MOD_CTRL: 4,
|
||||
MOD_META: 23,
|
||||
MOD_SHIFT: 29,
|
||||
MOD_NUM_LOCK: 63,
|
||||
|
||||
/** Upstream legacy_keyboard_mode: no wire event for lock keys. */
|
||||
SILENT_LOCK_CODES: ['CapsLock', 'NumLock', 'ScrollLock'],
|
||||
|
||||
/** DOM code → RustDesk ControlKey name (Legacy path). */
|
||||
KEY_MAP: {
|
||||
Escape: 'Escape',
|
||||
Backspace: 'Backspace',
|
||||
Tab: 'Tab',
|
||||
Enter: 'Return',
|
||||
ShiftLeft: 'Shift',
|
||||
ShiftRight: 'RShift',
|
||||
ControlLeft: 'Control',
|
||||
ControlRight: 'RControl',
|
||||
AltLeft: 'Alt',
|
||||
AltRight: 'RAlt',
|
||||
MetaLeft: 'Meta',
|
||||
MetaRight: 'RWin',
|
||||
Pause: 'Pause',
|
||||
CapsLock: 'CapsLock',
|
||||
Space: 'Space',
|
||||
PageUp: 'PageUp',
|
||||
PageDown: 'PageDown',
|
||||
End: 'End',
|
||||
Home: 'Home',
|
||||
ArrowLeft: 'LeftArrow',
|
||||
ArrowUp: 'UpArrow',
|
||||
ArrowRight: 'RightArrow',
|
||||
ArrowDown: 'DownArrow',
|
||||
PrintScreen: 'Snapshot',
|
||||
Insert: 'Insert',
|
||||
Delete: 'Delete',
|
||||
ScrollLock: 'Scroll',
|
||||
NumLock: 'NumLock',
|
||||
F1: 'F1', F2: 'F2', F3: 'F3', F4: 'F4',
|
||||
F5: 'F5', F6: 'F6', F7: 'F7', F8: 'F8',
|
||||
F9: 'F9', F10: 'F10', F11: 'F11', F12: 'F12',
|
||||
Numpad0: 'Numpad0', Numpad1: 'Numpad1', Numpad2: 'Numpad2',
|
||||
Numpad3: 'Numpad3', Numpad4: 'Numpad4', Numpad5: 'Numpad5',
|
||||
Numpad6: 'Numpad6', Numpad7: 'Numpad7', Numpad8: 'Numpad8',
|
||||
Numpad9: 'Numpad9',
|
||||
NumpadMultiply: 'Multiply',
|
||||
NumpadAdd: 'Add',
|
||||
NumpadSubtract: 'Subtract',
|
||||
NumpadDecimal: 'Decimal',
|
||||
NumpadDivide: 'Divide',
|
||||
NumpadEnter: 'NumpadEnter',
|
||||
ContextMenu: 'Apps',
|
||||
AudioVolumeMute: 'VolumeMute',
|
||||
AudioVolumeDown: 'VolumeDown',
|
||||
AudioVolumeUp: 'VolumeUp',
|
||||
},
|
||||
|
||||
/** Physical key → Legacy chr (lowercase), upstream legacy_keyboard_mode. */
|
||||
LEGACY_CHAR_MAP: {
|
||||
Digit1: '1', Digit2: '2', Digit3: '3', Digit4: '4', Digit5: '5',
|
||||
Digit6: '6', Digit7: '7', Digit8: '8', Digit9: '9', Digit0: '0',
|
||||
KeyA: 'a', KeyB: 'b', KeyC: 'c', KeyD: 'd', KeyE: 'e', KeyF: 'f',
|
||||
KeyG: 'g', KeyH: 'h', KeyI: 'i', KeyJ: 'j', KeyK: 'k', KeyL: 'l',
|
||||
KeyM: 'm', KeyN: 'n', KeyO: 'o', KeyP: 'p', KeyQ: 'q', KeyR: 'r',
|
||||
KeyS: 's', KeyT: 't', KeyU: 'u', KeyV: 'v', KeyW: 'w', KeyX: 'x',
|
||||
KeyY: 'y', KeyZ: 'z',
|
||||
Comma: ',', Period: '.', Semicolon: ';', Quote: '\'',
|
||||
BracketLeft: '[', BracketRight: ']', Slash: '/', Backslash: '\\',
|
||||
Minus: '-', Equal: '=', Backquote: '`',
|
||||
},
|
||||
|
||||
isLetterCode(code) {
|
||||
return /^Key[A-Z]$/.test(code);
|
||||
},
|
||||
|
||||
isNumpadCode(code) {
|
||||
return /^Numpad/.test(code);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolve UI keyboard mode to wire mode (Auto → Map, matching native default).
|
||||
* @param {'Legacy'|'Map'|'Auto'} keyboardMode
|
||||
* @returns {'Legacy'|'Map'}
|
||||
*/
|
||||
resolveWireMode(keyboardMode) {
|
||||
if (keyboardMode === 'Legacy') return 'Legacy';
|
||||
return 'Map';
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Iterable<string>} pressedCodes
|
||||
* @returns {{ shift: boolean, ctrl: boolean, alt: boolean, meta: boolean }}
|
||||
*/
|
||||
modifierStateFromPressed(pressedCodes) {
|
||||
const set = pressedCodes instanceof Set ? pressedCodes : new Set(pressedCodes);
|
||||
return {
|
||||
shift: set.has('ShiftLeft') || set.has('ShiftRight'),
|
||||
ctrl: set.has('ControlLeft') || set.has('ControlRight'),
|
||||
alt: set.has('AltLeft') || set.has('AltRight'),
|
||||
meta: set.has('MetaLeft') || set.has('MetaRight'),
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Port client::legacy_modifiers — exclude self-referential control keys.
|
||||
* @param {string} code
|
||||
* @param {{ shift: boolean, ctrl: boolean, alt: boolean, meta: boolean }} modState
|
||||
* @returns {number[]}
|
||||
*/
|
||||
legacyModifiers(code, modState) {
|
||||
const mods = [];
|
||||
if (modState.alt && code !== 'AltLeft' && code !== 'AltRight') {
|
||||
mods.push(this.MOD_ALT);
|
||||
}
|
||||
if (modState.shift && code !== 'ShiftLeft' && code !== 'ShiftRight') {
|
||||
mods.push(this.MOD_SHIFT);
|
||||
}
|
||||
if (modState.ctrl && code !== 'ControlLeft' && code !== 'ControlRight') {
|
||||
mods.push(this.MOD_CTRL);
|
||||
}
|
||||
if (modState.meta && code !== 'MetaLeft' && code !== 'MetaRight') {
|
||||
mods.push(this.MOD_META);
|
||||
}
|
||||
return mods;
|
||||
},
|
||||
|
||||
/**
|
||||
* Port add_lock_modes_modifiers (Caps/Num only).
|
||||
* @param {string} code
|
||||
* @param {KeyboardEvent|null} e
|
||||
* @returns {number[]}
|
||||
*/
|
||||
lockModeModifiers(code, e) {
|
||||
const mods = [];
|
||||
if (!e || typeof e.getModifierState !== 'function') return mods;
|
||||
if (this.isLetterCode(code) && e.getModifierState('CapsLock')) {
|
||||
mods.push(this.MOD_CAPS_LOCK);
|
||||
}
|
||||
if (this.isNumpadCode(code) && e.getModifierState('NumLock')) {
|
||||
mods.push(this.MOD_NUM_LOCK);
|
||||
}
|
||||
return mods;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @param {string} key
|
||||
* @returns {number|null}
|
||||
*/
|
||||
legacyCharCodePoint(code, key) {
|
||||
const mapped = this.LEGACY_CHAR_MAP[code];
|
||||
if (mapped) return mapped.codePointAt(0);
|
||||
if (!key || key.length === 0 || key === 'Unidentified' || key === 'Dead') {
|
||||
return null;
|
||||
}
|
||||
return key.codePointAt(0) ?? null;
|
||||
},
|
||||
|
||||
/**
|
||||
* AltGr on Windows appears as Ctrl+Alt; for printable Legacy chr omit Ctrl+Alt.
|
||||
* @param {string} code
|
||||
* @param {KeyboardEvent|null} e
|
||||
* @param {number[]} mods
|
||||
* @returns {number[]}
|
||||
*/
|
||||
applyAltGrLegacyFilter(code, e, mods) {
|
||||
if (this.KEY_MAP[code]) return mods;
|
||||
const altGraph = e && typeof e.getModifierState === 'function' &&
|
||||
e.getModifierState('AltGraph');
|
||||
if (!altGraph) return mods;
|
||||
return mods.filter((m) => m !== this.MOD_CTRL && m !== this.MOD_ALT);
|
||||
},
|
||||
|
||||
_getScancodeLib(scancodeLib) {
|
||||
if (scancodeLib) return scancodeLib;
|
||||
if (typeof RDKeyboardScancode !== 'undefined') return RDKeyboardScancode;
|
||||
if (typeof window !== 'undefined' && window.RDKeyboardScancode) {
|
||||
return window.RDKeyboardScancode;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Build zero or one RustDesk keyEvent payload.
|
||||
* @param {object} opts
|
||||
* @param {string} opts.code
|
||||
* @param {string} opts.key
|
||||
* @param {boolean} opts.down
|
||||
* @param {boolean} opts.press
|
||||
* @param {KeyboardEvent|null} [opts.e]
|
||||
* @param {'Legacy'|'Map'|'Auto'} opts.keyboardMode
|
||||
* @param {string} [opts.peerPlatform]
|
||||
* @param {Set<string>|Iterable<string>} opts.pressedCodes
|
||||
* @param {object|null} [opts.scancodeLib]
|
||||
* @returns {object|null}
|
||||
*/
|
||||
encodeKeyEvent(opts) {
|
||||
const {
|
||||
code,
|
||||
key,
|
||||
down,
|
||||
press,
|
||||
e = null,
|
||||
keyboardMode,
|
||||
peerPlatform = '',
|
||||
pressedCodes,
|
||||
scancodeLib = null,
|
||||
} = opts;
|
||||
|
||||
const wireMode = this.resolveWireMode(keyboardMode);
|
||||
const modState = this.modifierStateFromPressed(pressedCodes);
|
||||
const controlKey = this.KEY_MAP[code];
|
||||
|
||||
if (this.SILENT_LOCK_CODES.includes(code)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (controlKey) {
|
||||
let modifiers = this.legacyModifiers(code, modState);
|
||||
modifiers = modifiers.concat(this.lockModeModifiers(code, e));
|
||||
return {
|
||||
controlKey,
|
||||
down,
|
||||
press,
|
||||
modifiers,
|
||||
mode: 'Legacy',
|
||||
};
|
||||
}
|
||||
|
||||
const scLib = this._getScancodeLib(scancodeLib);
|
||||
|
||||
if (wireMode === 'Map') {
|
||||
const sc = scLib?.codeToScancode(code, peerPlatform);
|
||||
if (sc != null) {
|
||||
return {
|
||||
chr: sc,
|
||||
down,
|
||||
press,
|
||||
modifiers: this.lockModeModifiers(code, e),
|
||||
mode: 'Map',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const chr = this.legacyCharCodePoint(code, key);
|
||||
if (chr == null) return null;
|
||||
|
||||
let modifiers = this.legacyModifiers(code, modState);
|
||||
modifiers = modifiers.concat(this.lockModeModifiers(code, e));
|
||||
modifiers = this.applyAltGrLegacyFilter(code, e, modifiers);
|
||||
|
||||
return {
|
||||
chr,
|
||||
down,
|
||||
press,
|
||||
modifiers,
|
||||
mode: 'Legacy',
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Build modifier keyup events for toolbar recovery (always Legacy controlKey).
|
||||
* @param {object|null} scancodeLib
|
||||
* @returns {object[]}
|
||||
*/
|
||||
buildModifierReleaseEvents(scancodeLib) {
|
||||
const lib = this._getScancodeLib(scancodeLib);
|
||||
const codes = lib?.MODIFIER_CODES || [
|
||||
'ShiftLeft', 'ShiftRight', 'ControlLeft', 'ControlRight',
|
||||
'AltLeft', 'AltRight', 'MetaLeft', 'MetaRight',
|
||||
];
|
||||
const events = [];
|
||||
for (const code of codes) {
|
||||
const controlKey = this.KEY_MAP[code];
|
||||
if (controlKey) {
|
||||
events.push({
|
||||
controlKey,
|
||||
down: false,
|
||||
press: false,
|
||||
modifiers: [],
|
||||
mode: 'Legacy',
|
||||
});
|
||||
}
|
||||
}
|
||||
return events;
|
||||
},
|
||||
};
|
||||
|
||||
window.RDKeyboardEncoder = RDKeyboardEncoder;
|
||||
@@ -65,6 +65,7 @@ function makeInputHarness(extraGlobals) {
|
||||
|
||||
const sandbox = loadBrowserScripts([
|
||||
'public/js/rdclient/keyboard-scancode.js',
|
||||
'public/js/rdclient/keyboard-encoder.js',
|
||||
'public/js/rdclient/input.js',
|
||||
], {
|
||||
document,
|
||||
@@ -92,6 +93,20 @@ function makeInputHarness(extraGlobals) {
|
||||
return { sandbox, document, win, makeInput };
|
||||
}
|
||||
|
||||
function keyEvt(base) {
|
||||
return {
|
||||
repeat: false,
|
||||
preventDefault() {},
|
||||
stopPropagation() {},
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
getModifierState() { return false; },
|
||||
...base,
|
||||
};
|
||||
}
|
||||
|
||||
describe('RDKeyboardScancode', () => {
|
||||
let RDKeyboardScancode;
|
||||
|
||||
@@ -106,14 +121,91 @@ describe('RDKeyboardScancode', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('RDInput keyboard release sync', () => {
|
||||
let makeInput;
|
||||
let RDInput;
|
||||
describe('RDKeyboardEncoder parity', () => {
|
||||
let RDKeyboardEncoder;
|
||||
|
||||
beforeAll(() => {
|
||||
const harness = makeInputHarness({});
|
||||
makeInput = harness.makeInput;
|
||||
RDInput = harness.sandbox.window.RDInput;
|
||||
const sandbox = loadBrowserScripts([
|
||||
'public/js/rdclient/keyboard-scancode.js',
|
||||
'public/js/rdclient/keyboard-encoder.js',
|
||||
], {});
|
||||
RDKeyboardEncoder = sandbox.RDKeyboardEncoder;
|
||||
});
|
||||
|
||||
it('Auto resolves to Map wire mode', () => {
|
||||
expect(RDKeyboardEncoder.resolveWireMode('Auto')).toBe('Map');
|
||||
});
|
||||
|
||||
it('legacyModifiers excludes self on ShiftLeft', () => {
|
||||
const mods = RDKeyboardEncoder.legacyModifiers('ShiftLeft', {
|
||||
shift: true, ctrl: false, alt: false, meta: false,
|
||||
});
|
||||
expect(mods).not.toContain(29);
|
||||
});
|
||||
|
||||
it('Legacy Shift+A → chr 97 + Shift modifier', () => {
|
||||
const evt = RDKeyboardEncoder.encodeKeyEvent({
|
||||
code: 'KeyA',
|
||||
key: 'A',
|
||||
down: true,
|
||||
press: false,
|
||||
keyboardMode: 'Legacy',
|
||||
pressedCodes: new Set(['ShiftLeft']),
|
||||
});
|
||||
expect(evt.mode).toBe('Legacy');
|
||||
expect(evt.chr).toBe(97);
|
||||
expect(evt.modifiers).toContain(29);
|
||||
});
|
||||
|
||||
it('Map Shift+7 → scancode 0x08, no Shift in modifiers', () => {
|
||||
const evt = RDKeyboardEncoder.encodeKeyEvent({
|
||||
code: 'Digit7',
|
||||
key: '&',
|
||||
down: true,
|
||||
press: false,
|
||||
keyboardMode: 'Map',
|
||||
peerPlatform: 'Windows',
|
||||
pressedCodes: new Set(['ShiftLeft']),
|
||||
scancodeLib: RDKeyboardEncoder._getScancodeLib(),
|
||||
});
|
||||
expect(evt.mode).toBe('Map');
|
||||
expect(evt.chr).toBe(0x08);
|
||||
expect(evt.modifiers).not.toContain(29);
|
||||
});
|
||||
|
||||
it('CapsLock key alone produces no event', () => {
|
||||
const evt = RDKeyboardEncoder.encodeKeyEvent({
|
||||
code: 'CapsLock',
|
||||
key: 'CapsLock',
|
||||
down: true,
|
||||
press: false,
|
||||
keyboardMode: 'Legacy',
|
||||
pressedCodes: new Set(),
|
||||
});
|
||||
expect(evt).toBeNull();
|
||||
});
|
||||
|
||||
it('Legacy Caps+A → chr 97 + CapsLock modifier', () => {
|
||||
const evt = RDKeyboardEncoder.encodeKeyEvent({
|
||||
code: 'KeyA',
|
||||
key: 'a',
|
||||
down: true,
|
||||
press: false,
|
||||
e: { getModifierState(s) { return s === 'CapsLock'; } },
|
||||
keyboardMode: 'Legacy',
|
||||
pressedCodes: new Set(),
|
||||
});
|
||||
expect(evt.chr).toBe(97);
|
||||
expect(evt.modifiers).toContain(3);
|
||||
expect(evt.modifiers).not.toContain(29);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RDInput keyboard release sync', () => {
|
||||
let makeInput;
|
||||
|
||||
beforeAll(() => {
|
||||
({ makeInput } = makeInputHarness({}));
|
||||
});
|
||||
|
||||
it('sends keyup for held keys when stop() is called', () => {
|
||||
@@ -121,28 +213,16 @@ describe('RDInput keyboard release sync', () => {
|
||||
const input = makeInput((msg) => sent.push(msg));
|
||||
input.start();
|
||||
|
||||
input._handleKeyDown({
|
||||
input._handleKeyDown(keyEvt({
|
||||
code: 'ShiftLeft',
|
||||
key: 'Shift',
|
||||
repeat: false,
|
||||
preventDefault() {},
|
||||
stopPropagation() {},
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: true,
|
||||
});
|
||||
input._handleKeyDown({
|
||||
}));
|
||||
input._handleKeyDown(keyEvt({
|
||||
code: 'KeyA',
|
||||
key: 'a',
|
||||
repeat: false,
|
||||
preventDefault() {},
|
||||
stopPropagation() {},
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: true,
|
||||
});
|
||||
}));
|
||||
|
||||
sent.length = 0;
|
||||
input.stop();
|
||||
@@ -157,17 +237,11 @@ describe('RDInput keyboard release sync', () => {
|
||||
const harness = makeInputHarness({});
|
||||
const input = harness.makeInput((msg) => sent.push(msg));
|
||||
input.start();
|
||||
input._handleKeyDown({
|
||||
input._handleKeyDown(keyEvt({
|
||||
code: 'ControlLeft',
|
||||
key: 'Control',
|
||||
repeat: false,
|
||||
preventDefault() {},
|
||||
stopPropagation() {},
|
||||
ctrlKey: true,
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
});
|
||||
}));
|
||||
|
||||
sent.length = 0;
|
||||
harness.win._dispatch('blur', new Event('blur'));
|
||||
@@ -205,13 +279,102 @@ describe('RDInput keyboard release sync', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('RDInput KeyboardMode.Map', () => {
|
||||
describe('RDInput keyboard parity (RustDesk contract)', () => {
|
||||
let makeInput;
|
||||
|
||||
beforeAll(() => {
|
||||
({ makeInput } = makeInputHarness({}));
|
||||
});
|
||||
|
||||
it('ShiftLeft down (real DOM shiftKey) has no Shift in modifiers', () => {
|
||||
const sent = [];
|
||||
const input = makeInput((msg) => sent.push(msg));
|
||||
input.setPeerPlatform('Windows');
|
||||
input.setKeyboardMode('Auto');
|
||||
input.start();
|
||||
|
||||
input._handleKeyDown(keyEvt({
|
||||
code: 'ShiftLeft',
|
||||
key: 'Shift',
|
||||
shiftKey: true,
|
||||
}));
|
||||
|
||||
expect(sent[0].keyEvent.mode).toBe('Legacy');
|
||||
expect(sent[0].keyEvent.controlKey).toBe('Shift');
|
||||
expect(sent[0].keyEvent.modifiers).not.toContain(29);
|
||||
});
|
||||
|
||||
it('Auto/Windows Shift+A → Map scancode without Shift modifier', () => {
|
||||
const sent = [];
|
||||
const input = makeInput((msg) => sent.push(msg));
|
||||
input.setPeerPlatform('Windows');
|
||||
input.setKeyboardMode('Auto');
|
||||
input.start();
|
||||
|
||||
input._handleKeyDown(keyEvt({
|
||||
code: 'ShiftLeft',
|
||||
key: 'Shift',
|
||||
shiftKey: true,
|
||||
}));
|
||||
input._handleKeyDown(keyEvt({
|
||||
code: 'KeyA',
|
||||
key: 'A',
|
||||
shiftKey: true,
|
||||
}));
|
||||
|
||||
const letter = sent.find((m) => m.keyEvent && m.keyEvent.chr === 0x1E);
|
||||
expect(letter).toBeDefined();
|
||||
expect(letter.keyEvent.mode).toBe('Map');
|
||||
expect(letter.keyEvent.modifiers).not.toContain(29);
|
||||
});
|
||||
|
||||
it('Legacy Shift+A → lowercase chr + Shift modifier', () => {
|
||||
const sent = [];
|
||||
const input = makeInput((msg) => sent.push(msg));
|
||||
input.setKeyboardMode('Legacy');
|
||||
input.start();
|
||||
|
||||
input._handleKeyDown(keyEvt({
|
||||
code: 'ShiftLeft',
|
||||
key: 'Shift',
|
||||
shiftKey: true,
|
||||
}));
|
||||
input._handleKeyDown(keyEvt({
|
||||
code: 'KeyA',
|
||||
key: 'A',
|
||||
shiftKey: true,
|
||||
}));
|
||||
|
||||
const letter = sent.find((m) => m.keyEvent && m.keyEvent.chr === 97);
|
||||
expect(letter).toBeDefined();
|
||||
expect(letter.keyEvent.mode).toBe('Legacy');
|
||||
expect(letter.keyEvent.modifiers).toContain(29);
|
||||
});
|
||||
|
||||
it('Auto/Map Shift+7 uses Map scancode not Legacy chr', () => {
|
||||
const sent = [];
|
||||
const input = makeInput((msg) => sent.push(msg));
|
||||
input.setPeerPlatform('Windows');
|
||||
input.setKeyboardMode('Auto');
|
||||
input.start();
|
||||
|
||||
input._handleKeyDown(keyEvt({
|
||||
code: 'ShiftLeft',
|
||||
key: 'Shift',
|
||||
shiftKey: true,
|
||||
}));
|
||||
input._handleKeyDown(keyEvt({
|
||||
code: 'Digit7',
|
||||
key: '&',
|
||||
shiftKey: true,
|
||||
}));
|
||||
|
||||
const digit = sent.find((m) => m.keyEvent && m.keyEvent.chr === 0x08);
|
||||
expect(digit).toBeDefined();
|
||||
expect(digit.keyEvent.mode).toBe('Map');
|
||||
expect(digit.keyEvent.modifiers).not.toContain(29);
|
||||
});
|
||||
|
||||
it('Auto mode uses Map scancode for letters on Windows peers', () => {
|
||||
const sent = [];
|
||||
const input = makeInput((msg) => sent.push(msg));
|
||||
@@ -219,90 +382,53 @@ describe('RDInput KeyboardMode.Map', () => {
|
||||
input.setKeyboardMode('Auto');
|
||||
input.start();
|
||||
|
||||
input._handleKeyDown({
|
||||
code: 'KeyA',
|
||||
key: 'a',
|
||||
repeat: false,
|
||||
preventDefault() {},
|
||||
stopPropagation() {},
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
});
|
||||
input._handleKeyDown(keyEvt({ code: 'KeyA', key: 'a' }));
|
||||
|
||||
expect(sent[0].keyEvent.mode).toBe('Map');
|
||||
expect(sent[0].keyEvent.chr).toBe(0x1E);
|
||||
});
|
||||
|
||||
it('Auto mode uses Legacy chr for Shift+digit symbols on Windows', () => {
|
||||
it('Legacy mode sends lowercase chr for letters', () => {
|
||||
const sent = [];
|
||||
const input = makeInput((msg) => sent.push(msg));
|
||||
input.setPeerPlatform('Windows');
|
||||
input.setKeyboardMode('Auto');
|
||||
input.setKeyboardMode('Legacy');
|
||||
input.start();
|
||||
|
||||
input._handleKeyDown({
|
||||
code: 'Digit7',
|
||||
key: '&',
|
||||
repeat: false,
|
||||
preventDefault() {},
|
||||
stopPropagation() {},
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: true,
|
||||
});
|
||||
input._handleKeyDown(keyEvt({
|
||||
code: 'KeyA',
|
||||
key: 'a',
|
||||
getModifierState(state) {
|
||||
return state === 'CapsLock';
|
||||
},
|
||||
}));
|
||||
|
||||
expect(sent[0].keyEvent.mode).toBe('Legacy');
|
||||
expect(sent[0].keyEvent.chr).toBe('&'.codePointAt(0));
|
||||
expect(sent[0].keyEvent.modifiers).not.toContain(29);
|
||||
expect(sent[0].keyEvent.chr).toBe(97);
|
||||
expect(sent[0].keyEvent.modifiers).toContain(3);
|
||||
});
|
||||
|
||||
it('Legacy mode sends character chr for printable keys', () => {
|
||||
it('Legacy mode sends digit chr for Digit1', () => {
|
||||
const sent = [];
|
||||
const input = makeInput((msg) => sent.push(msg));
|
||||
input.setPeerPlatform('Linux');
|
||||
input.setKeyboardMode('Legacy');
|
||||
input.start();
|
||||
|
||||
input._handleKeyDown({
|
||||
code: 'Digit1',
|
||||
key: '1',
|
||||
repeat: false,
|
||||
preventDefault() {},
|
||||
stopPropagation() {},
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
});
|
||||
input._handleKeyDown(keyEvt({ code: 'Digit1', key: '1' }));
|
||||
|
||||
expect(sent[0].keyEvent.mode).toBe('Legacy');
|
||||
expect(sent[0].keyEvent.chr).toBe(49);
|
||||
});
|
||||
|
||||
it('Auto mode sends Legacy controlKey for Shift', () => {
|
||||
it('CapsLock key alone sends no wire event', () => {
|
||||
const sent = [];
|
||||
const input = makeInput((msg) => sent.push(msg));
|
||||
input.setPeerPlatform('Windows');
|
||||
input.setKeyboardMode('Auto');
|
||||
input.start();
|
||||
|
||||
input._handleKeyDown({
|
||||
code: 'ShiftLeft',
|
||||
key: 'Shift',
|
||||
repeat: false,
|
||||
preventDefault() {},
|
||||
stopPropagation() {},
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
});
|
||||
input._handleKeyDown(keyEvt({ code: 'CapsLock', key: 'CapsLock' }));
|
||||
|
||||
expect(sent[0].keyEvent.mode).toBe('Legacy');
|
||||
expect(sent[0].keyEvent.controlKey).toBe('Shift');
|
||||
expect(sent.length).toBe(0);
|
||||
});
|
||||
|
||||
it('includes CapsLock modifier on Map letter keys when Caps is on', () => {
|
||||
@@ -312,46 +438,16 @@ describe('RDInput KeyboardMode.Map', () => {
|
||||
input.setKeyboardMode('Auto');
|
||||
input.start();
|
||||
|
||||
input._handleKeyDown({
|
||||
input._handleKeyDown(keyEvt({
|
||||
code: 'KeyA',
|
||||
key: 'a',
|
||||
repeat: false,
|
||||
preventDefault() {},
|
||||
stopPropagation() {},
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
getModifierState(state) {
|
||||
return state === 'CapsLock';
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
expect(sent[0].keyEvent.mode).toBe('Map');
|
||||
expect(sent[0].keyEvent.modifiers).toContain(3);
|
||||
});
|
||||
|
||||
it('Legacy mode uppercases letters when CapsLock is on', () => {
|
||||
const sent = [];
|
||||
const input = makeInput((msg) => sent.push(msg));
|
||||
input.setKeyboardMode('Legacy');
|
||||
input.start();
|
||||
|
||||
input._handleKeyDown({
|
||||
code: 'KeyA',
|
||||
key: 'a',
|
||||
repeat: false,
|
||||
preventDefault() {},
|
||||
stopPropagation() {},
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
getModifierState(state) {
|
||||
return state === 'CapsLock';
|
||||
},
|
||||
});
|
||||
|
||||
expect(sent[0].keyEvent.chr).toBe(65);
|
||||
expect(sent[0].keyEvent.modifiers).not.toContain(29);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,24 @@ function loadBrowserScript(relativePath, extraGlobals) {
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
function loadBrowserScripts(relativePaths, extraGlobals) {
|
||||
const sandbox = {
|
||||
console,
|
||||
window: {},
|
||||
globalThis: {},
|
||||
...extraGlobals,
|
||||
};
|
||||
if (!extraGlobals || extraGlobals.window === undefined) {
|
||||
sandbox.window = sandbox;
|
||||
}
|
||||
sandbox.globalThis = sandbox.window;
|
||||
for (const rel of relativePaths) {
|
||||
const filename = path.join(__dirname, '..', rel);
|
||||
vm.runInNewContext(fs.readFileSync(filename, 'utf8'), sandbox, { filename });
|
||||
}
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
describe('syncSessionMediaCapture', () => {
|
||||
it('activates only the active streaming session', () => {
|
||||
const clientA = { setSessionActive: jest.fn() };
|
||||
@@ -155,15 +173,14 @@ describe('RDInput multi-session keyboard isolation', () => {
|
||||
removeEventListener() {},
|
||||
};
|
||||
|
||||
const sandbox = loadBrowserScript('public/js/rdclient/input.js', {
|
||||
const sandbox = loadBrowserScripts([
|
||||
'public/js/rdclient/keyboard-scancode.js',
|
||||
'public/js/rdclient/keyboard-encoder.js',
|
||||
'public/js/rdclient/input.js',
|
||||
], {
|
||||
document,
|
||||
window: windowStub,
|
||||
RDProtocol: {},
|
||||
RDKeyboardScancode: {
|
||||
MODIFIER_CODES: ['ShiftLeft', 'ShiftRight', 'ControlLeft', 'ControlRight',
|
||||
'AltLeft', 'AltRight', 'MetaLeft', 'MetaRight'],
|
||||
codeToScancode: () => null,
|
||||
},
|
||||
makeCanvas,
|
||||
});
|
||||
RDInput = sandbox.window.RDInput;
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
<script src="/js/rdclient/audio.js?v=<%= cacheVersion %>"></script>
|
||||
<script src="/js/rdclient/renderer.js?v=<%= cacheVersion %>"></script>
|
||||
<script src="/js/rdclient/keyboard-scancode.js?v=<%= cacheVersion %>"></script>
|
||||
<script src="/js/rdclient/keyboard-encoder.js?v=<%= cacheVersion %>"></script>
|
||||
<script src="/js/rdclient/input.js?v=<%= cacheVersion %>"></script>
|
||||
<script src="/js/rdclient/touch.js?v=<%= cacheVersion %>"></script>
|
||||
<script src="/js/rdclient/file-connection.js?v=<%= cacheVersion %>"></script>
|
||||
|
||||
Reference in New Issue
Block a user