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)
This commit is contained in:
Marc Brooks 2025-08-07 06:23:35 +00:00
parent 608f69db13
commit 93c5887153
No known key found for this signature in database
GPG Key ID: 583A6AF2D6AE1DC6
15 changed files with 570 additions and 434 deletions

View File

@ -1,10 +1,10 @@
package usbgadget package usbgadget
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"os" "os"
"reflect"
"time" "time"
) )
@ -61,6 +61,8 @@ var keyboardReportDesc = []byte{
const ( const (
hidReadBufferSize = 8 hidReadBufferSize = 8
hidKeyBufferSize = 6
hidErrorRollOver = 0x01
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf // https://www.usb.org/sites/default/files/documents/hid1_11.pdf
// https://www.usb.org/sites/default/files/hut1_2.pdf // https://www.usb.org/sites/default/files/hut1_2.pdf
KeyboardLedMaskNumLock = 1 << 0 KeyboardLedMaskNumLock = 1 << 0
@ -68,7 +70,9 @@ const (
KeyboardLedMaskScrollLock = 1 << 2 KeyboardLedMaskScrollLock = 1 << 2
KeyboardLedMaskCompose = 1 << 3 KeyboardLedMaskCompose = 1 << 3
KeyboardLedMaskKana = 1 << 4 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, // Synchronization between LED states and CAPS LOCK, NUM LOCK, SCROLL LOCK,
@ -81,6 +85,7 @@ type KeyboardState struct {
ScrollLock bool `json:"scroll_lock"` ScrollLock bool `json:"scroll_lock"`
Compose bool `json:"compose"` Compose bool `json:"compose"`
Kana bool `json:"kana"` Kana bool `json:"kana"`
Shift bool `json:"shift"` // This is not part of the main USB HID spec
} }
func getKeyboardState(b byte) KeyboardState { func getKeyboardState(b byte) KeyboardState {
@ -91,27 +96,27 @@ func getKeyboardState(b byte) KeyboardState {
ScrollLock: b&KeyboardLedMaskScrollLock != 0, ScrollLock: b&KeyboardLedMaskScrollLock != 0,
Compose: b&KeyboardLedMaskCompose != 0, Compose: b&KeyboardLedMaskCompose != 0,
Kana: b&KeyboardLedMaskKana != 0, Kana: b&KeyboardLedMaskKana != 0,
Shift: b&KeyboardLedMaskShift != 0,
} }
} }
func (u *UsbGadget) updateKeyboardState(b byte) { func (u *UsbGadget) updateKeyboardState(state byte) {
u.keyboardStateLock.Lock() u.keyboardStateLock.Lock()
defer u.keyboardStateLock.Unlock() defer u.keyboardStateLock.Unlock()
if b&^ValidKeyboardLedMasks != 0 { if state&^ValidKeyboardLedMasks != 0 {
u.log.Trace().Uint8("b", b).Msg("contains invalid bits, ignoring") u.log.Error().Uint8("state", state).Msg("ignoring invalid bits")
return return
} }
newState := getKeyboardState(b) if u.keyboardState == state {
if reflect.DeepEqual(u.keyboardState, newState) {
return return
} }
u.log.Info().Interface("old", u.keyboardState).Interface("new", newState).Msg("keyboardState updated") u.log.Trace().Interface("old", u.keyboardState).Interface("new", state).Msg("keyboardState updated")
u.keyboardState = newState u.keyboardState = state
if u.onKeyboardStateChange != nil { if u.onKeyboardStateChange != nil {
(*u.onKeyboardStateChange)(newState) (*u.onKeyboardStateChange)(getKeyboardState(state))
} }
} }
@ -123,7 +128,52 @@ func (u *UsbGadget) GetKeyboardState() KeyboardState {
u.keyboardStateLock.Lock() u.keyboardStateLock.Lock()
defer u.keyboardStateLock.Unlock() 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() { func (u *UsbGadget) listenKeyboardEvents() {
@ -142,7 +192,7 @@ func (u *UsbGadget) listenKeyboardEvents() {
l.Info().Msg("context done") l.Info().Msg("context done")
return return
default: default:
l.Trace().Msg("reading from keyboard") l.Trace().Msg("reading from keyboard for LED state changes")
if u.keyboardHidFile == nil { if u.keyboardHidFile == nil {
u.logWithSuppression("keyboardHidFileNil", 100, &l, nil, "keyboardHidFile is nil") u.logWithSuppression("keyboardHidFileNil", 100, &l, nil, "keyboardHidFile is nil")
// show the error every 100 times to avoid spamming the logs // show the error every 100 times to avoid spamming the logs
@ -195,12 +245,12 @@ func (u *UsbGadget) OpenKeyboardHidFile() error {
return u.openKeyboardHidFile() 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 { if err := u.openKeyboardHidFile(); err != nil {
return err return err
} }
_, err := u.keyboardHidFile.Write(data) _, err := u.keyboardHidFile.Write(append([]byte{modifier, 0x00}, keys[:]...))
if err != nil { if err != nil {
u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0") u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
u.keyboardHidFile.Close() u.keyboardHidFile.Close()
@ -211,22 +261,116 @@ func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
return nil return nil
} }
func (u *UsbGadget) KeyboardReport(modifier uint8, keys []uint8) error { func (u *UsbGadget) KeyboardReport(modifier byte, keys []byte) error {
u.keyboardLock.Lock() u.keyboardLock.Lock()
defer u.keyboardLock.Unlock() defer u.keyboardLock.Unlock()
defer u.resetUserInputTime()
if len(keys) > 6 { if len(keys) > hidKeyBufferSize {
keys = keys[:6] keys = keys[:hidKeyBufferSize]
} }
if len(keys) < 6 { if len(keys) < hidKeyBufferSize {
keys = append(keys, make([]uint8, 6-len(keys))...) 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]}) return u.keyboardWriteHidFile(modifier, keys)
if err != nil { }
return err
} const (
// https://www.usb.org/sites/default/files/documents/hut1_2.pdf
u.resetUserInputTime() // Dynamic Flags (DV)
return nil 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
} }

View File

@ -41,6 +41,11 @@ var defaultUsbGadgetDevices = Devices{
MassStorage: true, MassStorage: true,
} }
type KeysDownState struct {
Modifier byte `json:"modifier"`
Keys []byte `json:"keys"`
}
// UsbGadget is a struct that represents a USB gadget. // UsbGadget is a struct that represents a USB gadget.
type UsbGadget struct { type UsbGadget struct {
name string name string
@ -60,7 +65,9 @@ type UsbGadget struct {
relMouseHidFile *os.File relMouseHidFile *os.File
relMouseLock sync.Mutex 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 keyboardStateLock sync.Mutex
keyboardStateCtx context.Context keyboardStateCtx context.Context
keyboardStateCancel context.CancelFunc keyboardStateCancel context.CancelFunc
@ -77,6 +84,7 @@ type UsbGadget struct {
txLock sync.Mutex txLock sync.Mutex
onKeyboardStateChange *func(state KeyboardState) onKeyboardStateChange *func(state KeyboardState)
onKeysDownChange *func(state KeysDownState)
log *zerolog.Logger log *zerolog.Logger
@ -122,7 +130,8 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev
txLock: sync.Mutex{}, txLock: sync.Mutex{},
keyboardStateCtx: keyboardCtx, keyboardStateCtx: keyboardCtx,
keyboardStateCancel: keyboardCancel, keyboardStateCancel: keyboardCancel,
keyboardState: KeyboardState{}, keyboardState: 0,
keysDownState: KeysDownState{Modifier: 0, Keys: make([]byte, hidKeyBufferSize)},
enabledDevices: *enabledDevices, enabledDevices: *enabledDevices,
lastUserInput: time.Now(), lastUserInput: time.Now(),
log: logger, log: logger,

View File

@ -1047,6 +1047,8 @@ var rpcHandlers = map[string]RPCHandler{
"renewDHCPLease": {Func: rpcRenewDHCPLease}, "renewDHCPLease": {Func: rpcRenewDHCPLease},
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
"getKeyboardLedState": {Func: rpcGetKeyboardLedState}, "getKeyboardLedState": {Func: rpcGetKeyboardLedState},
"keypressReport": {Func: rpcKeypressReport, Params: []string{"key", "press"}},
"getKeyDownState": {Func: rpcGetKeysDownState},
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},

View File

@ -66,6 +66,10 @@ module.exports = defineConfig([{
groups: ["builtin", "external", "internal", "parent", "sibling"], groups: ["builtin", "external", "internal", "parent", "sibling"],
"newlines-between": "always", "newlines-between": "always",
}], }],
"@typescript-eslint/no-unused-vars": ["warn", {
"argsIgnorePattern": "^_", "varsIgnorePattern": "^_"
}],
}, },
settings: { settings: {

426
ui/package-lock.json generated
View File

@ -112,9 +112,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
"integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -128,9 +128,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
"integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -144,9 +144,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
"integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -160,9 +160,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
"integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -176,9 +176,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
"integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -192,9 +192,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
"integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -208,9 +208,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
"integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -224,9 +224,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
"integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -240,9 +240,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
"integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -256,9 +256,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
"integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -272,9 +272,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
"integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -288,9 +288,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
"integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -304,9 +304,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
"integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@ -320,9 +320,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
"integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -336,9 +336,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
"integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -352,9 +352,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
"integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -368,9 +368,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
"integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -384,9 +384,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-arm64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
"integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -400,9 +400,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
"integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -416,9 +416,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
"integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -432,9 +432,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
"integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -448,9 +448,9 @@
} }
}, },
"node_modules/@esbuild/openharmony-arm64": { "node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
"integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -464,9 +464,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
"integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -480,9 +480,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
"integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -496,9 +496,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
"integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -512,9 +512,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
"integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -555,9 +555,9 @@
} }
}, },
"node_modules/@eslint/compat": { "node_modules/@eslint/compat": {
"version": "1.3.1", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.2.tgz",
"integrity": "sha512-k8MHony59I5EPic6EQTCNOuPoVBnoYXkP+20xvwFjN7t0qI3ImyvyBgg+hIVPwC8JaxVjjUZld+cLfBLFDLucg==", "integrity": "sha512-jRNwzTbd6p2Rw4sZ1CgWRS8YMtqG15YyZf7zvb6gY2rB2u6n+2Z+ELW0GtL0fQgyl0pr4Y/BzBfng/BdsereRA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@ -587,18 +587,18 @@
} }
}, },
"node_modules/@eslint/config-helpers": { "node_modules/@eslint/config-helpers": {
"version": "0.3.0", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz",
"integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@eslint/core": { "node_modules/@eslint/core": {
"version": "0.15.1", "version": "0.15.2",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@types/json-schema": "^7.0.15" "@types/json-schema": "^7.0.15"
@ -643,9 +643,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.32.0", "version": "9.33.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz",
"integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -664,12 +664,12 @@
} }
}, },
"node_modules/@eslint/plugin-kit": { "node_modules/@eslint/plugin-kit": {
"version": "0.3.4", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
"integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@eslint/core": "^0.15.1", "@eslint/core": "^0.15.2",
"levn": "^0.4.1" "levn": "^0.4.1"
}, },
"engines": { "engines": {
@ -845,9 +845,9 @@
} }
}, },
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.12", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -866,16 +866,16 @@
} }
}, },
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.4", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.29", "version": "0.3.30",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1964,9 +1964,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.1.9", "version": "19.1.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
@ -1996,17 +1996,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.39.0", "version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz",
"integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", "integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/scope-manager": "8.39.1",
"@typescript-eslint/type-utils": "8.39.0", "@typescript-eslint/type-utils": "8.39.1",
"@typescript-eslint/utils": "8.39.0", "@typescript-eslint/utils": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.1",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^7.0.0", "ignore": "^7.0.0",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@ -2020,7 +2020,7 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/parser": "^8.39.0", "@typescript-eslint/parser": "^8.39.1",
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0" "typescript": ">=4.8.4 <6.0.0"
} }
@ -2036,16 +2036,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.39.0", "version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz",
"integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/scope-manager": "8.39.1",
"@typescript-eslint/types": "8.39.0", "@typescript-eslint/types": "8.39.1",
"@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.1",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@ -2061,14 +2061,14 @@
} }
}, },
"node_modules/@typescript-eslint/project-service": { "node_modules/@typescript-eslint/project-service": {
"version": "8.39.0", "version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz",
"integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.39.0", "@typescript-eslint/tsconfig-utils": "^8.39.1",
"@typescript-eslint/types": "^8.39.0", "@typescript-eslint/types": "^8.39.1",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@ -2083,14 +2083,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.39.0", "version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz",
"integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.39.0", "@typescript-eslint/types": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.0" "@typescript-eslint/visitor-keys": "8.39.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -2101,9 +2101,9 @@
} }
}, },
"node_modules/@typescript-eslint/tsconfig-utils": { "node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.39.0", "version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz",
"integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2118,15 +2118,15 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.39.0", "version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz",
"integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", "integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.39.0", "@typescript-eslint/types": "8.39.1",
"@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.1",
"@typescript-eslint/utils": "8.39.0", "@typescript-eslint/utils": "8.39.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^2.1.0" "ts-api-utils": "^2.1.0"
}, },
@ -2143,9 +2143,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.39.0", "version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
"integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2157,16 +2157,16 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.39.0", "version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz",
"integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/project-service": "8.39.0", "@typescript-eslint/project-service": "8.39.1",
"@typescript-eslint/tsconfig-utils": "8.39.0", "@typescript-eslint/tsconfig-utils": "8.39.1",
"@typescript-eslint/types": "8.39.0", "@typescript-eslint/types": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@ -2212,16 +2212,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.39.0", "version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz",
"integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.7.0", "@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/scope-manager": "8.39.1",
"@typescript-eslint/types": "8.39.0", "@typescript-eslint/types": "8.39.1",
"@typescript-eslint/typescript-estree": "8.39.0" "@typescript-eslint/typescript-estree": "8.39.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -2236,13 +2236,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.39.0", "version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz",
"integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.39.0", "@typescript-eslint/types": "8.39.1",
"eslint-visitor-keys": "^4.2.1" "eslint-visitor-keys": "^4.2.1"
}, },
"engines": { "engines": {
@ -2650,9 +2650,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.25.1", "version": "4.25.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz",
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -2670,8 +2670,8 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001726", "caniuse-lite": "^1.0.30001733",
"electron-to-chromium": "^1.5.173", "electron-to-chromium": "^1.5.199",
"node-releases": "^2.0.19", "node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.3" "update-browserslist-db": "^1.1.3"
}, },
@ -2739,9 +2739,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001731", "version": "1.0.30001734",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz",
"integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -3159,9 +3159,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.198", "version": "1.5.200",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.198.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.200.tgz",
"integrity": "sha512-G5COfnp3w+ydVu80yprgWSfmfQaYRh9DOxfhAxstLyetKaLyl55QrNjx8C38Pc/C+RaDmb1M0Lk8wPEMQ+bGgQ==", "integrity": "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -3350,9 +3350,9 @@
} }
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.8", "version": "0.25.9",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
"integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@ -3362,32 +3362,32 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.8", "@esbuild/aix-ppc64": "0.25.9",
"@esbuild/android-arm": "0.25.8", "@esbuild/android-arm": "0.25.9",
"@esbuild/android-arm64": "0.25.8", "@esbuild/android-arm64": "0.25.9",
"@esbuild/android-x64": "0.25.8", "@esbuild/android-x64": "0.25.9",
"@esbuild/darwin-arm64": "0.25.8", "@esbuild/darwin-arm64": "0.25.9",
"@esbuild/darwin-x64": "0.25.8", "@esbuild/darwin-x64": "0.25.9",
"@esbuild/freebsd-arm64": "0.25.8", "@esbuild/freebsd-arm64": "0.25.9",
"@esbuild/freebsd-x64": "0.25.8", "@esbuild/freebsd-x64": "0.25.9",
"@esbuild/linux-arm": "0.25.8", "@esbuild/linux-arm": "0.25.9",
"@esbuild/linux-arm64": "0.25.8", "@esbuild/linux-arm64": "0.25.9",
"@esbuild/linux-ia32": "0.25.8", "@esbuild/linux-ia32": "0.25.9",
"@esbuild/linux-loong64": "0.25.8", "@esbuild/linux-loong64": "0.25.9",
"@esbuild/linux-mips64el": "0.25.8", "@esbuild/linux-mips64el": "0.25.9",
"@esbuild/linux-ppc64": "0.25.8", "@esbuild/linux-ppc64": "0.25.9",
"@esbuild/linux-riscv64": "0.25.8", "@esbuild/linux-riscv64": "0.25.9",
"@esbuild/linux-s390x": "0.25.8", "@esbuild/linux-s390x": "0.25.9",
"@esbuild/linux-x64": "0.25.8", "@esbuild/linux-x64": "0.25.9",
"@esbuild/netbsd-arm64": "0.25.8", "@esbuild/netbsd-arm64": "0.25.9",
"@esbuild/netbsd-x64": "0.25.8", "@esbuild/netbsd-x64": "0.25.9",
"@esbuild/openbsd-arm64": "0.25.8", "@esbuild/openbsd-arm64": "0.25.9",
"@esbuild/openbsd-x64": "0.25.8", "@esbuild/openbsd-x64": "0.25.9",
"@esbuild/openharmony-arm64": "0.25.8", "@esbuild/openharmony-arm64": "0.25.9",
"@esbuild/sunos-x64": "0.25.8", "@esbuild/sunos-x64": "0.25.9",
"@esbuild/win32-arm64": "0.25.8", "@esbuild/win32-arm64": "0.25.9",
"@esbuild/win32-ia32": "0.25.8", "@esbuild/win32-ia32": "0.25.9",
"@esbuild/win32-x64": "0.25.8" "@esbuild/win32-x64": "0.25.9"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
@ -3413,19 +3413,19 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.32.0", "version": "9.33.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz",
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.21.0", "@eslint/config-array": "^0.21.0",
"@eslint/config-helpers": "^0.3.0", "@eslint/config-helpers": "^0.3.1",
"@eslint/core": "^0.15.0", "@eslint/core": "^0.15.2",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.32.0", "@eslint/js": "9.33.0",
"@eslint/plugin-kit": "^0.3.4", "@eslint/plugin-kit": "^0.3.5",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2", "@humanwhocodes/retry": "^0.4.2",
@ -4747,9 +4747,9 @@
} }
}, },
"node_modules/js-base64": { "node_modules/js-base64": {
"version": "3.7.7", "version": "3.7.8",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz",
"integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/js-tokens": { "node_modules/js-tokens": {
@ -5851,9 +5851,9 @@
} }
}, },
"node_modules/react-simple-keyboard": { "node_modules/react-simple-keyboard": {
"version": "3.8.106", "version": "3.8.108",
"resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.106.tgz", "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.108.tgz",
"integrity": "sha512-ItCHCdhVCzn9huhenuyuHQMOGsl3UMLu5xAO1bkjj4AAgVoktFC1DQ4HWkOS6BGPvUJejFM3Q5hVM8Bl2oX9pA==", "integrity": "sha512-Q3JK/qnaDjTMaE6EcNOG4MJV8jHqug2K1YIz9j+QXgmAE/nUuHXuyAnLIZuU3uvqv2n/556l2hLkqr9jW1LgUw==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",

View File

@ -38,9 +38,6 @@ export default function InfoBar() {
}, [rpcDataChannel]); }, [rpcDataChannel]);
const keyboardLedState = useHidStore(state => state.keyboardLedState); 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 isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse);
const usbState = useHidStore(state => state.usbState); const usbState = useHidStore(state => state.usbState);
@ -122,19 +119,6 @@ export default function InfoBar() {
</div> </div>
)} )}
{keyboardLedStateSyncAvailable ? (
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
keyboardLedSync !== "browser"
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
title={"Your keyboard LED state is managed by" + (keyboardLedSync === "browser" ? " the browser" : " the host")}
>
{keyboardLedSync === "browser" ? "Browser" : "Host"}
</div>
) : null}
<div <div
className={cx( className={cx(
"shrink-0 p-1 px-1.5 text-xs", "shrink-0 p-1 px-1.5 text-xs",
@ -175,6 +159,11 @@ export default function InfoBar() {
Kana Kana
</div> </div>
) : null} ) : null}
{keyboardLedState?.shift ? (
<div className="shrink-0 p-1 px-1.5 text-xs">
Shift
</div>
) : null}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
import { ChevronDownIcon } from "@heroicons/react/16/solid"; import { ChevronDownIcon } from "@heroicons/react/16/solid";
import { AnimatePresence, motion } from "framer-motion"; 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 Keyboard from "react-simple-keyboard";
import Card from "@components/Card"; 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 AttachIconRaw from "@/assets/attach-icon.svg";
import DetachIconRaw from "@/assets/detach-icon.svg"; import DetachIconRaw from "@/assets/detach-icon.svg";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores"; import { useHidStore, useUiStore } from "@/hooks/stores";
import useKeyboard from "@/hooks/useKeyboard"; import useKeyboard from "@/hooks/useKeyboard";
import { keyDisplayMap, keys, modifiers } from "@/keyboardMappings"; import { keyDisplayMap, keys, modifiers } from "@/keyboardMappings";
@ -44,15 +44,19 @@ function KeyboardWrapper() {
const isCapsLockActive = useHidStore(useShallow(state => state.keyboardLedState?.caps_lock)); const isCapsLockActive = useHidStore(useShallow(state => state.keyboardLedState?.caps_lock));
// HID related states /*
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable); // These will be used to display the currently pressed keys and modifiers on the virtual keyboard
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
const isKeyboardLedManagedByHost = useMemo(() =>
keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable,
[keyboardLedSync, keyboardLedStateSyncAvailable],
);
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) => { const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
if (!keyboardRef.current) return; if (!keyboardRef.current) return;
@ -168,19 +172,11 @@ function KeyboardWrapper() {
toggleLayout(); toggleLayout();
if (isCapsLockActive) { if (isCapsLockActive) {
if (!isKeyboardLedManagedByHost) {
setIsCapsLockActive(false);
}
sendKeyboardEvent([keys["CapsLock"]], []); sendKeyboardEvent([keys["CapsLock"]], []);
return; return;
} }
} }
// Handle caps lock state change
if (isKeyCaps && !isKeyboardLedManagedByHost) {
setIsCapsLockActive(!isCapsLockActive);
}
// Collect new active keys and modifiers // Collect new active keys and modifiers
const newKeys = keys[cleanKey] ? [keys[cleanKey]] : []; const newKeys = keys[cleanKey] ? [keys[cleanKey]] : [];
const newModifiers = const newModifiers =
@ -196,7 +192,7 @@ function KeyboardWrapper() {
setTimeout(resetKeyboardState, 100); setTimeout(resetKeyboardState, 100);
}, },
[isCapsLockActive, isKeyboardLedManagedByHost, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive], [isCapsLockActive, sendKeyboardEvent, resetKeyboardState],
); );
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled); const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);

View File

@ -27,6 +27,7 @@ import {
export default function WebRTCVideo() { export default function WebRTCVideo() {
// Video and stream related refs and states // Video and stream related refs and states
const oldSchool = false;// useSettingsStore(state => state.oldSchoolKeyboard);
const videoElm = useRef<HTMLVideoElement>(null); const videoElm = useRef<HTMLVideoElement>(null);
const mediaStream = useRTCStore(state => state.mediaStream); const mediaStream = useRTCStore(state => state.mediaStream);
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
@ -34,7 +35,7 @@ export default function WebRTCVideo() {
const [isPointerLockActive, setIsPointerLockActive] = useState(false); const [isPointerLockActive, setIsPointerLockActive] = useState(false);
// Store hooks // Store hooks
const settings = useSettingsStore(); const settings = useSettingsStore();
const { sendKeyboardEvent, resetKeyboardState } = useKeyboard(); const { sendKeyboardEvent, sendKeypressEvent, resetKeyboardState } = useKeyboard();
const setMousePosition = useMouseStore(state => state.setMousePosition); const setMousePosition = useMouseStore(state => state.setMousePosition);
const setMouseMove = useMouseStore(state => state.setMouseMove); const setMouseMove = useMouseStore(state => state.setMouseMove);
const { const {
@ -51,18 +52,6 @@ export default function WebRTCVideo() {
const videoBrightness = useSettingsStore(state => state.videoBrightness); const videoBrightness = useSettingsStore(state => state.videoBrightness);
const videoContrast = useSettingsStore(state => state.videoContrast); 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 // RTC related states
const peerConnection = useRTCStore(state => state.peerConnection); const peerConnection = useRTCStore(state => state.peerConnection);
@ -414,83 +403,66 @@ export default function WebRTCVideo() {
async (e: KeyboardEvent) => { async (e: KeyboardEvent) => {
e.preventDefault(); e.preventDefault();
const prev = useHidStore.getState(); const prev = useHidStore.getState();
let code = e.code; const code = getAdjustedKeyCode(e);
const key = e.key; const hidKey = keys[code];
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],
]);
// When pressing the meta key + another key, the key will never trigger a keyup // 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 // event, so we need to clear the keys after a short delay
// https://bugs.chromium.org/p/chromium/issues/detail?id=28089 // https://bugs.chromium.org/p/chromium/issues/detail?id=28089
// https://bugzilla.mozilla.org/show_bug.cgi?id=1299553 // https://bugzilla.mozilla.org/show_bug.cgi?id=1299553
if (e.metaKey) {
setTimeout(() => { if (oldSchool) {
const prev = useHidStore.getState(); // Add the modifier to the active modifiers
sendKeyboardEvent([], newModifiers || prev.activeModifiers); const newModifiers = handleModifierKeys(e, [
}, 10); ...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, oldSchool, sendKeyboardEvent, sendKeypressEvent],
handleModifierKeys,
sendKeyboardEvent,
isKeyboardLedManagedByHost,
setIsNumLockActive,
setIsCapsLockActive,
setIsScrollLockActive,
],
); );
const keyUpHandler = useCallback( const keyUpHandler = useCallback(
(e: KeyboardEvent) => { async (e: KeyboardEvent) => {
e.preventDefault(); e.preventDefault();
const prev = useHidStore.getState(); const prev = useHidStore.getState();
const code = getAdjustedKeyCode(e);
const hidKey = keys[code];
if (!isKeyboardLedManagedByHost) { if (oldSchool) {
setIsNumLockActive(e.getModifierState("NumLock")); // Filter out the modifier that was just released
setIsCapsLockActive(e.getModifierState("CapsLock")); const newModifiers = handleModifierKeys(
setIsScrollLockActive(e.getModifierState("ScrollLock")); 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, oldSchool, sendKeyboardEvent, sendKeypressEvent],
handleModifierKeys,
sendKeyboardEvent,
isKeyboardLedManagedByHost,
setIsNumLockActive,
setIsCapsLockActive,
setIsScrollLockActive,
],
); );
const videoKeyUpHandler = useCallback((e: KeyboardEvent) => { const videoKeyUpHandler = useCallback((e: KeyboardEvent) => {
@ -667,6 +639,18 @@ export default function WebRTCVideo() {
}; };
}, [videoSaturation, videoBrightness, videoContrast]); }, [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 ( return (
<div className="grid h-full w-full grid-rows-(--grid-layout)"> <div className="grid h-full w-full grid-rows-(--grid-layout)">
<div className="flex min-h-[39.5px] flex-col"> <div className="flex min-h-[39.5px] flex-col">

View File

@ -283,8 +283,6 @@ export const useVideoStore = create<VideoState>(set => ({
}, },
})); }));
export type KeyboardLedSync = "auto" | "browser" | "host";
interface SettingsState { interface SettingsState {
isCursorHidden: boolean; isCursorHidden: boolean;
setCursorVisibility: (enabled: boolean) => void; setCursorVisibility: (enabled: boolean) => void;
@ -308,9 +306,6 @@ interface SettingsState {
keyboardLayout: string; keyboardLayout: string;
setKeyboardLayout: (layout: string) => void; setKeyboardLayout: (layout: string) => void;
keyboardLedSync: KeyboardLedSync;
setKeyboardLedSync: (sync: KeyboardLedSync) => void;
scrollThrottling: number; scrollThrottling: number;
setScrollThrottling: (value: number) => void; setScrollThrottling: (value: number) => void;
@ -356,9 +351,6 @@ export const useSettingsStore = create(
keyboardLayout: "en-US", keyboardLayout: "en-US",
setKeyboardLayout: layout => set({ keyboardLayout: layout }), setKeyboardLayout: layout => set({ keyboardLayout: layout }),
keyboardLedSync: "auto",
setKeyboardLedSync: sync => set({ keyboardLedSync: sync }),
scrollThrottling: 0, scrollThrottling: 0,
setScrollThrottling: value => set({ scrollThrottling: value }), setScrollThrottling: value => set({ scrollThrottling: value }),
@ -436,14 +428,13 @@ export interface KeyboardLedState {
scroll_lock: boolean; scroll_lock: boolean;
compose: boolean; compose: boolean;
kana: boolean; kana: boolean;
shift: boolean; // Optional, as not all keyboards have a shift LED
}; };
const defaultKeyboardLedState: KeyboardLedState = {
num_lock: false, export interface KeysDownState {
caps_lock: false, modifier: number;
scroll_lock: false, keys: number[];
compose: false, }
kana: false,
};
export interface HidState { export interface HidState {
activeKeys: number[]; activeKeys: number[];
@ -465,12 +456,9 @@ export interface HidState {
keyboardLedState?: KeyboardLedState; keyboardLedState?: KeyboardLedState;
setKeyboardLedState: (state: KeyboardLedState) => void; setKeyboardLedState: (state: KeyboardLedState) => void;
setIsNumLockActive: (active: boolean) => void;
setIsCapsLockActive: (active: boolean) => void;
setIsScrollLockActive: (active: boolean) => void;
keyboardLedStateSyncAvailable: boolean; keysDownState?: KeysDownState;
setKeyboardLedStateSyncAvailable: (available: boolean) => void; setKeysDownState: (state: KeysDownState) => void;
isVirtualKeyboardEnabled: boolean; isVirtualKeyboardEnabled: boolean;
setVirtualKeyboardEnabled: (enabled: boolean) => void; setVirtualKeyboardEnabled: (enabled: boolean) => void;
@ -482,9 +470,10 @@ export interface HidState {
setUsbState: (state: HidState["usbState"]) => void; setUsbState: (state: HidState["usbState"]) => void;
} }
export const useHidStore = create<HidState>((set, get) => ({ export const useHidStore = create<HidState>((set) => ({
activeKeys: [], activeKeys: [],
activeModifiers: [], activeModifiers: [],
updateActiveKeysAndModifiers: ({ keys, modifiers }) => { updateActiveKeysAndModifiers: ({ keys, modifiers }) => {
return set({ activeKeys: keys, activeModifiers: modifiers }); return set({ activeKeys: keys, activeModifiers: modifiers });
}, },
@ -498,25 +487,11 @@ export const useHidStore = create<HidState>((set, get) => ({
altGrCtrlTime: 0, altGrCtrlTime: 0,
setAltGrCtrlTime: time => set({ altGrCtrlTime: time }), setAltGrCtrlTime: time => set({ altGrCtrlTime: time }),
keyboardLedState: undefined,
setKeyboardLedState: ledState => set({ keyboardLedState: ledState }), 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, keysDownState: undefined,
setKeyboardLedStateSyncAvailable: available => set({ keyboardLedStateSyncAvailable: available }), setKeysDownState: state => set({ keysDownState: state }),
isVirtualKeyboardEnabled: false, isVirtualKeyboardEnabled: false,
setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }), setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }),

View File

@ -1,6 +1,6 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useHidStore, useRTCStore } from "@/hooks/stores"; import { KeysDownState, useHidStore, useRTCStore } from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc"; import { useJsonRpc } from "@/hooks/useJsonRpc";
import { keys, modifiers } from "@/keyboardMappings"; import { keys, modifiers } from "@/keyboardMappings";
@ -25,6 +25,30 @@ export default function useKeyboard() {
[rpcDataChannel?.readyState, send, updateActiveKeysAndModifiers], [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(() => { const resetKeyboardState = useCallback(() => {
sendKeyboardEvent([], []); sendKeyboardEvent([], []);
}, [sendKeyboardEvent]); }, [sendKeyboardEvent]);
@ -52,5 +76,5 @@ export default function useKeyboard() {
} }
}; };
return { sendKeyboardEvent, resetKeyboardState, executeMacro }; return { sendKeyboardEvent, sendKeypressEvent, resetKeyboardState, executeMacro };
} }

View File

@ -42,6 +42,7 @@ export const keys = {
F10: 0x43, F10: 0x43,
F11: 0x44, F11: 0x44,
F12: 0x45, F12: 0x45,
F13: 0x68,
F14: 0x69, F14: 0x69,
F15: 0x6a, F15: 0x6a,
F16: 0x6b, F16: 0x6b,

View File

@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo } from "react";
import { KeyboardLedSync, useSettingsStore } from "@/hooks/stores"; import { useSettingsStore } from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc"; import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
@ -13,14 +13,10 @@ import { SettingsItem } from "./devices.$id.settings";
export default function SettingsKeyboardRoute() { export default function SettingsKeyboardRoute() {
const keyboardLayout = useSettingsStore(state => state.keyboardLayout); const keyboardLayout = useSettingsStore(state => state.keyboardLayout);
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
const showPressedKeys = useSettingsStore(state => state.showPressedKeys); const showPressedKeys = useSettingsStore(state => state.showPressedKeys);
const setKeyboardLayout = useSettingsStore( const setKeyboardLayout = useSettingsStore(
state => state.setKeyboardLayout, state => state.setKeyboardLayout,
); );
const setKeyboardLedSync = useSettingsStore(
state => state.setKeyboardLedSync,
);
const setShowPressedKeys = useSettingsStore( const setShowPressedKeys = useSettingsStore(
state => state.setShowPressedKeys, state => state.setShowPressedKeys,
); );
@ -33,11 +29,6 @@ export default function SettingsKeyboardRoute() {
}, [keyboardLayout]); }, [keyboardLayout]);
const layoutOptions = keyboardOptions(); const layoutOptions = keyboardOptions();
const ledSyncOptions = [
{ value: "auto", label: "Automatic" },
{ value: "browser", label: "Browser Only" },
{ value: "host", label: "Host Only" },
];
const [send] = useJsonRpc(); const [send] = useJsonRpc();
@ -91,23 +82,6 @@ export default function SettingsKeyboardRoute() {
</p> </p>
</div> </div>
<div className="space-y-4">
{ /* this menu item could be renamed to plain "Keyboard layout" in the future, when also the virtual keyboard layout mappings are being implemented */ }
<SettingsItem
title="LED state synchronization"
description="Synchronize the LED state of the keyboard with the target device"
>
<SelectMenuBasic
size="SM"
label=""
fullWidth
value={keyboardLedSync}
onChange={e => setKeyboardLedSync(e.target.value as KeyboardLedSync)}
options={ledSyncOptions}
/>
</SettingsItem>
</div>
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem <SettingsItem
title="Show Pressed Keys" title="Show Pressed Keys"

View File

@ -20,6 +20,7 @@ import { cx } from "@/cva.config";
import { import {
HidState, HidState,
KeyboardLedState, KeyboardLedState,
KeysDownState,
NetworkState, NetworkState,
UpdateState, UpdateState,
useDeviceStore, useDeviceStore,
@ -590,7 +591,8 @@ export default function KvmIdRoute() {
const keyboardLedState = useHidStore(state => state.keyboardLedState); const keyboardLedState = useHidStore(state => state.keyboardLedState);
const setKeyboardLedState = useHidStore(state => state.setKeyboardLedState); 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 [hasUpdated, setHasUpdated] = useState(false);
const { navigateTo } = useDeviceUiNavigation(); const { navigateTo } = useDeviceUiNavigation();
@ -617,7 +619,12 @@ export default function KvmIdRoute() {
const ledState = resp.params as KeyboardLedState; const ledState = resp.params as KeyboardLedState;
console.log("Setting keyboard led state", ledState); console.log("Setting keyboard led state", ledState);
setKeyboardLedState(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") { if (resp.method === "otaState") {
@ -664,20 +671,29 @@ export default function KvmIdRoute() {
send("getKeyboardLedState", {}, resp => { send("getKeyboardLedState", {}, resp => {
if ("error" in resp) { if ("error" in resp) {
// -32601 means the method is not supported console.error("Failed to get keyboard led state", resp.error);
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);
}
return; return;
} }
console.log("Keyboard led state", resp.result); console.log("Keyboard led state", resp.result);
setKeyboardLedState(resp.result as KeyboardLedState); 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 // When the update is successful, we need to refresh the client javascript and show a success modal
useEffect(() => { useEffect(() => {

20
usb.go
View File

@ -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 // open the keyboard hid file to listen for keyboard events
if err := gadget.OpenKeyboardHidFile(); err != nil { if err := gadget.OpenKeyboardHidFile(); err != nil {
usbLogger.Error().Err(err).Msg("failed to open keyboard hid file") 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) 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 { func rpcAbsMouseReport(x, y int, buttons uint8) error {
return gadget.AbsMouseReport(x, y, buttons) return gadget.AbsMouseReport(x, y, buttons)
} }
@ -57,6 +67,10 @@ func rpcGetKeyboardLedState() (state usbgadget.KeyboardState) {
return gadget.GetKeyboardState() return gadget.GetKeyboardState()
} }
func rpcGetKeysDownState() (state usbgadget.KeysDownState) {
return gadget.GetKeysDownState()
}
var usbState = "unknown" var usbState = "unknown"
func rpcGetUSBState() (state string) { func rpcGetUSBState() (state string) {
@ -66,7 +80,7 @@ func rpcGetUSBState() (state string) {
func triggerUSBStateUpdate() { func triggerUSBStateUpdate() {
go func() { go func() {
if currentSession == nil { 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 return
} }
writeJSONRPCEvent("usbState", usbState, currentSession) writeJSONRPCEvent("usbState", usbState, currentSession)
@ -78,9 +92,9 @@ func checkUSBState() {
if newState == usbState { if newState == usbState {
return return
} }
usbLogger.Info().Str("from", usbState).Str("to", newState).Msg("USB state changed")
usbState = newState usbState = newState
usbLogger.Info().Str("from", usbState).Str("to", newState).Msg("USB state changed")
requestDisplayUpdate(true) requestDisplayUpdate(true)
triggerUSBStateUpdate() triggerUSBStateUpdate()
} }

View File

@ -102,6 +102,7 @@ func newSession(config SessionConfig) (*Session, error) {
ICEServers: []webrtc.ICEServer{iceServer}, ICEServers: []webrtc.ICEServer{iceServer},
}) })
if err != nil { if err != nil {
scopedLogger.Warn().Err(err).Msg("Failed to create PeerConnection")
return nil, err return nil, err
} }
session := &Session{peerConnection: peerConnection} 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") session.VideoTrack, err = webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264}, "video", "kvm")
if err != nil { if err != nil {
scopedLogger.Warn().Err(err).Msg("Failed to create VideoTrack")
return nil, err return nil, err
} }
rtpSender, err := peerConnection.AddTrack(session.VideoTrack) rtpSender, err := peerConnection.AddTrack(session.VideoTrack)
if err != nil { if err != nil {
scopedLogger.Warn().Err(err).Msg("Failed to add VideoTrack to PeerConnection")
return nil, err return nil, err
} }
@ -187,8 +190,9 @@ func newSession(config SessionConfig) (*Session, error) {
currentSession = nil currentSession = nil
} }
if session.shouldUmountVirtualMedia { if session.shouldUmountVirtualMedia {
err := rpcUnmountImage() if err := rpcUnmountImage(); err != nil {
scopedLogger.Warn().Err(err).Msg("unmount image failed on connection close") scopedLogger.Warn().Err(err).Msg("unmount image failed on connection close")
}
} }
if isConnected { if isConnected {
isConnected = false isConnected = false