From ebf69da86c9e0361ba3eb58fee8bb8b6d4b9429e Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 13 Aug 2025 00:39:55 -0500 Subject: [PATCH 1/3] Lint fix from last merge. --- ui/src/routes/devices.$id.settings.mouse.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/src/routes/devices.$id.settings.mouse.tsx b/ui/src/routes/devices.$id.settings.mouse.tsx index d683721..26c4b5b 100644 --- a/ui/src/routes/devices.$id.settings.mouse.tsx +++ b/ui/src/routes/devices.$id.settings.mouse.tsx @@ -9,13 +9,13 @@ import { useSettingsStore } from "@/hooks/stores"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SelectMenuBasic } from "@components/SelectMenuBasic"; +import { JigglerSetting } from "@components/JigglerSetting"; import { cx } from "../cva.config"; - -import { SettingsItem } from "./devices.$id.settings"; import notifications from "../notifications"; import SettingsNestedSection from "../components/SettingsNestedSection"; -import { JigglerSetting } from "@components/JigglerSetting"; + +import { SettingsItem } from "./devices.$id.settings"; export interface JigglerConfig { inactivity_limit_seconds: number; From bb609ea84d5f110b22a2bae19e5341298f2fe902 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Thu, 7 Aug 2025 06:23:35 +0000 Subject: [PATCH 2/3] Add ability to track modifier state on the device Remove LED sync source and add keypress reporting We return the modifiers as the valid bitmask so that the VirtualKeyboard can represent the correct keys as down. This is important when we have strokes like Left-Control + Right-Control + Keypad-1 (used in switching KVMs and such) --- internal/usbgadget/hid_keyboard.go | 196 ++++++-- internal/usbgadget/usbgadget.go | 13 +- jsonrpc.go | 2 + ui/eslint.config.cjs | 4 + ui/package-lock.json | 426 +++++++++--------- ui/src/components/InfoBar.tsx | 21 +- ui/src/components/VirtualKeyboard.tsx | 34 +- ui/src/components/WebRTCVideo.tsx | 134 +++--- ui/src/hooks/stores.ts | 51 +-- ui/src/hooks/useKeyboard.ts | 28 +- ui/src/keyboardMappings.ts | 1 + .../routes/devices.$id.settings.keyboard.tsx | 28 +- ui/src/routes/devices.$id.tsx | 38 +- usb.go | 20 +- webrtc.go | 8 +- 15 files changed, 570 insertions(+), 434 deletions(-) diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go index 6ad3b6a..7be48d4 100644 --- a/internal/usbgadget/hid_keyboard.go +++ b/internal/usbgadget/hid_keyboard.go @@ -1,10 +1,10 @@ package usbgadget import ( + "bytes" "context" "fmt" "os" - "reflect" "time" ) @@ -61,6 +61,8 @@ var keyboardReportDesc = []byte{ const ( hidReadBufferSize = 8 + hidKeyBufferSize = 6 + hidErrorRollOver = 0x01 // https://www.usb.org/sites/default/files/documents/hid1_11.pdf // https://www.usb.org/sites/default/files/hut1_2.pdf KeyboardLedMaskNumLock = 1 << 0 @@ -68,7 +70,9 @@ const ( KeyboardLedMaskScrollLock = 1 << 2 KeyboardLedMaskCompose = 1 << 3 KeyboardLedMaskKana = 1 << 4 - ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana + // power on/off LED is 5 + KeyboardLedMaskShift = 1 << 6 + ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana | KeyboardLedMaskShift ) // Synchronization between LED states and CAPS LOCK, NUM LOCK, SCROLL LOCK, @@ -81,6 +85,7 @@ type KeyboardState struct { ScrollLock bool `json:"scroll_lock"` Compose bool `json:"compose"` Kana bool `json:"kana"` + Shift bool `json:"shift"` // This is not part of the main USB HID spec } func getKeyboardState(b byte) KeyboardState { @@ -91,27 +96,27 @@ func getKeyboardState(b byte) KeyboardState { ScrollLock: b&KeyboardLedMaskScrollLock != 0, Compose: b&KeyboardLedMaskCompose != 0, Kana: b&KeyboardLedMaskKana != 0, + Shift: b&KeyboardLedMaskShift != 0, } } -func (u *UsbGadget) updateKeyboardState(b byte) { +func (u *UsbGadget) updateKeyboardState(state byte) { u.keyboardStateLock.Lock() defer u.keyboardStateLock.Unlock() - if b&^ValidKeyboardLedMasks != 0 { - u.log.Trace().Uint8("b", b).Msg("contains invalid bits, ignoring") + if state&^ValidKeyboardLedMasks != 0 { + u.log.Error().Uint8("state", state).Msg("ignoring invalid bits") return } - newState := getKeyboardState(b) - if reflect.DeepEqual(u.keyboardState, newState) { + if u.keyboardState == state { return } - u.log.Info().Interface("old", u.keyboardState).Interface("new", newState).Msg("keyboardState updated") - u.keyboardState = newState + u.log.Trace().Interface("old", u.keyboardState).Interface("new", state).Msg("keyboardState updated") + u.keyboardState = state if u.onKeyboardStateChange != nil { - (*u.onKeyboardStateChange)(newState) + (*u.onKeyboardStateChange)(getKeyboardState(state)) } } @@ -123,7 +128,52 @@ func (u *UsbGadget) GetKeyboardState() KeyboardState { u.keyboardStateLock.Lock() defer u.keyboardStateLock.Unlock() - return u.keyboardState + return getKeyboardState(u.keyboardState) +} + +const ( + // https://www.usb.org/sites/default/files/documents/hid1_11.pdf Appendix C + ModifierMaskLeftControl = 0x01 + ModifierMaskRightControl = 0x10 + ModifierMaskLeftShift = 0x02 + ModifierMaskRightShift = 0x20 + ModifierMaskLeftAlt = 0x04 + ModifierMaskRightAlt = 0x40 + ModifierMaskLeftSuper = 0x08 + ModifierMaskRightSuper = 0x80 + + EitherShiftMask = ModifierMaskLeftShift | ModifierMaskRightShift + EitherControlMask = ModifierMaskLeftControl | ModifierMaskRightControl + EitherAltMask = ModifierMaskLeftAlt | ModifierMaskRightAlt + EitherSuperMask = ModifierMaskLeftSuper | ModifierMaskRightSuper +) + +func (u *UsbGadget) GetKeysDownState() KeysDownState { + u.keyboardStateLock.Lock() + defer u.keyboardStateLock.Unlock() + + return u.keysDownState +} + +func (u *UsbGadget) updateKeyDownState(state KeysDownState) { + u.keyboardStateLock.Lock() + defer u.keyboardStateLock.Unlock() + + if u.keysDownState.Modifier == state.Modifier && + bytes.Equal(u.keysDownState.Keys, state.Keys) { + return // No change in key down state + } + + u.log.Trace().Interface("old", u.keysDownState).Interface("new", state).Msg("keysDownState updated") + u.keysDownState = state + + if u.onKeysDownChange != nil { + (*u.onKeysDownChange)(state) + } +} + +func (u *UsbGadget) SetOnKeysDownChange(f func(state KeysDownState)) { + u.onKeysDownChange = &f } func (u *UsbGadget) listenKeyboardEvents() { @@ -142,7 +192,7 @@ func (u *UsbGadget) listenKeyboardEvents() { l.Info().Msg("context done") return default: - l.Trace().Msg("reading from keyboard") + l.Trace().Msg("reading from keyboard for LED state changes") if u.keyboardHidFile == nil { u.logWithSuppression("keyboardHidFileNil", 100, &l, nil, "keyboardHidFile is nil") // show the error every 100 times to avoid spamming the logs @@ -195,12 +245,12 @@ func (u *UsbGadget) OpenKeyboardHidFile() error { return u.openKeyboardHidFile() } -func (u *UsbGadget) keyboardWriteHidFile(data []byte) error { +func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error { if err := u.openKeyboardHidFile(); err != nil { return err } - _, err := u.keyboardHidFile.Write(data) + _, err := u.keyboardHidFile.Write(append([]byte{modifier, 0x00}, keys[:]...)) if err != nil { u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0") u.keyboardHidFile.Close() @@ -211,22 +261,116 @@ func (u *UsbGadget) keyboardWriteHidFile(data []byte) error { return nil } -func (u *UsbGadget) KeyboardReport(modifier uint8, keys []uint8) error { +func (u *UsbGadget) KeyboardReport(modifier byte, keys []byte) error { u.keyboardLock.Lock() defer u.keyboardLock.Unlock() + defer u.resetUserInputTime() - if len(keys) > 6 { - keys = keys[:6] + if len(keys) > hidKeyBufferSize { + keys = keys[:hidKeyBufferSize] } - if len(keys) < 6 { - keys = append(keys, make([]uint8, 6-len(keys))...) + if len(keys) < hidKeyBufferSize { + keys = append(keys, make([]byte, hidKeyBufferSize-len(keys))...) } - err := u.keyboardWriteHidFile([]byte{modifier, 0, keys[0], keys[1], keys[2], keys[3], keys[4], keys[5]}) - if err != nil { - return err - } - - u.resetUserInputTime() - return nil + return u.keyboardWriteHidFile(modifier, keys) +} + +const ( + // https://www.usb.org/sites/default/files/documents/hut1_2.pdf + // Dynamic Flags (DV) + LeftControl = 0xE0 + LeftShift = 0xE1 + LeftAlt = 0xE2 + LeftSuper = 0xE3 // Left GUI (e.g. Windows key, Apple Command key) + RightControl = 0xE4 + RightShift = 0xE5 + RightAlt = 0xE6 + RightSuper = 0xE7 // Right GUI (e.g. Windows key, Apple Command key) +) + +// KeyCodeMask maps a key code to its corresponding bit mask +type KeyCodeMask struct { + KeyCode byte + Mask byte +} + +// KeyCodeToMaskMap is a slice of KeyCodeMask for quick lookup +var KeyCodeToMaskMap = map[uint8]uint8{ + LeftControl: ModifierMaskLeftControl, + LeftShift: ModifierMaskLeftShift, + LeftAlt: ModifierMaskLeftAlt, + LeftSuper: ModifierMaskLeftSuper, + RightControl: ModifierMaskRightControl, + RightShift: ModifierMaskRightShift, + RightAlt: ModifierMaskRightAlt, + RightSuper: ModifierMaskRightSuper, +} + +func (u *UsbGadget) KeypressReport(key byte, press bool) (KeysDownState, error) { + u.keyboardLock.Lock() + defer u.keyboardLock.Unlock() + defer u.resetUserInputTime() + + var state = u.keysDownState + modifier := state.Modifier + keys := state.Keys[:] + + if mask, exists := KeyCodeToMaskMap[key]; exists { + // If the key is a modifier key, we update the keyboardModifier state + // by setting or clearing the corresponding bit in the modifier byte. + // This allows us to track the state of modifier keys like Shift, Control, Alt, and Super. + if press { + modifier |= mask + } else { + modifier &^= mask + } + } else { + // handle other keys that are not modifier keys by placing or removing them + // from the key buffer since the buffer tracks currently pressed keys + overrun := true + for i := range hidKeyBufferSize { + // If we find the key in the buffer the buffer, we either remove it (if press is false) + // or do nothing (if down is true) because the buffer tracks currently pressed keys + // and if we find a zero byte, we can place the key there (if press is true) + if keys[i] == key || keys[i] == 0 { + if press { + keys[i] = key // overwrites the zero byte or the same key if already pressed + } else { + // we are releasing the key, remove it from the buffer + if keys[i] != 0 { + copy(keys[i:], keys[i+1:]) + keys[hidKeyBufferSize-1] = 0 // Clear the last byte + } + } + overrun = false // We found a slot for the key + break + } + } + + // If we reach here it means we didn't find an empty slot or the key in the buffer + if overrun { + if press { + u.log.Error().Uint8("key", key).Msg("keyboard buffer overflow, key not added") + // Fill all key slots with ErrorRollOver (0x01) to indicate overflow + for i := range keys { + keys[i] = hidErrorRollOver + } + } else { + // If we are releasing a key, and we didn't find it in a slot, who cares? + u.log.Warn().Uint8("key", key).Msg("key not found in buffer, nothing to release") + } + } + } + + if err := u.keyboardWriteHidFile(modifier, keys); err != nil { + u.log.Warn().Uint8("modifier", modifier).Bytes("keys", keys).Msg("Could not write keypress report to hidg0") + } + + state.Modifier = modifier + state.Keys = keys + + u.updateKeyDownState(state) + + return state, nil } diff --git a/internal/usbgadget/usbgadget.go b/internal/usbgadget/usbgadget.go index cb70655..21e37cd 100644 --- a/internal/usbgadget/usbgadget.go +++ b/internal/usbgadget/usbgadget.go @@ -41,6 +41,11 @@ var defaultUsbGadgetDevices = Devices{ MassStorage: true, } +type KeysDownState struct { + Modifier byte `json:"modifier"` + Keys []byte `json:"keys"` +} + // UsbGadget is a struct that represents a USB gadget. type UsbGadget struct { name string @@ -60,7 +65,9 @@ type UsbGadget struct { relMouseHidFile *os.File relMouseLock sync.Mutex - keyboardState KeyboardState + keyboardState byte // keyboard latched state (NumLock, CapsLock, ScrollLock, Compose, Kana) + keysDownState KeysDownState // keyboard dynamic state (modifier keys and pressed keys) + keyboardStateLock sync.Mutex keyboardStateCtx context.Context keyboardStateCancel context.CancelFunc @@ -77,6 +84,7 @@ type UsbGadget struct { txLock sync.Mutex onKeyboardStateChange *func(state KeyboardState) + onKeysDownChange *func(state KeysDownState) log *zerolog.Logger @@ -122,7 +130,8 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev txLock: sync.Mutex{}, keyboardStateCtx: keyboardCtx, keyboardStateCancel: keyboardCancel, - keyboardState: KeyboardState{}, + keyboardState: 0, + keysDownState: KeysDownState{Modifier: 0, Keys: make([]byte, hidKeyBufferSize)}, enabledDevices: *enabledDevices, lastUserInput: time.Now(), log: logger, diff --git a/jsonrpc.go b/jsonrpc.go index a0264b8..81b6a65 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -1047,6 +1047,8 @@ var rpcHandlers = map[string]RPCHandler{ "renewDHCPLease": {Func: rpcRenewDHCPLease}, "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, "getKeyboardLedState": {Func: rpcGetKeyboardLedState}, + "keypressReport": {Func: rpcKeypressReport, Params: []string{"key", "press"}}, + "getKeyDownState": {Func: rpcGetKeysDownState}, "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, diff --git a/ui/eslint.config.cjs b/ui/eslint.config.cjs index a6c0c1f..6e97258 100644 --- a/ui/eslint.config.cjs +++ b/ui/eslint.config.cjs @@ -66,6 +66,10 @@ module.exports = defineConfig([{ groups: ["builtin", "external", "internal", "parent", "sibling"], "newlines-between": "always", }], + + "@typescript-eslint/no-unused-vars": ["warn", { + "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" + }], }, settings: { diff --git a/ui/package-lock.json b/ui/package-lock.json index 72a4849..bc05d41 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -112,9 +112,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", - "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "cpu": [ "ppc64" ], @@ -128,9 +128,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", - "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ "arm" ], @@ -144,9 +144,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", - "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], @@ -160,9 +160,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", - "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "cpu": [ "x64" ], @@ -176,9 +176,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", - "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], @@ -192,9 +192,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", - "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], @@ -208,9 +208,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", - "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ "arm64" ], @@ -224,9 +224,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", - "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ "x64" ], @@ -240,9 +240,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", - "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ "arm" ], @@ -256,9 +256,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", - "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "cpu": [ "arm64" ], @@ -272,9 +272,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", - "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "cpu": [ "ia32" ], @@ -288,9 +288,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", - "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "cpu": [ "loong64" ], @@ -304,9 +304,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", - "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "cpu": [ "mips64el" ], @@ -320,9 +320,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", - "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "cpu": [ "ppc64" ], @@ -336,9 +336,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", - "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "cpu": [ "riscv64" ], @@ -352,9 +352,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", - "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ "s390x" ], @@ -368,9 +368,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", - "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ "x64" ], @@ -384,9 +384,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", - "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", "cpu": [ "arm64" ], @@ -400,9 +400,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", - "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "cpu": [ "x64" ], @@ -416,9 +416,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", - "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", "cpu": [ "arm64" ], @@ -432,9 +432,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", - "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "cpu": [ "x64" ], @@ -448,9 +448,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", - "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", "cpu": [ "arm64" ], @@ -464,9 +464,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", - "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "cpu": [ "x64" ], @@ -480,9 +480,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", - "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "cpu": [ "arm64" ], @@ -496,9 +496,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", - "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "cpu": [ "ia32" ], @@ -512,9 +512,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", - "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "cpu": [ "x64" ], @@ -555,9 +555,9 @@ } }, "node_modules/@eslint/compat": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.1.tgz", - "integrity": "sha512-k8MHony59I5EPic6EQTCNOuPoVBnoYXkP+20xvwFjN7t0qI3ImyvyBgg+hIVPwC8JaxVjjUZld+cLfBLFDLucg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.2.tgz", + "integrity": "sha512-jRNwzTbd6p2Rw4sZ1CgWRS8YMtqG15YyZf7zvb6gY2rB2u6n+2Z+ELW0GtL0fQgyl0pr4Y/BzBfng/BdsereRA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -587,18 +587,18 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -643,9 +643,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -664,12 +664,12 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -845,9 +845,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -866,16 +866,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1964,9 +1964,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", - "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", + "version": "19.1.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz", + "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==", "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1996,17 +1996,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", - "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz", + "integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/type-utils": "8.39.0", - "@typescript-eslint/utils": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/type-utils": "8.39.1", + "@typescript-eslint/utils": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2020,7 +2020,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/parser": "^8.39.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -2036,16 +2036,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", - "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz", + "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4" }, "engines": { @@ -2061,14 +2061,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", - "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", + "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.39.0", - "@typescript-eslint/types": "^8.39.0", + "@typescript-eslint/tsconfig-utils": "^8.39.1", + "@typescript-eslint/types": "^8.39.1", "debug": "^4.3.4" }, "engines": { @@ -2083,14 +2083,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", - "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", + "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0" + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2101,9 +2101,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", - "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", + "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", "dev": true, "license": "MIT", "engines": { @@ -2118,15 +2118,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", - "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz", + "integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0", - "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/utils": "8.39.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2143,9 +2143,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", - "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz", + "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", "dev": true, "license": "MIT", "engines": { @@ -2157,16 +2157,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", - "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", + "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.39.0", - "@typescript-eslint/tsconfig-utils": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/project-service": "8.39.1", + "@typescript-eslint/tsconfig-utils": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2212,16 +2212,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", - "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz", + "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0" + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2236,13 +2236,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", - "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", + "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/types": "8.39.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2650,9 +2650,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", + "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", "dev": true, "funding": [ { @@ -2670,8 +2670,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", + "caniuse-lite": "^1.0.30001733", + "electron-to-chromium": "^1.5.199", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -2739,9 +2739,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001731", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "version": "1.0.30001734", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", + "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", "dev": true, "funding": [ { @@ -3159,9 +3159,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.198", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.198.tgz", - "integrity": "sha512-G5COfnp3w+ydVu80yprgWSfmfQaYRh9DOxfhAxstLyetKaLyl55QrNjx8C38Pc/C+RaDmb1M0Lk8wPEMQ+bGgQ==", + "version": "1.5.200", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.200.tgz", + "integrity": "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==", "dev": true, "license": "ISC" }, @@ -3350,9 +3350,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", - "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -3362,32 +3362,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.8", - "@esbuild/android-arm": "0.25.8", - "@esbuild/android-arm64": "0.25.8", - "@esbuild/android-x64": "0.25.8", - "@esbuild/darwin-arm64": "0.25.8", - "@esbuild/darwin-x64": "0.25.8", - "@esbuild/freebsd-arm64": "0.25.8", - "@esbuild/freebsd-x64": "0.25.8", - "@esbuild/linux-arm": "0.25.8", - "@esbuild/linux-arm64": "0.25.8", - "@esbuild/linux-ia32": "0.25.8", - "@esbuild/linux-loong64": "0.25.8", - "@esbuild/linux-mips64el": "0.25.8", - "@esbuild/linux-ppc64": "0.25.8", - "@esbuild/linux-riscv64": "0.25.8", - "@esbuild/linux-s390x": "0.25.8", - "@esbuild/linux-x64": "0.25.8", - "@esbuild/netbsd-arm64": "0.25.8", - "@esbuild/netbsd-x64": "0.25.8", - "@esbuild/openbsd-arm64": "0.25.8", - "@esbuild/openbsd-x64": "0.25.8", - "@esbuild/openharmony-arm64": "0.25.8", - "@esbuild/sunos-x64": "0.25.8", - "@esbuild/win32-arm64": "0.25.8", - "@esbuild/win32-ia32": "0.25.8", - "@esbuild/win32-x64": "0.25.8" + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, "node_modules/escalade": { @@ -3413,19 +3413,19 @@ } }, "node_modules/eslint": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", - "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.32.0", - "@eslint/plugin-kit": "^0.3.4", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -4747,9 +4747,9 @@ } }, "node_modules/js-base64": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", - "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", "license": "BSD-3-Clause" }, "node_modules/js-tokens": { @@ -5851,9 +5851,9 @@ } }, "node_modules/react-simple-keyboard": { - "version": "3.8.106", - "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.106.tgz", - "integrity": "sha512-ItCHCdhVCzn9huhenuyuHQMOGsl3UMLu5xAO1bkjj4AAgVoktFC1DQ4HWkOS6BGPvUJejFM3Q5hVM8Bl2oX9pA==", + "version": "3.8.108", + "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.108.tgz", + "integrity": "sha512-Q3JK/qnaDjTMaE6EcNOG4MJV8jHqug2K1YIz9j+QXgmAE/nUuHXuyAnLIZuU3uvqv2n/556l2hLkqr9jW1LgUw==", "license": "MIT", "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx index 7ce67a4..02ff170 100644 --- a/ui/src/components/InfoBar.tsx +++ b/ui/src/components/InfoBar.tsx @@ -38,9 +38,6 @@ export default function InfoBar() { }, [rpcDataChannel]); const keyboardLedState = useHidStore(state => state.keyboardLedState); - const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable); - const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); - const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse); const usbState = useHidStore(state => state.usbState); @@ -122,19 +119,6 @@ export default function InfoBar() { )} - {keyboardLedStateSyncAvailable ? ( -
- {keyboardLedSync === "browser" ? "Browser" : "Host"} -
- ) : null}
) : null} + {keyboardLedState?.shift ? ( +
+ Shift +
+ ) : null}
diff --git a/ui/src/components/VirtualKeyboard.tsx b/ui/src/components/VirtualKeyboard.tsx index 4ff04a9..18c5fbe 100644 --- a/ui/src/components/VirtualKeyboard.tsx +++ b/ui/src/components/VirtualKeyboard.tsx @@ -1,7 +1,7 @@ import { useShallow } from "zustand/react/shallow"; import { ChevronDownIcon } from "@heroicons/react/16/solid"; import { AnimatePresence, motion } from "framer-motion"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import Keyboard from "react-simple-keyboard"; import Card from "@components/Card"; @@ -13,7 +13,7 @@ import "react-simple-keyboard/build/css/index.css"; import AttachIconRaw from "@/assets/attach-icon.svg"; import DetachIconRaw from "@/assets/detach-icon.svg"; import { cx } from "@/cva.config"; -import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores"; +import { useHidStore, useUiStore } from "@/hooks/stores"; import useKeyboard from "@/hooks/useKeyboard"; import { keyDisplayMap, keys, modifiers } from "@/keyboardMappings"; @@ -44,15 +44,19 @@ function KeyboardWrapper() { const isCapsLockActive = useHidStore(useShallow(state => state.keyboardLedState?.caps_lock)); - // HID related states - const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable); - const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); - const isKeyboardLedManagedByHost = useMemo(() => - keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable, - [keyboardLedSync, keyboardLedStateSyncAvailable], - ); + /* + // These will be used to display the currently pressed keys and modifiers on the virtual keyboard - const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive); + // used to show the modifier keys that are in the "down state" on the virtual keyboard + const keyNamesFromModifierMask = (activeModifiers: number): string[] => { + return Object.entries(modifiers).filter(m => (activeModifiers & m[1]) !== 0).map(m => m[0]); + } + + // used to show the regular keys that are in the "down state" on the virtual keyboard + const keyNamesFromDownKeys = (downKeys: number[]) => { + return Object.entries(keys).filter(([_, code]) => downKeys.includes(code)).map(([name, _]) => name); + } + */ const startDrag = useCallback((e: MouseEvent | TouchEvent) => { if (!keyboardRef.current) return; @@ -168,19 +172,11 @@ function KeyboardWrapper() { toggleLayout(); if (isCapsLockActive) { - if (!isKeyboardLedManagedByHost) { - setIsCapsLockActive(false); - } sendKeyboardEvent([keys["CapsLock"]], []); return; } } - // Handle caps lock state change - if (isKeyCaps && !isKeyboardLedManagedByHost) { - setIsCapsLockActive(!isCapsLockActive); - } - // Collect new active keys and modifiers const newKeys = keys[cleanKey] ? [keys[cleanKey]] : []; const newModifiers = @@ -196,7 +192,7 @@ function KeyboardWrapper() { setTimeout(resetKeyboardState, 100); }, - [isCapsLockActive, isKeyboardLedManagedByHost, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive], + [isCapsLockActive, sendKeyboardEvent, resetKeyboardState], ); const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled); diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 4312c91..3878284 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -27,6 +27,7 @@ import { export default function WebRTCVideo() { // Video and stream related refs and states + const oldSchool = false;// useSettingsStore(state => state.oldSchoolKeyboard); const videoElm = useRef(null); const mediaStream = useRTCStore(state => state.mediaStream); const [isPlaying, setIsPlaying] = useState(false); @@ -34,7 +35,7 @@ export default function WebRTCVideo() { const [isPointerLockActive, setIsPointerLockActive] = useState(false); // Store hooks const settings = useSettingsStore(); - const { sendKeyboardEvent, resetKeyboardState } = useKeyboard(); + const { sendKeyboardEvent, sendKeypressEvent, resetKeyboardState } = useKeyboard(); const setMousePosition = useMouseStore(state => state.setMousePosition); const setMouseMove = useMouseStore(state => state.setMouseMove); const { @@ -51,18 +52,6 @@ export default function WebRTCVideo() { const videoBrightness = useSettingsStore(state => state.videoBrightness); const videoContrast = useSettingsStore(state => state.videoContrast); - // HID related states - const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable); - const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); - const isKeyboardLedManagedByHost = useMemo(() => - keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable, - [keyboardLedSync, keyboardLedStateSyncAvailable], - ); - - const setIsNumLockActive = useHidStore(state => state.setIsNumLockActive); - const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive); - const setIsScrollLockActive = useHidStore(state => state.setIsScrollLockActive); - // RTC related states const peerConnection = useRTCStore(state => state.peerConnection); @@ -414,83 +403,66 @@ export default function WebRTCVideo() { async (e: KeyboardEvent) => { e.preventDefault(); const prev = useHidStore.getState(); - let code = e.code; - const key = e.key; - - if (!isKeyboardLedManagedByHost) { - setIsNumLockActive(e.getModifierState("NumLock")); - setIsCapsLockActive(e.getModifierState("CapsLock")); - setIsScrollLockActive(e.getModifierState("ScrollLock")); - } - - if (code == "IntlBackslash" && ["`", "~"].includes(key)) { - code = "Backquote"; - } else if (code == "Backquote" && ["§", "±"].includes(key)) { - code = "IntlBackslash"; - } - - // Add the key to the active keys - const newKeys = [...prev.activeKeys, keys[code]].filter(Boolean); - - // Add the modifier to the active modifiers - const newModifiers = handleModifierKeys(e, [ - ...prev.activeModifiers, - modifiers[code], - ]); + const code = getAdjustedKeyCode(e); + const hidKey = keys[code]; // When pressing the meta key + another key, the key will never trigger a keyup // event, so we need to clear the keys after a short delay // https://bugs.chromium.org/p/chromium/issues/detail?id=28089 // https://bugzilla.mozilla.org/show_bug.cgi?id=1299553 - if (e.metaKey) { - setTimeout(() => { - const prev = useHidStore.getState(); - sendKeyboardEvent([], newModifiers || prev.activeModifiers); - }, 10); - } + + if (oldSchool) { + // Add the modifier to the active modifiers + const newModifiers = handleModifierKeys(e, [ + ...prev.activeModifiers, + modifiers[code], + ]); - sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); + // Add the key to the active keys + const newKeys = [...prev.activeKeys, hidKey].filter(Boolean); + + if (e.metaKey) { + setTimeout((activeModifiers: number[]) => { + // TODO this should probably be passing prev.activeKeys not an empty array + sendKeyboardEvent([], newModifiers || activeModifiers); + }, 10, prev.activeModifiers); + } + sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); + } + else { + if (e.metaKey) { + setTimeout(() => { + sendKeypressEvent(hidKey, false); + }, 10); + } + sendKeypressEvent(hidKey, true); + } }, - [ - handleModifierKeys, - sendKeyboardEvent, - isKeyboardLedManagedByHost, - setIsNumLockActive, - setIsCapsLockActive, - setIsScrollLockActive, - ], + [handleModifierKeys, oldSchool, sendKeyboardEvent, sendKeypressEvent], ); const keyUpHandler = useCallback( - (e: KeyboardEvent) => { + async (e: KeyboardEvent) => { e.preventDefault(); const prev = useHidStore.getState(); + const code = getAdjustedKeyCode(e); + const hidKey = keys[code]; - if (!isKeyboardLedManagedByHost) { - setIsNumLockActive(e.getModifierState("NumLock")); - setIsCapsLockActive(e.getModifierState("CapsLock")); - setIsScrollLockActive(e.getModifierState("ScrollLock")); + if (oldSchool) { + // Filter out the modifier that was just released + const newModifiers = handleModifierKeys( + e, + prev.activeModifiers.filter(k => k !== modifiers[code]), + ); + + // Filtering out the key that was just released (keys[e.code]) + const newKeys = prev.activeKeys.filter(k => k !== hidKey).filter(Boolean); + sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); + } else { + sendKeypressEvent(hidKey, false); } - - // Filtering out the key that was just released (keys[e.code]) - const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean); - - // Filter out the modifier that was just released - const newModifiers = handleModifierKeys( - e, - prev.activeModifiers.filter(k => k !== modifiers[e.code]), - ); - - sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); }, - [ - handleModifierKeys, - sendKeyboardEvent, - isKeyboardLedManagedByHost, - setIsNumLockActive, - setIsCapsLockActive, - setIsScrollLockActive, - ], + [handleModifierKeys, oldSchool, sendKeyboardEvent, sendKeypressEvent], ); const videoKeyUpHandler = useCallback((e: KeyboardEvent) => { @@ -667,6 +639,18 @@ export default function WebRTCVideo() { }; }, [videoSaturation, videoBrightness, videoContrast]); + function getAdjustedKeyCode(e: KeyboardEvent) { + const key = e.key; + let code = e.code; + + if (code == "IntlBackslash" && ["`", "~"].includes(key)) { + code = "Backquote"; + } else if (code == "Backquote" && ["§", "±"].includes(key)) { + code = "IntlBackslash"; + } + return code; + } + return (
diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index aa29528..e788faa 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -283,8 +283,6 @@ export const useVideoStore = create(set => ({ }, })); -export type KeyboardLedSync = "auto" | "browser" | "host"; - interface SettingsState { isCursorHidden: boolean; setCursorVisibility: (enabled: boolean) => void; @@ -308,9 +306,6 @@ interface SettingsState { keyboardLayout: string; setKeyboardLayout: (layout: string) => void; - keyboardLedSync: KeyboardLedSync; - setKeyboardLedSync: (sync: KeyboardLedSync) => void; - scrollThrottling: number; setScrollThrottling: (value: number) => void; @@ -356,9 +351,6 @@ export const useSettingsStore = create( keyboardLayout: "en-US", setKeyboardLayout: layout => set({ keyboardLayout: layout }), - keyboardLedSync: "auto", - setKeyboardLedSync: sync => set({ keyboardLedSync: sync }), - scrollThrottling: 0, setScrollThrottling: value => set({ scrollThrottling: value }), @@ -436,14 +428,13 @@ export interface KeyboardLedState { scroll_lock: boolean; compose: boolean; kana: boolean; + shift: boolean; // Optional, as not all keyboards have a shift LED }; -const defaultKeyboardLedState: KeyboardLedState = { - num_lock: false, - caps_lock: false, - scroll_lock: false, - compose: false, - kana: false, -}; + +export interface KeysDownState { + modifier: number; + keys: number[]; +} export interface HidState { activeKeys: number[]; @@ -465,12 +456,9 @@ export interface HidState { keyboardLedState?: KeyboardLedState; setKeyboardLedState: (state: KeyboardLedState) => void; - setIsNumLockActive: (active: boolean) => void; - setIsCapsLockActive: (active: boolean) => void; - setIsScrollLockActive: (active: boolean) => void; - keyboardLedStateSyncAvailable: boolean; - setKeyboardLedStateSyncAvailable: (available: boolean) => void; + keysDownState?: KeysDownState; + setKeysDownState: (state: KeysDownState) => void; isVirtualKeyboardEnabled: boolean; setVirtualKeyboardEnabled: (enabled: boolean) => void; @@ -482,9 +470,10 @@ export interface HidState { setUsbState: (state: HidState["usbState"]) => void; } -export const useHidStore = create((set, get) => ({ +export const useHidStore = create((set) => ({ activeKeys: [], activeModifiers: [], + updateActiveKeysAndModifiers: ({ keys, modifiers }) => { return set({ activeKeys: keys, activeModifiers: modifiers }); }, @@ -498,25 +487,11 @@ export const useHidStore = create((set, get) => ({ altGrCtrlTime: 0, setAltGrCtrlTime: time => set({ altGrCtrlTime: time }), + keyboardLedState: undefined, setKeyboardLedState: ledState => set({ keyboardLedState: ledState }), - setIsNumLockActive: active => { - const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) }; - keyboardLedState.num_lock = active; - set({ keyboardLedState }); - }, - setIsCapsLockActive: active => { - const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) }; - keyboardLedState.caps_lock = active; - set({ keyboardLedState }); - }, - setIsScrollLockActive: active => { - const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) }; - keyboardLedState.scroll_lock = active; - set({ keyboardLedState }); - }, - keyboardLedStateSyncAvailable: false, - setKeyboardLedStateSyncAvailable: available => set({ keyboardLedStateSyncAvailable: available }), + keysDownState: undefined, + setKeysDownState: state => set({ keysDownState: state }), isVirtualKeyboardEnabled: false, setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }), diff --git a/ui/src/hooks/useKeyboard.ts b/ui/src/hooks/useKeyboard.ts index 0ce1eef..7861dd9 100644 --- a/ui/src/hooks/useKeyboard.ts +++ b/ui/src/hooks/useKeyboard.ts @@ -1,6 +1,6 @@ import { useCallback } from "react"; -import { useHidStore, useRTCStore } from "@/hooks/stores"; +import { KeysDownState, useHidStore, useRTCStore } from "@/hooks/stores"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import { keys, modifiers } from "@/keyboardMappings"; @@ -25,6 +25,30 @@ export default function useKeyboard() { [rpcDataChannel?.readyState, send, updateActiveKeysAndModifiers], ); + const modifiersFromModifierMask = (activeModifiers: number): number[] => { + return Object.values(modifiers).filter(m => (activeModifiers & m) !== 0); + } + + const sendKeypressEvent = useCallback( + (key: number, press: boolean) => { + if (rpcDataChannel?.readyState !== "open") return; + + send("keypressReport", { key, press }, resp => { + if ("error" in resp) { + console.error("Failed to send keypress:", resp.error); + } else { + const keyDownState = resp.result as KeysDownState; + const keysDown = keyDownState.keys; + const activeModifiers = modifiersFromModifierMask(keyDownState.modifier) + + // We do this for the info bar to display the currently pressed keys for the user + updateActiveKeysAndModifiers({ keys: keysDown, modifiers: activeModifiers }); + } + }); + }, + [rpcDataChannel?.readyState, send, updateActiveKeysAndModifiers], + ); + const resetKeyboardState = useCallback(() => { sendKeyboardEvent([], []); }, [sendKeyboardEvent]); @@ -52,5 +76,5 @@ export default function useKeyboard() { } }; - return { sendKeyboardEvent, resetKeyboardState, executeMacro }; + return { sendKeyboardEvent, sendKeypressEvent, resetKeyboardState, executeMacro }; } diff --git a/ui/src/keyboardMappings.ts b/ui/src/keyboardMappings.ts index bb24fbb..1395a59 100644 --- a/ui/src/keyboardMappings.ts +++ b/ui/src/keyboardMappings.ts @@ -42,6 +42,7 @@ export const keys = { F10: 0x43, F11: 0x44, F12: 0x45, + F13: 0x68, F14: 0x69, F15: 0x6a, F16: 0x6b, diff --git a/ui/src/routes/devices.$id.settings.keyboard.tsx b/ui/src/routes/devices.$id.settings.keyboard.tsx index 57119ba..40c7c6f 100644 --- a/ui/src/routes/devices.$id.settings.keyboard.tsx +++ b/ui/src/routes/devices.$id.settings.keyboard.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo } from "react"; -import { KeyboardLedSync, useSettingsStore } from "@/hooks/stores"; +import { useSettingsStore } from "@/hooks/stores"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import notifications from "@/notifications"; import { SettingsPageHeader } from "@components/SettingsPageheader"; @@ -13,14 +13,10 @@ import { SettingsItem } from "./devices.$id.settings"; export default function SettingsKeyboardRoute() { const keyboardLayout = useSettingsStore(state => state.keyboardLayout); - const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); const showPressedKeys = useSettingsStore(state => state.showPressedKeys); const setKeyboardLayout = useSettingsStore( state => state.setKeyboardLayout, ); - const setKeyboardLedSync = useSettingsStore( - state => state.setKeyboardLedSync, - ); const setShowPressedKeys = useSettingsStore( state => state.setShowPressedKeys, ); @@ -33,11 +29,6 @@ export default function SettingsKeyboardRoute() { }, [keyboardLayout]); const layoutOptions = keyboardOptions(); - const ledSyncOptions = [ - { value: "auto", label: "Automatic" }, - { value: "browser", label: "Browser Only" }, - { value: "host", label: "Host Only" }, - ]; const [send] = useJsonRpc(); @@ -91,23 +82,6 @@ export default function SettingsKeyboardRoute() {

-
- { /* this menu item could be renamed to plain "Keyboard layout" in the future, when also the virtual keyboard layout mappings are being implemented */ } - - setKeyboardLedSync(e.target.value as KeyboardLedSync)} - options={ledSyncOptions} - /> - -
-
state.keyboardLedState); const setKeyboardLedState = useHidStore(state => state.setKeyboardLedState); - const setKeyboardLedStateSyncAvailable = useHidStore(state => state.setKeyboardLedStateSyncAvailable); + const keysDownState = useHidStore(state => state.keysDownState); + const setKeysDownState = useHidStore(state => state.setKeysDownState); const [hasUpdated, setHasUpdated] = useState(false); const { navigateTo } = useDeviceUiNavigation(); @@ -617,7 +619,12 @@ export default function KvmIdRoute() { const ledState = resp.params as KeyboardLedState; console.log("Setting keyboard led state", ledState); setKeyboardLedState(ledState); - setKeyboardLedStateSyncAvailable(true); + } + + if (resp.method === "keysDownState") { + const downState = resp.params as KeysDownState; + console.log("Setting key down state", downState); + setKeysDownState(downState); } if (resp.method === "otaState") { @@ -664,20 +671,29 @@ export default function KvmIdRoute() { send("getKeyboardLedState", {}, resp => { if ("error" in resp) { - // -32601 means the method is not supported - if (resp.error.code === -32601) { - setKeyboardLedStateSyncAvailable(false); - console.error("Failed to get keyboard led state, disabling sync", resp.error); - } else { - console.error("Failed to get keyboard led state", resp.error); - } + console.error("Failed to get keyboard led state", resp.error); return; } console.log("Keyboard led state", resp.result); setKeyboardLedState(resp.result as KeyboardLedState); - setKeyboardLedStateSyncAvailable(true); }); - }, [rpcDataChannel?.readyState, send, setKeyboardLedState, setKeyboardLedStateSyncAvailable, keyboardLedState]); + }, [rpcDataChannel?.readyState, send, setKeyboardLedState, keyboardLedState]); + + // request keyboard key down state from the device + useEffect(() => { + if (rpcDataChannel?.readyState !== "open") return; + if (keysDownState !== undefined) return; + console.log("Requesting keys down state"); + + send("getKeyDownState", {}, resp => { + if ("error" in resp) { + console.error("Failed to get key down state", resp.error); + return; + } + console.log("Keyboard key down state", resp.result); + setKeysDownState(resp.result as KeysDownState); + }); + }, [keysDownState, rpcDataChannel?.readyState, send, setKeysDownState]); // When the update is successful, we need to refresh the client javascript and show a success modal useEffect(() => { diff --git a/usb.go b/usb.go index f777f89..3cf6908 100644 --- a/usb.go +++ b/usb.go @@ -31,16 +31,26 @@ func initUsbGadget() { } }) + gadget.SetOnKeysDownChange(func(state usbgadget.KeysDownState) { + if currentSession != nil { + writeJSONRPCEvent("keysDownState", state, currentSession) + } + }) + // open the keyboard hid file to listen for keyboard events if err := gadget.OpenKeyboardHidFile(); err != nil { usbLogger.Error().Err(err).Msg("failed to open keyboard hid file") } } -func rpcKeyboardReport(modifier uint8, keys []uint8) error { +func rpcKeyboardReport(modifier byte, keys []byte) error { return gadget.KeyboardReport(modifier, keys) } +func rpcKeypressReport(key byte, press bool) (usbgadget.KeysDownState, error) { + return gadget.KeypressReport(key, press) +} + func rpcAbsMouseReport(x, y int, buttons uint8) error { return gadget.AbsMouseReport(x, y, buttons) } @@ -57,6 +67,10 @@ func rpcGetKeyboardLedState() (state usbgadget.KeyboardState) { return gadget.GetKeyboardState() } +func rpcGetKeysDownState() (state usbgadget.KeysDownState) { + return gadget.GetKeysDownState() +} + var usbState = "unknown" func rpcGetUSBState() (state string) { @@ -66,7 +80,7 @@ func rpcGetUSBState() (state string) { func triggerUSBStateUpdate() { go func() { if currentSession == nil { - usbLogger.Info().Msg("No active RPC session, skipping update state update") + usbLogger.Info().Msg("No active RPC session, skipping USB state update") return } writeJSONRPCEvent("usbState", usbState, currentSession) @@ -78,9 +92,9 @@ func checkUSBState() { if newState == usbState { return } + usbLogger.Info().Str("from", usbState).Str("to", newState).Msg("USB state changed") usbState = newState - usbLogger.Info().Str("from", usbState).Str("to", newState).Msg("USB state changed") requestDisplayUpdate(true) triggerUSBStateUpdate() } diff --git a/webrtc.go b/webrtc.go index f6c8529..f62a0f2 100644 --- a/webrtc.go +++ b/webrtc.go @@ -102,6 +102,7 @@ func newSession(config SessionConfig) (*Session, error) { ICEServers: []webrtc.ICEServer{iceServer}, }) if err != nil { + scopedLogger.Warn().Err(err).Msg("Failed to create PeerConnection") return nil, err } session := &Session{peerConnection: peerConnection} @@ -133,11 +134,13 @@ func newSession(config SessionConfig) (*Session, error) { session.VideoTrack, err = webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264}, "video", "kvm") if err != nil { + scopedLogger.Warn().Err(err).Msg("Failed to create VideoTrack") return nil, err } rtpSender, err := peerConnection.AddTrack(session.VideoTrack) if err != nil { + scopedLogger.Warn().Err(err).Msg("Failed to add VideoTrack to PeerConnection") return nil, err } @@ -187,8 +190,9 @@ func newSession(config SessionConfig) (*Session, error) { currentSession = nil } if session.shouldUmountVirtualMedia { - err := rpcUnmountImage() - scopedLogger.Warn().Err(err).Msg("unmount image failed on connection close") + if err := rpcUnmountImage(); err != nil { + scopedLogger.Warn().Err(err).Msg("unmount image failed on connection close") + } } if isConnected { isConnected = false From f129900f994df59a3f709c4ef13c6881680c6ecd Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 13 Aug 2025 01:32:20 -0500 Subject: [PATCH 3/3] Remove all the old school code --- ui/src/components/WebRTCVideo.tsx | 125 +++--------------------------- 1 file changed, 11 insertions(+), 114 deletions(-) diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 3878284..ec86149 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -9,9 +9,8 @@ import notifications from "@/notifications"; import useKeyboard from "@/hooks/useKeyboard"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import { cx } from "@/cva.config"; -import { keys, modifiers } from "@/keyboardMappings"; +import { keys } from "@/keyboardMappings"; import { - useHidStore, useMouseStore, useRTCStore, useSettingsStore, @@ -27,7 +26,6 @@ import { export default function WebRTCVideo() { // Video and stream related refs and states - const oldSchool = false;// useSettingsStore(state => state.oldSchoolKeyboard); const videoElm = useRef(null); const mediaStream = useRTCStore(state => state.mediaStream); const [isPlaying, setIsPlaying] = useState(false); @@ -35,7 +33,7 @@ export default function WebRTCVideo() { const [isPointerLockActive, setIsPointerLockActive] = useState(false); // Store hooks const settings = useSettingsStore(); - const { sendKeyboardEvent, sendKeypressEvent, resetKeyboardState } = useKeyboard(); + const { sendKeypressEvent, resetKeyboardState } = useKeyboard(); const setMousePosition = useMouseStore(state => state.setMousePosition); const setMouseMove = useMouseStore(state => state.setMouseMove); const { @@ -333,76 +331,9 @@ export default function WebRTCVideo() { sendAbsMouseMovement(0, 0, 0); }, [sendAbsMouseMovement]); - // Keyboard-related - const handleModifierKeys = useCallback( - (e: KeyboardEvent, activeModifiers: number[]) => { - const { shiftKey, ctrlKey, altKey, metaKey } = e; - - const filteredModifiers = activeModifiers.filter(Boolean); - - // Example: activeModifiers = [0x01, 0x02, 0x04, 0x08] - // Assuming 0x01 = ControlLeft, 0x02 = ShiftLeft, 0x04 = AltLeft, 0x08 = MetaLeft - return ( - filteredModifiers - // Shift: Keep if Shift is pressed or if the key isn't a Shift key - // Example: If shiftKey is true, keep all modifiers - // If shiftKey is false, filter out 0x02 (ShiftLeft) and 0x20 (ShiftRight) - .filter( - modifier => - shiftKey || - (modifier !== modifiers["ShiftLeft"] && - modifier !== modifiers["ShiftRight"]), - ) - // Ctrl: Keep if Ctrl is pressed or if the key isn't a Ctrl key - // Example: If ctrlKey is true, keep all modifiers - // If ctrlKey is false, filter out 0x01 (ControlLeft) and 0x10 (ControlRight) - .filter( - modifier => - ctrlKey || - (modifier !== modifiers["ControlLeft"] && - modifier !== modifiers["ControlRight"]), - ) - // Alt: Keep if Alt is pressed or if the key isn't an Alt key - // Example: If altKey is true, keep all modifiers - // If altKey is false, filter out 0x04 (AltLeft) - // - // But intentionally do not filter out 0x40 (AltRight) to accomodate - // Alt Gr (Alt Graph) as a modifier. Oddly, Alt Gr does not declare - // itself to be an altKey. For example, the KeyboardEvent for - // Alt Gr + 2 has the following structure: - // - altKey: false - // - code: "Digit2" - // - type: [ "keydown" | "keyup" ] - // - // For context, filteredModifiers aims to keep track which modifiers - // are being pressed on the physical keyboard at any point in time. - // There is logic in the keyUpHandler and keyDownHandler to add and - // remove 0x40 (AltRight) from the list of new modifiers. - // - // But relying on the two handlers alone to track the state of the - // modifier bears the risk that the key up event for Alt Gr could - // get lost while the browser window is temporarily out of focus, - // which means the Alt Gr key state would then be "stuck". At this - // point, we would need to rely on the user to press Alt Gr again - // to properly release the state of that modifier. - .filter(modifier => altKey || modifier !== modifiers["AltLeft"]) - // Meta: Keep if Meta is pressed or if the key isn't a Meta key - // Example: If metaKey is true, keep all modifiers - // If metaKey is false, filter out 0x08 (MetaLeft) and 0x80 (MetaRight) - .filter( - modifier => - metaKey || - (modifier !== modifiers["MetaLeft"] && modifier !== modifiers["MetaRight"]), - ) - ); - }, - [], - ); - const keyDownHandler = useCallback( async (e: KeyboardEvent) => { e.preventDefault(); - const prev = useHidStore.getState(); const code = getAdjustedKeyCode(e); const hidKey = keys[code]; @@ -410,59 +341,25 @@ export default function WebRTCVideo() { // event, so we need to clear the keys after a short delay // https://bugs.chromium.org/p/chromium/issues/detail?id=28089 // https://bugzilla.mozilla.org/show_bug.cgi?id=1299553 - - if (oldSchool) { - // Add the modifier to the active modifiers - const newModifiers = handleModifierKeys(e, [ - ...prev.activeModifiers, - modifiers[code], - ]); - - // Add the key to the active keys - const newKeys = [...prev.activeKeys, hidKey].filter(Boolean); - - if (e.metaKey) { - setTimeout((activeModifiers: number[]) => { - // TODO this should probably be passing prev.activeKeys not an empty array - sendKeyboardEvent([], newModifiers || activeModifiers); - }, 10, prev.activeModifiers); - } - sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); - } - else { - if (e.metaKey) { - setTimeout(() => { - sendKeypressEvent(hidKey, false); - }, 10); - } - sendKeypressEvent(hidKey, true); + if (e.metaKey) { + setTimeout(() => { + sendKeypressEvent(hidKey, false); + }, 10); } + sendKeypressEvent(hidKey, true); + }, - [handleModifierKeys, oldSchool, sendKeyboardEvent, sendKeypressEvent], + [sendKeypressEvent], ); const keyUpHandler = useCallback( async (e: KeyboardEvent) => { e.preventDefault(); - const prev = useHidStore.getState(); const code = getAdjustedKeyCode(e); const hidKey = keys[code]; - - if (oldSchool) { - // Filter out the modifier that was just released - const newModifiers = handleModifierKeys( - e, - prev.activeModifiers.filter(k => k !== modifiers[code]), - ); - - // Filtering out the key that was just released (keys[e.code]) - const newKeys = prev.activeKeys.filter(k => k !== hidKey).filter(Boolean); - sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); - } else { - sendKeypressEvent(hidKey, false); - } + sendKeypressEvent(hidKey, false); }, - [handleModifierKeys, oldSchool, sendKeyboardEvent, sendKeypressEvent], + [sendKeypressEvent], ); const videoKeyUpHandler = useCallback((e: KeyboardEvent) => {