From d827d1ccf6bcc6bdfe64f13bacdcab5475d853da Mon Sep 17 00:00:00 2001 From: Nitish Agarwal <1592163+nitishagar@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:55:43 +0530 Subject: [PATCH] feat: add Num Lock / Caps Lock toggle buttons in InfoBar Make the existing Num Lock and Caps Lock indicators in the InfoBar interactive. Clicking one sends the lock-key scancode (press+release) to the remote over the existing keypressReport path, and each button's active state reflects the remote LED state from keyboardLedState. The buttons send the raw scancode like the e2e keyboard harness does, independent of the virtual-keyboard widget's keymap layer. Fixes #1464 --- ui/e2e/infobar-lock-toggles.spec.ts | 44 ++++++++++++++++++++++++ ui/src/components/InfoBar.tsx | 52 +++++++++++++++++------------ 2 files changed, 74 insertions(+), 22 deletions(-) create mode 100644 ui/e2e/infobar-lock-toggles.spec.ts diff --git a/ui/e2e/infobar-lock-toggles.spec.ts b/ui/e2e/infobar-lock-toggles.spec.ts new file mode 100644 index 000000000..950e3a62b --- /dev/null +++ b/ui/e2e/infobar-lock-toggles.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from "@playwright/test"; + +import { ensureLocalAuthMode, waitForWebRTCReady, getLedState, waitForLedState } from "./helpers"; + +test.describe("InfoBar lock toggle buttons", () => { + test.setTimeout(60_000); + + test.beforeEach(async ({ page }) => { + await page.goto("/"); + await page.waitForLoadState("networkidle"); + await ensureLocalAuthMode(page, { mode: "noPassword" }); + await waitForWebRTCReady(page); + }); + + test("Caps Lock toggle button sends scancode and reflects LED state", async ({ page }) => { + const initial = await getLedState(page); + expect(initial, "LED state should be available").not.toBeNull(); + const initialCapsLock = initial!.caps_lock; + + const capsBtn = page.getByTestId("caps-lock-toggle"); + await expect(capsBtn).toBeVisible(); + + await capsBtn.click(); + await waitForLedState(page, "caps_lock", !initialCapsLock); + + await capsBtn.click(); + await waitForLedState(page, "caps_lock", initialCapsLock); + }); + + test("Num Lock toggle button sends scancode and reflects LED state", async ({ page }) => { + const initial = await getLedState(page); + expect(initial, "LED state should be available").not.toBeNull(); + const initialNumLock = initial!.num_lock; + + const numBtn = page.getByTestId("num-lock-toggle"); + await expect(numBtn).toBeVisible(); + + await numBtn.click(); + await waitForLedState(page, "num_lock", !initialNumLock); + + await numBtn.click(); + await waitForLedState(page, "num_lock", initialNumLock); + }); +}); diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx index 12e45a9d9..4e1f6e484 100644 --- a/ui/src/components/InfoBar.tsx +++ b/ui/src/components/InfoBar.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useHidStore, @@ -9,9 +9,11 @@ import { VideoState, } from "@hooks/stores"; import { useHidRpc } from "@hooks/useHidRpc"; +import { useJsonRpc } from "@/hooks/useJsonRpc"; import { keys, modifiers } from "@/keyboardMappings"; import { cx } from "@/cva.config"; import { m } from "@localizations/messages.js"; +import { Button } from "@components/Button"; export default function InfoBar() { const { keysDownState } = useHidStore(); @@ -32,6 +34,18 @@ export default function InfoBar() { const { isTurnServerInUse, peerConnection, peerConnectionState } = useRTCStore(); const { hdmiState } = useVideoStore(); + const { send } = useJsonRpc(); + + const sendLockKey = useCallback( + (keyCode: number) => { + send("keypressReport", { key: keyCode, press: true }); + setTimeout(() => { + send("keypressReport", { key: keyCode, press: false }); + }, 20); + }, + [send], + ); + const [videoCodec, setVideoCodec] = useState(null); const [videoBitrate, setVideoBitrate] = useState(null); @@ -211,27 +225,21 @@ export default function InfoBar() { )} -
- {m.info_caps_lock()} -
- -
- {m.info_num_lock()} -
+