diff --git a/display.go b/display.go index 274bb8b..aab19fb 100644 --- a/display.go +++ b/display.go @@ -30,7 +30,7 @@ const ( // do not call this function directly, use switchToScreenIfDifferent instead // this function is not thread safe func switchToScreen(screen string) { - _, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen}) + _, err := CallCtrlAction("lv_scr_load", map[string]any{"obj": screen}) if err != nil { displayLogger.Warn().Err(err).Str("screen", screen).Msg("failed to switch to screen") return @@ -39,15 +39,15 @@ func switchToScreen(screen string) { } func lvObjSetState(objName string, state string) (*CtrlResponse, error) { - return CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": objName, "state": state}) + return CallCtrlAction("lv_obj_set_state", map[string]any{"obj": objName, "state": state}) } func lvObjAddFlag(objName string, flag string) (*CtrlResponse, error) { - return CallCtrlAction("lv_obj_add_flag", map[string]interface{}{"obj": objName, "flag": flag}) + return CallCtrlAction("lv_obj_add_flag", map[string]any{"obj": objName, "flag": flag}) } func lvObjClearFlag(objName string, flag string) (*CtrlResponse, error) { - return CallCtrlAction("lv_obj_clear_flag", map[string]interface{}{"obj": objName, "flag": flag}) + return CallCtrlAction("lv_obj_clear_flag", map[string]any{"obj": objName, "flag": flag}) } func lvObjHide(objName string) (*CtrlResponse, error) { @@ -59,27 +59,27 @@ func lvObjShow(objName string) (*CtrlResponse, error) { } func lvObjSetOpacity(objName string, opacity int) (*CtrlResponse, error) { // nolint:unused - return CallCtrlAction("lv_obj_set_style_opa_layered", map[string]interface{}{"obj": objName, "opa": opacity}) + return CallCtrlAction("lv_obj_set_style_opa_layered", map[string]any{"obj": objName, "opa": opacity}) } func lvObjFadeIn(objName string, duration uint32) (*CtrlResponse, error) { - return CallCtrlAction("lv_obj_fade_in", map[string]interface{}{"obj": objName, "time": duration}) + return CallCtrlAction("lv_obj_fade_in", map[string]any{"obj": objName, "time": duration}) } func lvObjFadeOut(objName string, duration uint32) (*CtrlResponse, error) { - return CallCtrlAction("lv_obj_fade_out", map[string]interface{}{"obj": objName, "time": duration}) + return CallCtrlAction("lv_obj_fade_out", map[string]any{"obj": objName, "time": duration}) } func lvLabelSetText(objName string, text string) (*CtrlResponse, error) { - return CallCtrlAction("lv_label_set_text", map[string]interface{}{"obj": objName, "text": text}) + return CallCtrlAction("lv_label_set_text", map[string]any{"obj": objName, "text": text}) } func lvImgSetSrc(objName string, src string) (*CtrlResponse, error) { - return CallCtrlAction("lv_img_set_src", map[string]interface{}{"obj": objName, "src": src}) + return CallCtrlAction("lv_img_set_src", map[string]any{"obj": objName, "src": src}) } func lvDispSetRotation(rotation string) (*CtrlResponse, error) { - return CallCtrlAction("lv_disp_set_rotation", map[string]interface{}{"rotation": rotation}) + return CallCtrlAction("lv_disp_set_rotation", map[string]any{"rotation": rotation}) } func updateLabelIfChanged(objName string, newText string) { diff --git a/internal/confparser/confparser.go b/internal/confparser/confparser.go index 5ccd1cb..aaa3968 100644 --- a/internal/confparser/confparser.go +++ b/internal/confparser/confparser.go @@ -16,22 +16,22 @@ import ( type FieldConfig struct { Name string Required bool - RequiredIf map[string]interface{} + RequiredIf map[string]any OneOf []string ValidateTypes []string - Defaults interface{} + Defaults any IsEmpty bool - CurrentValue interface{} + CurrentValue any TypeString string Delegated bool shouldUpdateValue bool } -func SetDefaultsAndValidate(config interface{}) error { +func SetDefaultsAndValidate(config any) error { return setDefaultsAndValidate(config, true) } -func setDefaultsAndValidate(config interface{}, isRoot bool) error { +func setDefaultsAndValidate(config any, isRoot bool) error { // first we need to check if the config is a pointer if reflect.TypeOf(config).Kind() != reflect.Ptr { return fmt.Errorf("config is not a pointer") @@ -55,7 +55,7 @@ func setDefaultsAndValidate(config interface{}, isRoot bool) error { Name: field.Name, OneOf: splitString(field.Tag.Get("one_of")), ValidateTypes: splitString(field.Tag.Get("validate_type")), - RequiredIf: make(map[string]interface{}), + RequiredIf: make(map[string]any), CurrentValue: fieldValue.Interface(), IsEmpty: false, TypeString: fieldType, @@ -142,8 +142,8 @@ func setDefaultsAndValidate(config interface{}, isRoot bool) error { // now check if the field has required_if requiredIf := field.Tag.Get("required_if") if requiredIf != "" { - requiredIfParts := strings.Split(requiredIf, ",") - for _, part := range requiredIfParts { + requiredIfParts := strings.SplitSeq(requiredIf, ",") + for part := range requiredIfParts { partVal := strings.SplitN(part, "=", 2) if len(partVal) != 2 { return fmt.Errorf("invalid required_if for field `%s`: %s", field.Name, requiredIf) @@ -168,7 +168,7 @@ func setDefaultsAndValidate(config interface{}, isRoot bool) error { return nil } -func validateFields(config interface{}, fields map[string]FieldConfig) error { +func validateFields(config any, fields map[string]FieldConfig) error { // now we can start to validate the fields for _, fieldConfig := range fields { if err := fieldConfig.validate(fields); err != nil { @@ -215,7 +215,7 @@ func (f *FieldConfig) validate(fields map[string]FieldConfig) error { return nil } -func (f *FieldConfig) populate(config interface{}) { +func (f *FieldConfig) populate(config any) { // update the field if it's not empty if !f.shouldUpdateValue { return diff --git a/internal/confparser/utils.go b/internal/confparser/utils.go index a46871e..36ee28b 100644 --- a/internal/confparser/utils.go +++ b/internal/confparser/utils.go @@ -16,7 +16,7 @@ func splitString(s string) []string { return strings.Split(s, ",") } -func toString(v interface{}) (string, error) { +func toString(v any) (string, error) { switch v := v.(type) { case string: return v, nil diff --git a/internal/logging/logger.go b/internal/logging/logger.go index 39156ec..3a8274c 100644 --- a/internal/logging/logger.go +++ b/internal/logging/logger.go @@ -50,7 +50,7 @@ var ( TimeFormat: time.RFC3339, PartsOrder: []string{"time", "level", "scope", "component", "message"}, FieldsExclude: []string{"scope", "component"}, - FormatPartValueByName: func(value interface{}, name string) string { + FormatPartValueByName: func(value any, name string) string { val := fmt.Sprintf("%s", value) if name == "component" { if value == nil { @@ -121,8 +121,8 @@ func (l *Logger) updateLogLevel() { continue } - scopes := strings.Split(strings.ToLower(env), ",") - for _, scope := range scopes { + scopes := strings.SplitSeq(strings.ToLower(env), ",") + for scope := range scopes { l.scopeLevels[scope] = level } } diff --git a/internal/logging/pion.go b/internal/logging/pion.go index 453b8bc..2676caf 100644 --- a/internal/logging/pion.go +++ b/internal/logging/pion.go @@ -13,32 +13,32 @@ type pionLogger struct { func (c pionLogger) Trace(msg string) { c.logger.Trace().Msg(msg) } -func (c pionLogger) Tracef(format string, args ...interface{}) { +func (c pionLogger) Tracef(format string, args ...any) { c.logger.Trace().Msgf(format, args...) } func (c pionLogger) Debug(msg string) { c.logger.Debug().Msg(msg) } -func (c pionLogger) Debugf(format string, args ...interface{}) { +func (c pionLogger) Debugf(format string, args ...any) { c.logger.Debug().Msgf(format, args...) } func (c pionLogger) Info(msg string) { c.logger.Info().Msg(msg) } -func (c pionLogger) Infof(format string, args ...interface{}) { +func (c pionLogger) Infof(format string, args ...any) { c.logger.Info().Msgf(format, args...) } func (c pionLogger) Warn(msg string) { c.logger.Warn().Msg(msg) } -func (c pionLogger) Warnf(format string, args ...interface{}) { +func (c pionLogger) Warnf(format string, args ...any) { c.logger.Warn().Msgf(format, args...) } func (c pionLogger) Error(msg string) { c.logger.Error().Msg(msg) } -func (c pionLogger) Errorf(format string, args ...interface{}) { +func (c pionLogger) Errorf(format string, args ...any) { c.logger.Error().Msgf(format, args...) } diff --git a/internal/logging/utils.go b/internal/logging/utils.go index e622d96..73ae37a 100644 --- a/internal/logging/utils.go +++ b/internal/logging/utils.go @@ -13,7 +13,7 @@ func GetDefaultLogger() *zerolog.Logger { return &defaultLogger } -func ErrorfL(l *zerolog.Logger, format string, err error, args ...interface{}) error { +func ErrorfL(l *zerolog.Logger, format string, err error, args ...any) error { // TODO: move rootLogger to logging package if l == nil { l = &defaultLogger diff --git a/internal/network/hostname.go b/internal/network/hostname.go index d75255c..09d3996 100644 --- a/internal/network/hostname.go +++ b/internal/network/hostname.go @@ -42,7 +42,7 @@ func updateEtcHosts(hostname string, fqdn string) error { hostLine := fmt.Sprintf("127.0.1.1\t%s %s", hostname, fqdn) hostLineExists := false - for _, line := range strings.Split(string(lines), "\n") { + for line := range strings.SplitSeq(string(lines), "\n") { if strings.HasPrefix(line, "127.0.1.1") { hostLineExists = true line = hostLine diff --git a/internal/network/utils.go b/internal/network/utils.go index 6d64332..797fd72 100644 --- a/internal/network/utils.go +++ b/internal/network/utils.go @@ -13,7 +13,7 @@ func lifetimeToTime(lifetime int) *time.Time { return &t } -func IsSame(a, b interface{}) bool { +func IsSame(a, b any) bool { aJSON, err := json.Marshal(a) if err != nil { return false diff --git a/internal/udhcpc/parser.go b/internal/udhcpc/parser.go index 66c3ba2..d75857c 100644 --- a/internal/udhcpc/parser.go +++ b/internal/udhcpc/parser.go @@ -101,7 +101,7 @@ func (l *Lease) SetLeaseExpiry() (time.Time, error) { func UnmarshalDHCPCLease(lease *Lease, str string) error { // parse the lease file as a map data := make(map[string]string) - for _, line := range strings.Split(str, "\n") { + for line := range strings.SplitSeq(str, "\n") { line = strings.TrimSpace(line) // skip empty lines and comments if line == "" || strings.HasPrefix(line, "#") { @@ -165,7 +165,7 @@ func UnmarshalDHCPCLease(lease *Lease, str string) error { field.Set(reflect.ValueOf(ip)) case []net.IP: val := make([]net.IP, 0) - for _, ipStr := range strings.Fields(value) { + for ipStr := range strings.FieldsSeq(value) { ip := net.ParseIP(ipStr) if ip == nil { continue diff --git a/internal/udhcpc/udhcpc.go b/internal/udhcpc/udhcpc.go index 128ea66..7b4d6e4 100644 --- a/internal/udhcpc/udhcpc.go +++ b/internal/udhcpc/udhcpc.go @@ -52,7 +52,7 @@ func NewDHCPClient(options *DHCPClientOptions) *DHCPClient { } func (c *DHCPClient) getWatchPaths() []string { - watchPaths := make(map[string]interface{}) + watchPaths := make(map[string]any) watchPaths[filepath.Dir(c.leaseFile)] = nil if c.pidFile != "" { diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go index 6ad3b6a..2c4a456 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 @@ -159,7 +209,7 @@ func (u *UsbGadget) listenKeyboardEvents() { } u.resetLogSuppressionCounter("keyboardHidFileRead") - l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard") + l.Trace().Int("n", n).Uints8("buf", buf).Msg("got data from keyboard") if n != 1 { l.Trace().Int("n", n).Msg("expected 1 byte, got") continue @@ -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[:hidKeyBufferSize]...)) if err != nil { u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0") u.keyboardHidFile.Close() @@ -211,22 +261,112 @@ 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) +) + +// KeyCodeToMaskMap is a slice of KeyCodeMask for quick lookup +var KeyCodeToMaskMap = map[byte]byte{ + 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 := append([]byte(nil), 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).Uints8("keys", keys).Msg("Could not write keypress report to hidg0") + } + + var result = KeysDownState{ + Modifier: modifier, + Keys: []byte(keys[:]), + } + + u.updateKeyDownState(result) + + return result, nil } diff --git a/internal/usbgadget/hid_mouse_absolute.go b/internal/usbgadget/hid_mouse_absolute.go index 2718f20..c083b60 100644 --- a/internal/usbgadget/hid_mouse_absolute.go +++ b/internal/usbgadget/hid_mouse_absolute.go @@ -85,17 +85,17 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error { return nil } -func (u *UsbGadget) AbsMouseReport(x, y int, buttons uint8) error { +func (u *UsbGadget) AbsMouseReport(x int, y int, buttons uint8) error { u.absMouseLock.Lock() defer u.absMouseLock.Unlock() err := u.absMouseWriteHidFile([]byte{ - 1, // Report ID 1 - buttons, // Buttons - uint8(x), // X Low Byte - uint8(x >> 8), // X High Byte - uint8(y), // Y Low Byte - uint8(y >> 8), // Y High Byte + 1, // Report ID 1 + buttons, // Buttons + byte(x), // X Low Byte + byte(x >> 8), // X High Byte + byte(y), // Y Low Byte + byte(y >> 8), // Y High Byte }) if err != nil { return err diff --git a/internal/usbgadget/hid_mouse_relative.go b/internal/usbgadget/hid_mouse_relative.go index 786f265..70cb72c 100644 --- a/internal/usbgadget/hid_mouse_relative.go +++ b/internal/usbgadget/hid_mouse_relative.go @@ -75,15 +75,15 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error { return nil } -func (u *UsbGadget) RelMouseReport(mx, my int8, buttons uint8) error { +func (u *UsbGadget) RelMouseReport(mx int8, my int8, buttons uint8) error { u.relMouseLock.Lock() defer u.relMouseLock.Unlock() err := u.relMouseWriteHidFile([]byte{ - buttons, // Buttons - uint8(mx), // X - uint8(my), // Y - 0, // Wheel + buttons, // Buttons + byte(mx), // X + byte(my), // Y + 0, // Wheel }) if err != nil { return err diff --git a/internal/usbgadget/usbgadget.go b/internal/usbgadget/usbgadget.go index cb70655..3a01a44 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 ByteSlice `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: []byte{0, 0, 0, 0, 0, 0}}, // must be initialized to hidKeyBufferSize (6) zero bytes enabledDevices: *enabledDevices, lastUserInput: time.Now(), log: logger, diff --git a/internal/usbgadget/utils.go b/internal/usbgadget/utils.go index 8654924..05fcd3a 100644 --- a/internal/usbgadget/utils.go +++ b/internal/usbgadget/utils.go @@ -2,6 +2,7 @@ package usbgadget import ( "bytes" + "encoding/json" "fmt" "path/filepath" "strconv" @@ -10,6 +11,31 @@ import ( "github.com/rs/zerolog" ) +type ByteSlice []byte + +func (s ByteSlice) MarshalJSON() ([]byte, error) { + vals := make([]int, len(s)) + for i, v := range s { + vals[i] = int(v) + } + return json.Marshal(vals) +} + +func (s *ByteSlice) UnmarshalJSON(data []byte) error { + var vals []int + if err := json.Unmarshal(data, &vals); err != nil { + return err + } + *s = make([]byte, len(vals)) + for i, v := range vals { + if v < 0 || v > 255 { + return fmt.Errorf("value %d out of byte range", v) + } + (*s)[i] = byte(v) + } + return nil +} + func joinPath(basePath string, paths []string) string { pathArr := append([]string{basePath}, paths...) return filepath.Join(pathArr...) @@ -81,7 +107,7 @@ func compareFileContent(oldContent []byte, newContent []byte, looserMatch bool) return false } -func (u *UsbGadget) logWithSuppression(counterName string, every int, logger *zerolog.Logger, err error, msg string, args ...interface{}) { +func (u *UsbGadget) logWithSuppression(counterName string, every int, logger *zerolog.Logger, err error, msg string, args ...any) { u.logSuppressionLock.Lock() defer u.logSuppressionLock.Unlock() diff --git a/jsonrpc.go b/jsonrpc.go index a0264b8..7d05933 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -13,29 +13,30 @@ import ( "time" "github.com/pion/webrtc/v4" + "github.com/rs/zerolog" "go.bug.st/serial" "github.com/jetkvm/kvm/internal/usbgadget" ) type JSONRPCRequest struct { - JSONRPC string `json:"jsonrpc"` - Method string `json:"method"` - Params map[string]interface{} `json:"params,omitempty"` - ID interface{} `json:"id,omitempty"` + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params map[string]any `json:"params,omitempty"` + ID any `json:"id,omitempty"` } type JSONRPCResponse struct { - JSONRPC string `json:"jsonrpc"` - Result interface{} `json:"result,omitempty"` - Error interface{} `json:"error,omitempty"` - ID interface{} `json:"id"` + JSONRPC string `json:"jsonrpc"` + Result any `json:"result,omitempty"` + Error any `json:"error,omitempty"` + ID any `json:"id"` } type JSONRPCEvent struct { - JSONRPC string `json:"jsonrpc"` - Method string `json:"method"` - Params interface{} `json:"params,omitempty"` + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params any `json:"params,omitempty"` } type DisplayRotationSettings struct { @@ -61,7 +62,7 @@ func writeJSONRPCResponse(response JSONRPCResponse, session *Session) { } } -func writeJSONRPCEvent(event string, params interface{}, session *Session) { +func writeJSONRPCEvent(event string, params any, session *Session) { request := JSONRPCEvent{ JSONRPC: "2.0", Method: event, @@ -102,7 +103,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { errorResponse := JSONRPCResponse{ JSONRPC: "2.0", - Error: map[string]interface{}{ + Error: map[string]any{ "code": -32700, "message": "Parse error", }, @@ -123,7 +124,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { if !ok { errorResponse := JSONRPCResponse{ JSONRPC: "2.0", - Error: map[string]interface{}{ + Error: map[string]any{ "code": -32601, "message": "Method not found", }, @@ -134,12 +135,12 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { } scopedLogger.Trace().Msg("Calling RPC handler") - result, err := callRPCHandler(handler, request.Params) + result, err := callRPCHandler(scopedLogger, handler, request.Params) if err != nil { scopedLogger.Error().Err(err).Msg("Error calling RPC handler") errorResponse := JSONRPCResponse{ JSONRPC: "2.0", - Error: map[string]interface{}{ + Error: map[string]any{ "code": -32603, "message": "Internal error", "data": err.Error(), @@ -200,7 +201,7 @@ func rpcGetStreamQualityFactor() (float64, error) { func rpcSetStreamQualityFactor(factor float64) error { logger.Info().Float64("factor", factor).Msg("Setting stream quality factor") - var _, err = CallCtrlAction("set_video_quality_factor", map[string]interface{}{"quality_factor": factor}) + var _, err = CallCtrlAction("set_video_quality_factor", map[string]any{"quality_factor": factor}) if err != nil { return err } @@ -240,7 +241,7 @@ func rpcSetEDID(edid string) error { } else { logger.Info().Str("edid", edid).Msg("Setting EDID") } - _, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": edid}) + _, err := CallCtrlAction("set_edid", map[string]any{"edid": edid}) if err != nil { return err } @@ -467,12 +468,12 @@ func rpcSetTLSState(state TLSState) error { } type RPCHandler struct { - Func interface{} + Func any Params []string } // call the handler but recover from a panic to ensure our RPC thread doesn't collapse on malformed calls -func callRPCHandler(handler RPCHandler, params map[string]interface{}) (result interface{}, err error) { +func callRPCHandler(logger zerolog.Logger, handler RPCHandler, params map[string]any) (result any, err error) { // Use defer to recover from a panic defer func() { if r := recover(); r != nil { @@ -486,11 +487,11 @@ func callRPCHandler(handler RPCHandler, params map[string]interface{}) (result i }() // Call the handler - result, err = riskyCallRPCHandler(handler, params) - return result, err + result, err = riskyCallRPCHandler(logger, handler, params) + return result, err // do not combine these two lines into one, as it breaks the above defer function's setting of err } -func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (interface{}, error) { +func riskyCallRPCHandler(logger zerolog.Logger, handler RPCHandler, params map[string]any) (any, error) { handlerValue := reflect.ValueOf(handler.Func) handlerType := handlerValue.Type() @@ -499,20 +500,24 @@ func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (int } numParams := handlerType.NumIn() - args := make([]reflect.Value, numParams) - // Get the parameter names from the RPCHandler - paramNames := handler.Params + paramNames := handler.Params // Get the parameter names from the RPCHandler if len(paramNames) != numParams { - return nil, errors.New("mismatch between handler parameters and defined parameter names") + err := fmt.Errorf("mismatch between handler parameters (%d) and defined parameter names (%d)", numParams, len(paramNames)) + logger.Error().Strs("paramNames", paramNames).Err(err).Msg("Cannot call RPC handler") + return nil, err } - for i := 0; i < numParams; i++ { + args := make([]reflect.Value, numParams) + + for i := range numParams { paramType := handlerType.In(i) paramName := paramNames[i] paramValue, ok := params[paramName] if !ok { - return nil, errors.New("missing parameter: " + paramName) + err := fmt.Errorf("missing parameter: %s", paramName) + logger.Error().Err(err).Msg("Cannot marshal arguments for RPC handler") + return nil, err } convertedValue := reflect.ValueOf(paramValue) @@ -529,7 +534,7 @@ func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (int if elemValue.Kind() == reflect.Float64 && paramType.Elem().Kind() == reflect.Uint8 { intValue := int(elemValue.Float()) if intValue < 0 || intValue > 255 { - return nil, fmt.Errorf("value out of range for uint8: %v", intValue) + return nil, fmt.Errorf("value out of range for uint8: %v for parameter %s", intValue, paramName) } newSlice.Index(j).SetUint(uint64(intValue)) } else { @@ -545,12 +550,12 @@ func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (int } else if paramType.Kind() == reflect.Struct && convertedValue.Kind() == reflect.Map { jsonData, err := json.Marshal(convertedValue.Interface()) if err != nil { - return nil, fmt.Errorf("failed to marshal map to JSON: %v", err) + return nil, fmt.Errorf("failed to marshal map to JSON: %v for parameter %s", err, paramName) } newStruct := reflect.New(paramType).Interface() if err := json.Unmarshal(jsonData, newStruct); err != nil { - return nil, fmt.Errorf("failed to unmarshal JSON into struct: %v", err) + return nil, fmt.Errorf("failed to unmarshal JSON into struct: %v for parameter %s", err, paramName) } args[i] = reflect.ValueOf(newStruct).Elem() } else { @@ -561,6 +566,7 @@ func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (int } } + logger.Trace().Msg("Calling RPC handler") results := handlerValue.Call(args) if len(results) == 0 { @@ -568,23 +574,32 @@ func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (int } if len(results) == 1 { - if results[0].Type().Implements(reflect.TypeOf((*error)(nil)).Elem()) { - if !results[0].IsNil() { - return nil, results[0].Interface().(error) + if ok, err := asError(results[0]); ok { + return nil, err + } + return results[0].Interface(), nil + } + + if len(results) == 2 { + if ok, err := asError(results[1]); ok { + if err != nil { + return nil, err } - return nil, nil } return results[0].Interface(), nil } - if len(results) == 2 && results[1].Type().Implements(reflect.TypeOf((*error)(nil)).Elem()) { - if !results[1].IsNil() { - return nil, results[1].Interface().(error) - } - return results[0].Interface(), nil - } + return nil, fmt.Errorf("too many return values from handler: %d", len(results)) +} - return nil, errors.New("unexpected return values from handler") +func asError(value reflect.Value) (bool, error) { + if value.Type().Implements(reflect.TypeOf((*error)(nil)).Elem()) { + if value.IsNil() { + return true, nil + } + return true, value.Interface().(error) + } + return false, nil } func rpcSetMassStorageMode(mode string) (string, error) { @@ -923,7 +938,7 @@ func rpcSetKeyboardLayout(layout string) error { return nil } -func getKeyboardMacros() (interface{}, error) { +func getKeyboardMacros() (any, error) { macros := make([]KeyboardMacro, len(config.KeyboardMacros)) copy(macros, config.KeyboardMacros) @@ -931,10 +946,10 @@ func getKeyboardMacros() (interface{}, error) { } type KeyboardMacrosParams struct { - Macros []interface{} `json:"macros"` + Macros []any `json:"macros"` } -func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) { +func setKeyboardMacros(params KeyboardMacrosParams) (any, error) { if params.Macros == nil { return nil, fmt.Errorf("missing or invalid macros parameter") } @@ -942,7 +957,7 @@ func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) { newMacros := make([]KeyboardMacro, 0, len(params.Macros)) for i, item := range params.Macros { - macroMap, ok := item.(map[string]interface{}) + macroMap, ok := item.(map[string]any) if !ok { return nil, fmt.Errorf("invalid macro at index %d", i) } @@ -960,16 +975,16 @@ func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) { } steps := []KeyboardMacroStep{} - if stepsArray, ok := macroMap["steps"].([]interface{}); ok { + if stepsArray, ok := macroMap["steps"].([]any); ok { for _, stepItem := range stepsArray { - stepMap, ok := stepItem.(map[string]interface{}) + stepMap, ok := stepItem.(map[string]any) if !ok { continue } step := KeyboardMacroStep{} - if keysArray, ok := stepMap["keys"].([]interface{}); ok { + if keysArray, ok := stepMap["keys"].([]any); ok { for _, k := range keysArray { if keyStr, ok := k.(string); ok { step.Keys = append(step.Keys, keyStr) @@ -977,7 +992,7 @@ func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) { } } - if modsArray, ok := stepMap["modifiers"].([]interface{}); ok { + if modsArray, ok := stepMap["modifiers"].([]any); ok { for _, m := range modsArray { if modStr, ok := m.(string); ok { step.Modifiers = append(step.Modifiers, modStr) @@ -1047,6 +1062,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/log.go b/log.go index b353a2c..1a091b1 100644 --- a/log.go +++ b/log.go @@ -5,7 +5,7 @@ import ( "github.com/rs/zerolog" ) -func ErrorfL(l *zerolog.Logger, format string, err error, args ...interface{}) error { +func ErrorfL(l *zerolog.Logger, format string, err error, args ...any) error { return logging.ErrorfL(l, format, err, args...) } diff --git a/native.go b/native.go index 9807206..67f423a 100644 --- a/native.go +++ b/native.go @@ -21,18 +21,18 @@ import ( var ctrlSocketConn net.Conn type CtrlAction struct { - Action string `json:"action"` - Seq int32 `json:"seq,omitempty"` - Params map[string]interface{} `json:"params,omitempty"` + Action string `json:"action"` + Seq int32 `json:"seq,omitempty"` + Params map[string]any `json:"params,omitempty"` } type CtrlResponse struct { - Seq int32 `json:"seq,omitempty"` - Error string `json:"error,omitempty"` - Errno int32 `json:"errno,omitempty"` - Result map[string]interface{} `json:"result,omitempty"` - Event string `json:"event,omitempty"` - Data json.RawMessage `json:"data,omitempty"` + Seq int32 `json:"seq,omitempty"` + Error string `json:"error,omitempty"` + Errno int32 `json:"errno,omitempty"` + Result map[string]any `json:"result,omitempty"` + Event string `json:"event,omitempty"` + Data json.RawMessage `json:"data,omitempty"` } type EventHandler func(event CtrlResponse) @@ -48,7 +48,7 @@ var ( nativeCmdLock = &sync.Mutex{} ) -func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) { +func CallCtrlAction(action string, params map[string]any) (*CtrlResponse, error) { lock.Lock() defer lock.Unlock() ctrlAction := CtrlAction{ @@ -429,7 +429,7 @@ func ensureBinaryUpdated(destPath string) error { func restoreHdmiEdid() { if config.EdidString != "" { nativeLogger.Info().Str("edid", config.EdidString).Msg("Restoring HDMI EDID") - _, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": config.EdidString}) + _, err := CallCtrlAction("set_edid", map[string]any{"edid": config.EdidString}) if err != nil { nativeLogger.Warn().Err(err).Msg("Failed to restore HDMI EDID") } diff --git a/remote_mount.go b/remote_mount.go index befffcb..32a0fd2 100644 --- a/remote_mount.go +++ b/remote_mount.go @@ -27,10 +27,7 @@ func (w *WebRTCDiskReader) Read(ctx context.Context, offset int64, size int64) ( } mountedImageSize := currentVirtualMediaState.Size virtualMediaStateMutex.RUnlock() - end := offset + size - if end > mountedImageSize { - end = mountedImageSize - } + end := min(offset+size, mountedImageSize) req := DiskReadRequest{ Start: uint64(offset), End: uint64(end), 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..ab6f459 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -31,7 +31,7 @@ "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0", "react-router-dom": "^6.22.3", - "react-simple-keyboard": "^3.8.106", + "react-simple-keyboard": "^3.8.109", "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.10", "recharts": "^2.15.3", @@ -41,22 +41,22 @@ "zustand": "^4.5.2" }, "devDependencies": { - "@eslint/compat": "^1.3.1", + "@eslint/compat": "^1.3.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.32.0", + "@eslint/js": "^9.33.0", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/postcss": "^4.1.11", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.11", - "@types/react": "^19.1.9", + "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", "@types/semver": "^7.7.0", "@types/validator": "^13.15.2", - "@typescript-eslint/eslint-plugin": "^8.39.0", - "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/eslint-plugin": "^8.39.1", + "@typescript-eslint/parser": "^8.39.1", "@vitejs/plugin-react-swc": "^3.10.2", "autoprefixer": "^10.4.21", - "eslint": "^9.32.0", + "eslint": "^9.33.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", @@ -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.109", + "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.109.tgz", + "integrity": "sha512-FLlivKL4tb5G2cWOo2slOrMEkzzFX0Yg8P7k5qzisN8+TnqUPq+8G7N8D2+0oVkSmfeqZn6PyLCurGSitK4QIQ==", "license": "MIT", "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", diff --git a/ui/package.json b/ui/package.json index 9f0c298..4c929f0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -42,7 +42,7 @@ "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0", "react-router-dom": "^6.22.3", - "react-simple-keyboard": "^3.8.106", + "react-simple-keyboard": "^3.8.109", "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.10", "recharts": "^2.15.3", @@ -52,22 +52,22 @@ "zustand": "^4.5.2" }, "devDependencies": { - "@eslint/compat": "^1.3.1", + "@eslint/compat": "^1.3.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.32.0", + "@eslint/js": "^9.33.0", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/postcss": "^4.1.11", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.11", - "@types/react": "^19.1.9", + "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", "@types/semver": "^7.7.0", "@types/validator": "^13.15.2", - "@typescript-eslint/eslint-plugin": "^8.39.0", - "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/eslint-plugin": "^8.39.1", + "@typescript-eslint/parser": "^8.39.1", "@vitejs/plugin-react-swc": "^3.10.2", "autoprefixer": "^10.4.21", - "eslint": "^9.32.0", + "eslint": "^9.33.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx index 7ce67a4..8d3b40e 100644 --- a/ui/src/components/InfoBar.tsx +++ b/ui/src/components/InfoBar.tsx @@ -1,50 +1,65 @@ -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { cx } from "@/cva.config"; import { + HidState, + KeysDownState, + MouseState, + RTCState, + SettingsState, useHidStore, useMouseStore, useRTCStore, useSettingsStore, useVideoStore, + VideoState, } from "@/hooks/stores"; import { keys, modifiers } from "@/keyboardMappings"; export default function InfoBar() { - const activeKeys = useHidStore(state => state.activeKeys); - const activeModifiers = useHidStore(state => state.activeModifiers); - const mouseX = useMouseStore(state => state.mouseX); - const mouseY = useMouseStore(state => state.mouseY); - const mouseMove = useMouseStore(state => state.mouseMove); + const keysDownState = useHidStore((state: HidState) => state.keysDownState); + const mouseX = useMouseStore((state: MouseState) => state.mouseX); + const mouseY = useMouseStore((state: MouseState) => state.mouseY); + const mouseMove = useMouseStore((state: MouseState) => state.mouseMove); const videoClientSize = useVideoStore( - state => `${Math.round(state.clientWidth)}x${Math.round(state.clientHeight)}`, + (state: VideoState) => `${Math.round(state.clientWidth)}x${Math.round(state.clientHeight)}`, ); const videoSize = useVideoStore( - state => `${Math.round(state.width)}x${Math.round(state.height)}`, + (state: VideoState) => `${Math.round(state.width)}x${Math.round(state.height)}`, ); - const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); + const rpcDataChannel = useRTCStore((state: RTCState) => state.rpcDataChannel); const settings = useSettingsStore(); - const showPressedKeys = useSettingsStore(state => state.showPressedKeys); + const showPressedKeys = useSettingsStore((state: SettingsState) => state.showPressedKeys); useEffect(() => { if (!rpcDataChannel) return; rpcDataChannel.onclose = () => console.log("rpcDataChannel has closed"); - rpcDataChannel.onerror = e => + rpcDataChannel.onerror = (e: Event) => console.log(`Error on DataChannel '${rpcDataChannel.label}': ${e}`); }, [rpcDataChannel]); - const keyboardLedState = useHidStore(state => state.keyboardLedState); - const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable); - const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); + const keyboardLedState = useHidStore((state: HidState) => state.keyboardLedState); + const isTurnServerInUse = useRTCStore((state: RTCState) => state.isTurnServerInUse); - const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse); + const usbState = useHidStore((state: HidState) => state.usbState); + const hdmiState = useVideoStore((state: VideoState) => state.hdmiState); - const usbState = useHidStore(state => state.usbState); - const hdmiState = useVideoStore(state => state.hdmiState); + const displayKeys = useMemo(() => { + if (!showPressedKeys || !keysDownState) + return ""; + + const state = keysDownState as KeysDownState; + const activeModifierMask = state.modifier || 0; + const keysDown = state.keys || []; + const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name); + const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name); + + return [...modifierNames,...keyNames].join(", "); + }, [keysDownState, showPressedKeys]); return (
@@ -102,14 +117,7 @@ export default function InfoBar() {
Keys:

- {[ - ...activeKeys.map( - x => Object.entries(keys).filter(y => y[1] === x)[0][0], - ), - activeModifiers.map( - x => Object.entries(modifiers).filter(y => y[1] === x)[0][0], - ), - ].join(", ")} + {displayKeys}

)} @@ -122,19 +130,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..a938a6b 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -9,13 +9,16 @@ 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, + MouseState, + RTCState, + SettingsState, useMouseStore, useRTCStore, useSettingsStore, useVideoStore, + VideoState, } from "@/hooks/stores"; import { @@ -28,15 +31,15 @@ import { export default function WebRTCVideo() { // Video and stream related refs and states const videoElm = useRef(null); - const mediaStream = useRTCStore(state => state.mediaStream); + const mediaStream = useRTCStore((state: RTCState) => state.mediaStream); const [isPlaying, setIsPlaying] = useState(false); - const peerConnectionState = useRTCStore(state => state.peerConnectionState); + const peerConnectionState = useRTCStore((state: RTCState) => state.peerConnectionState); const [isPointerLockActive, setIsPointerLockActive] = useState(false); // Store hooks const settings = useSettingsStore(); - const { sendKeyboardEvent, resetKeyboardState } = useKeyboard(); - const setMousePosition = useMouseStore(state => state.setMousePosition); - const setMouseMove = useMouseStore(state => state.setMouseMove); + const { sendKeypressEvent, resetKeyboardState } = useKeyboard(); + const setMousePosition = useMouseStore((state: MouseState) => state.setMousePosition); + const setMouseMove = useMouseStore((state: MouseState) => state.setMouseMove); const { setClientSize: setVideoClientSize, setSize: setVideoSize, @@ -47,27 +50,15 @@ export default function WebRTCVideo() { } = useVideoStore(); // Video enhancement settings - const videoSaturation = useSettingsStore(state => state.videoSaturation); - 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); + const videoSaturation = useSettingsStore((state: SettingsState) => state.videoSaturation); + const videoBrightness = useSettingsStore((state: SettingsState) => state.videoBrightness); + const videoContrast = useSettingsStore((state: SettingsState) => state.videoContrast); // RTC related states - const peerConnection = useRTCStore(state => state.peerConnection); + const peerConnection = useRTCStore((state: RTCState ) => state.peerConnection); // HDMI and UI states - const hdmiState = useVideoStore(state => state.hdmiState); + const hdmiState = useVideoStore((state: VideoState) => state.hdmiState); const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState); const isVideoLoading = !isPlaying; @@ -344,153 +335,45 @@ 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(); - let code = e.code; - const key = e.key; + const code = getAdjustedKeyCode(e); + const hidKey = keys[code]; - if (!isKeyboardLedManagedByHost) { - setIsNumLockActive(e.getModifierState("NumLock")); - setIsCapsLockActive(e.getModifierState("CapsLock")); - setIsScrollLockActive(e.getModifierState("ScrollLock")); + if (hidKey === undefined) { + console.warn(`Key down not mapped: ${code}`); + return; } - 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 // 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) { + if (e.metaKey && hidKey < 0xE0) { setTimeout(() => { - const prev = useHidStore.getState(); - sendKeyboardEvent([], newModifiers || prev.activeModifiers); + sendKeypressEvent(hidKey, false); }, 10); } - - sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); + sendKeypressEvent(hidKey, true); }, - [ - handleModifierKeys, - sendKeyboardEvent, - isKeyboardLedManagedByHost, - setIsNumLockActive, - setIsCapsLockActive, - setIsScrollLockActive, - ], + [sendKeypressEvent], ); const keyUpHandler = useCallback( - (e: KeyboardEvent) => { + async (e: KeyboardEvent) => { e.preventDefault(); - const prev = useHidStore.getState(); - - if (!isKeyboardLedManagedByHost) { - setIsNumLockActive(e.getModifierState("NumLock")); - setIsCapsLockActive(e.getModifierState("CapsLock")); - setIsScrollLockActive(e.getModifierState("ScrollLock")); + const code = getAdjustedKeyCode(e); + const hidKey = keys[code]; + + if (hidKey === undefined) { + console.warn(`Key up not mapped: ${code}`); + return; } - // 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)]); + sendKeypressEvent(hidKey, false); }, - [ - handleModifierKeys, - sendKeyboardEvent, - isKeyboardLedManagedByHost, - setIsNumLockActive, - setIsCapsLockActive, - setIsScrollLockActive, - ], + [sendKeypressEvent], ); const videoKeyUpHandler = useCallback((e: KeyboardEvent) => { @@ -667,6 +550,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..223d994 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -47,12 +47,12 @@ export interface User { picture?: string; } -interface UserState { +export interface UserState { user: User | null; setUser: (user: User | null) => void; } -interface UIState { +export interface UIState { sidebarView: AvailableSidebarViews | null; setSidebarView: (view: AvailableSidebarViews | null) => void; @@ -68,21 +68,21 @@ interface UIState { setAttachedVirtualKeyboardVisibility: (enabled: boolean) => void; terminalType: AvailableTerminalTypes; - setTerminalType: (enabled: UIState["terminalType"]) => void; + setTerminalType: (type: UIState["terminalType"]) => void; } export const useUiStore = create(set => ({ terminalType: "none", - setTerminalType: type => set({ terminalType: type }), + setTerminalType: (type: UIState["terminalType"]) => set({ terminalType: type }), sidebarView: null, - setSidebarView: view => set({ sidebarView: view }), + setSidebarView: (view: AvailableSidebarViews | null) => set({ sidebarView: view }), disableVideoFocusTrap: false, - setDisableVideoFocusTrap: enabled => set({ disableVideoFocusTrap: enabled }), + setDisableVideoFocusTrap: (enabled: boolean) => set({ disableVideoFocusTrap: enabled }), isWakeOnLanModalVisible: false, - setWakeOnLanModalVisibility: enabled => set({ isWakeOnLanModalVisible: enabled }), + setWakeOnLanModalVisibility: (enabled: boolean) => set({ isWakeOnLanModalVisible: enabled }), toggleSidebarView: view => set(state => { @@ -94,11 +94,11 @@ export const useUiStore = create(set => ({ }), isAttachedVirtualKeyboardVisible: true, - setAttachedVirtualKeyboardVisibility: enabled => + setAttachedVirtualKeyboardVisibility: (enabled: boolean) => set({ isAttachedVirtualKeyboardVisible: enabled }), })); -interface RTCState { +export interface RTCState { peerConnection: RTCPeerConnection | null; setPeerConnection: (pc: RTCState["peerConnection"]) => void; @@ -118,18 +118,18 @@ interface RTCState { setMediaStream: (stream: MediaStream) => void; videoStreamStats: RTCInboundRtpStreamStats | null; - appendVideoStreamStats: (state: RTCInboundRtpStreamStats) => void; + appendVideoStreamStats: (stats: RTCInboundRtpStreamStats) => void; videoStreamStatsHistory: Map; isTurnServerInUse: boolean; setTurnServerInUse: (inUse: boolean) => void; inboundRtpStats: Map; - appendInboundRtpStats: (state: RTCInboundRtpStreamStats) => void; + appendInboundRtpStats: (stats: RTCInboundRtpStreamStats) => void; clearInboundRtpStats: () => void; candidatePairStats: Map; - appendCandidatePairStats: (pair: RTCIceCandidatePairStats) => void; + appendCandidatePairStats: (stats: RTCIceCandidatePairStats) => void; clearCandidatePairStats: () => void; // Remote ICE candidates stat type doesn't exist as of today @@ -141,7 +141,7 @@ interface RTCState { // Disk data channel stats type doesn't exist as of today diskDataChannelStats: Map; - appendDiskDataChannelStats: (stat: RTCDataChannelStats) => void; + appendDiskDataChannelStats: (stats: RTCDataChannelStats) => void; terminalChannel: RTCDataChannel | null; setTerminalChannel: (channel: RTCDataChannel) => void; @@ -149,78 +149,78 @@ interface RTCState { export const useRTCStore = create(set => ({ peerConnection: null, - setPeerConnection: pc => set({ peerConnection: pc }), + setPeerConnection: (pc: RTCState["peerConnection"]) => set({ peerConnection: pc }), rpcDataChannel: null, - setRpcDataChannel: channel => set({ rpcDataChannel: channel }), + setRpcDataChannel: (channel: RTCDataChannel) => set({ rpcDataChannel: channel }), transceiver: null, - setTransceiver: transceiver => set({ transceiver }), + setTransceiver: (transceiver: RTCRtpTransceiver) => set({ transceiver }), peerConnectionState: null, - setPeerConnectionState: state => set({ peerConnectionState: state }), + setPeerConnectionState: (state: RTCPeerConnectionState) => set({ peerConnectionState: state }), diskChannel: null, - setDiskChannel: channel => set({ diskChannel: channel }), + setDiskChannel: (channel: RTCDataChannel) => set({ diskChannel: channel }), mediaStream: null, - setMediaStream: stream => set({ mediaStream: stream }), + setMediaStream: (stream: MediaStream) => set({ mediaStream: stream }), videoStreamStats: null, - appendVideoStreamStats: stats => set({ videoStreamStats: stats }), + appendVideoStreamStats: (stats: RTCInboundRtpStreamStats) => set({ videoStreamStats: stats }), videoStreamStatsHistory: new Map(), isTurnServerInUse: false, - setTurnServerInUse: inUse => set({ isTurnServerInUse: inUse }), + setTurnServerInUse: (inUse: boolean) => set({ isTurnServerInUse: inUse }), inboundRtpStats: new Map(), - appendInboundRtpStats: newStat => { + appendInboundRtpStats: (stats: RTCInboundRtpStreamStats) => { set(prevState => ({ - inboundRtpStats: appendStatToMap(newStat, prevState.inboundRtpStats), + inboundRtpStats: appendStatToMap(stats, prevState.inboundRtpStats), })); }, clearInboundRtpStats: () => set({ inboundRtpStats: new Map() }), candidatePairStats: new Map(), - appendCandidatePairStats: newStat => { + appendCandidatePairStats: (stats: RTCIceCandidatePairStats) => { set(prevState => ({ - candidatePairStats: appendStatToMap(newStat, prevState.candidatePairStats), + candidatePairStats: appendStatToMap(stats, prevState.candidatePairStats), })); }, clearCandidatePairStats: () => set({ candidatePairStats: new Map() }), localCandidateStats: new Map(), - appendLocalCandidateStats: newStat => { + appendLocalCandidateStats: (stats: RTCIceCandidateStats) => { set(prevState => ({ - localCandidateStats: appendStatToMap(newStat, prevState.localCandidateStats), + localCandidateStats: appendStatToMap(stats, prevState.localCandidateStats), })); }, remoteCandidateStats: new Map(), - appendRemoteCandidateStats: newStat => { + appendRemoteCandidateStats: (stats: RTCIceCandidateStats) => { set(prevState => ({ - remoteCandidateStats: appendStatToMap(newStat, prevState.remoteCandidateStats), + remoteCandidateStats: appendStatToMap(stats, prevState.remoteCandidateStats), })); }, diskDataChannelStats: new Map(), - appendDiskDataChannelStats: newStat => { + appendDiskDataChannelStats: (stats: RTCDataChannelStats) => { set(prevState => ({ - diskDataChannelStats: appendStatToMap(newStat, prevState.diskDataChannelStats), + diskDataChannelStats: appendStatToMap(stats, prevState.diskDataChannelStats), })); }, // Add these new properties to the store implementation terminalChannel: null, - setTerminalChannel: channel => set({ terminalChannel: channel }), + setTerminalChannel: (channel: RTCDataChannel) => set({ terminalChannel: channel }), })); -interface MouseMove { +export interface MouseMove { x: number; y: number; buttons: number; } -interface MouseState { +export interface MouseState { mouseX: number; mouseY: number; mouseMove?: MouseMove; @@ -232,9 +232,14 @@ export const useMouseStore = create(set => ({ mouseX: 0, mouseY: 0, setMouseMove: (move?: MouseMove) => set({ mouseMove: move }), - setMousePosition: (x, y) => set({ mouseX: x, mouseY: y }), + setMousePosition: (x: number, y: number) => set({ mouseX: x, mouseY: y }), })); +export interface HdmiState { + ready: boolean; + error?: Extract; +} + export interface VideoState { width: number; height: number; @@ -263,13 +268,13 @@ export const useVideoStore = create(set => ({ clientHeight: 0, // The video element's client size - setClientSize: (clientWidth, clientHeight) => set({ clientWidth, clientHeight }), + setClientSize: (clientWidth: number, clientHeight: number) => set({ clientWidth, clientHeight }), // Resolution - setSize: (width, height) => set({ width, height }), + setSize: (width: number, height: number) => set({ width, height }), hdmiState: "connecting", - setHdmiState: state => { + setHdmiState: (state: HdmiState) => { if (!state) return; const { ready, error } = state; @@ -283,9 +288,7 @@ export const useVideoStore = create(set => ({ }, })); -export type KeyboardLedSync = "auto" | "browser" | "host"; - -interface SettingsState { +export interface SettingsState { isCursorHidden: boolean; setCursorVisibility: (enabled: boolean) => void; @@ -308,9 +311,6 @@ interface SettingsState { keyboardLayout: string; setKeyboardLayout: (layout: string) => void; - keyboardLedSync: KeyboardLedSync; - setKeyboardLedSync: (sync: KeyboardLedSync) => void; - scrollThrottling: number; setScrollThrottling: (value: number) => void; @@ -330,17 +330,17 @@ export const useSettingsStore = create( persist( set => ({ isCursorHidden: false, - setCursorVisibility: enabled => set({ isCursorHidden: enabled }), + setCursorVisibility: (enabled: boolean) => set({ isCursorHidden: enabled }), mouseMode: "absolute", - setMouseMode: mode => set({ mouseMode: mode }), + setMouseMode: (mode: string) => set({ mouseMode: mode }), debugMode: import.meta.env.DEV, - setDebugMode: enabled => set({ debugMode: enabled }), + setDebugMode: (enabled: boolean) => set({ debugMode: enabled }), // Add developer mode with default value developerMode: false, - setDeveloperMode: enabled => set({ developerMode: enabled }), + setDeveloperMode: (enabled: boolean) => set({ developerMode: enabled }), displayRotation: "270", setDisplayRotation: (rotation: string) => set({ displayRotation: rotation }), @@ -354,24 +354,21 @@ export const useSettingsStore = create( set({ backlightSettings: settings }), keyboardLayout: "en-US", - setKeyboardLayout: layout => set({ keyboardLayout: layout }), - - keyboardLedSync: "auto", - setKeyboardLedSync: sync => set({ keyboardLedSync: sync }), + setKeyboardLayout: (layout: string) => set({ keyboardLayout: layout }), scrollThrottling: 0, - setScrollThrottling: value => set({ scrollThrottling: value }), + setScrollThrottling: (value: number) => set({ scrollThrottling: value }), showPressedKeys: true, - setShowPressedKeys: show => set({ showPressedKeys: show }), + setShowPressedKeys: (show: boolean) => set({ showPressedKeys: show }), // Video enhancement settings with default values (1.0 = normal) videoSaturation: 1.0, - setVideoSaturation: value => set({ videoSaturation: value }), + setVideoSaturation: (value: number) => set({ videoSaturation: value }), videoBrightness: 1.0, - setVideoBrightness: value => set({ videoBrightness: value }), + setVideoBrightness: (value: number) => set({ videoBrightness: value }), videoContrast: 1.0, - setVideoContrast: value => set({ videoContrast: value }), + setVideoContrast: (value: number) => set({ videoContrast: value }), }), { name: "settings", @@ -411,23 +408,23 @@ export interface MountMediaState { export const useMountMediaStore = create(set => ({ localFile: null, - setLocalFile: file => set({ localFile: file }), + setLocalFile: (file: MountMediaState["localFile"]) => set({ localFile: file }), remoteVirtualMediaState: null, - setRemoteVirtualMediaState: state => set({ remoteVirtualMediaState: state }), + setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => set({ remoteVirtualMediaState: state }), modalView: "mode", - setModalView: view => set({ modalView: view }), + setModalView: (view: MountMediaState["modalView"]) => set({ modalView: view }), isMountMediaDialogOpen: false, - setIsMountMediaDialogOpen: isOpen => set({ isMountMediaDialogOpen: isOpen }), + setIsMountMediaDialogOpen: (isOpen: MountMediaState["isMountMediaDialogOpen"]) => set({ isMountMediaDialogOpen: isOpen }), uploadedFiles: [], - addUploadedFile: file => + addUploadedFile: (file: { name: string; size: string; uploadedAt: string }) => set(state => ({ uploadedFiles: [...state.uploadedFiles, file] })), errorMessage: null, - setErrorMessage: message => set({ errorMessage: message }), + setErrorMessage: (message: string | null) => set({ errorMessage: message }), })); export interface KeyboardLedState { @@ -436,24 +433,15 @@ 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[]; - activeModifiers: number[]; - - updateActiveKeysAndModifiers: (keysAndModifiers: { - keys: number[]; - modifiers: number[]; - }) => void; - altGrArmed: boolean; setAltGrArmed: (armed: boolean) => void; @@ -465,12 +453,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,51 +467,31 @@ export interface HidState { setUsbState: (state: HidState["usbState"]) => void; } -export const useHidStore = create((set, get) => ({ - activeKeys: [], - activeModifiers: [], - updateActiveKeysAndModifiers: ({ keys, modifiers }) => { - return set({ activeKeys: keys, activeModifiers: modifiers }); - }, - +export const useHidStore = create(set => ({ altGrArmed: false, - setAltGrArmed: armed => set({ altGrArmed: armed }), + setAltGrArmed: (armed: boolean): void => set({ altGrArmed: armed }), altGrTimer: 0, - setAltGrTimer: timeout => set({ altGrTimer: timeout }), + setAltGrTimer: (timeout: number | null): void => set({ altGrTimer: timeout }), altGrCtrlTime: 0, - setAltGrCtrlTime: time => set({ altGrCtrlTime: time }), + setAltGrCtrlTime: (time: number): void => set({ altGrCtrlTime: time }), - 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 }); - }, + keyboardLedState: undefined, + setKeyboardLedState: (ledState: KeyboardLedState): void => set({ keyboardLedState: ledState }), - keyboardLedStateSyncAvailable: false, - setKeyboardLedStateSyncAvailable: available => set({ keyboardLedStateSyncAvailable: available }), + keysDownState: undefined, + setKeysDownState: (state: KeysDownState): void => set({ keysDownState: state }), isVirtualKeyboardEnabled: false, - setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }), + setVirtualKeyboardEnabled: (enabled: boolean): void => set({ isVirtualKeyboardEnabled: enabled }), isPasteModeEnabled: false, - setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }), + setPasteModeEnabled: (enabled: boolean): void => set({ isPasteModeEnabled: enabled }), // Add these new properties for USB state usbState: "not attached", - setUsbState: state => set({ usbState: state }), + setUsbState: (state: HidState["usbState"]) => set({ usbState: state }), })); export const useUserStore = create(set => ({ @@ -584,7 +549,7 @@ export interface UpdateState { export const useUpdateStore = create(set => ({ isUpdatePending: false, - setIsUpdatePending: isPending => set({ isUpdatePending: isPending }), + setIsUpdatePending: (isPending: boolean) => set({ isUpdatePending: isPending }), setOtaState: state => set({ otaState: state }), otaState: { @@ -608,12 +573,12 @@ export const useUpdateStore = create(set => ({ }, updateDialogHasBeenMinimized: false, - setUpdateDialogHasBeenMinimized: hasBeenMinimized => + setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => set({ updateDialogHasBeenMinimized: hasBeenMinimized }), modalView: "loading", - setModalView: view => set({ modalView: view }), + setModalView: (view: UpdateState["modalView"]) => set({ modalView: view }), updateErrorMessage: null, - setUpdateErrorMessage: errorMessage => set({ updateErrorMessage: errorMessage }), + setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }), })); interface UsbConfigModalState { @@ -634,8 +599,8 @@ export interface UsbConfigState { export const useUsbConfigModalStore = create(set => ({ modalView: "updateUsbConfig", errorMessage: null, - setModalView: view => set({ modalView: view }), - setErrorMessage: message => set({ errorMessage: message }), + setModalView: (view: UsbConfigModalState["modalView"]) => set({ modalView: view }), + setErrorMessage: (message: string | null) => set({ errorMessage: message }), })); interface LocalAuthModalState { @@ -651,7 +616,7 @@ interface LocalAuthModalState { export const useLocalAuthModalStore = create(set => ({ modalView: "createPassword", - setModalView: view => set({ modalView: view }), + setModalView: (view: LocalAuthModalState["modalView"]) => set({ modalView: view }), })); export interface DeviceState { @@ -666,8 +631,8 @@ export const useDeviceStore = create(set => ({ appVersion: null, systemVersion: null, - setAppVersion: version => set({ appVersion: version }), - setSystemVersion: version => set({ systemVersion: version }), + setAppVersion: (version: string) => set({ appVersion: version }), + setSystemVersion: (version: string) => set({ systemVersion: version }), })); export interface DhcpLease { @@ -833,7 +798,7 @@ export const useMacrosStore = create((set, get) => ({ try { await new Promise((resolve, reject) => { - sendFn("getKeyboardMacros", {}, response => { + sendFn("getKeyboardMacros", {}, (response: JsonRpcResponse) => { if (response.error) { console.error("Error loading macros:", response.error); reject(new Error(response.error.message)); @@ -913,7 +878,7 @@ export const useMacrosStore = create((set, get) => ({ sendFn( "setKeyboardMacros", { params: { macros: macrosWithSortOrder } }, - response => { + (response: JsonRpcResponse) => { resolve(response); }, ); diff --git a/ui/src/hooks/useJsonRpc.ts b/ui/src/hooks/useJsonRpc.ts index 92b56ff..5f088dc 100644 --- a/ui/src/hooks/useJsonRpc.ts +++ b/ui/src/hooks/useJsonRpc.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect } from "react"; -import { useRTCStore } from "@/hooks/stores"; +import { RTCState, useRTCStore } from "@/hooks/stores"; export interface JsonRpcRequest { jsonrpc: string; @@ -33,7 +33,7 @@ const callbackStore = new Map void>( let requestCounter = 0; export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) { - const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); + const rpcDataChannel = useRTCStore((state: RTCState) => state.rpcDataChannel); const send = useCallback( (method: string, params: unknown, callback?: (resp: JsonRpcResponse) => void) => { diff --git a/ui/src/hooks/useKeyboard.ts b/ui/src/hooks/useKeyboard.ts index 0ce1eef..a77946c 100644 --- a/ui/src/hooks/useKeyboard.ts +++ b/ui/src/hooks/useKeyboard.ts @@ -1,16 +1,14 @@ import { useCallback } from "react"; -import { useHidStore, useRTCStore } from "@/hooks/stores"; -import { useJsonRpc } from "@/hooks/useJsonRpc"; +import { KeysDownState, HidState, useHidStore, RTCState, useRTCStore } from "@/hooks/stores"; +import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { keys, modifiers } from "@/keyboardMappings"; export default function useKeyboard() { const [send] = useJsonRpc(); - const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); - const updateActiveKeysAndModifiers = useHidStore( - state => state.updateActiveKeysAndModifiers, - ); + const rpcDataChannel = useRTCStore((state: RTCState) => state.rpcDataChannel); + const setKeysDownState = useHidStore((state: HidState) => state.setKeysDownState); const sendKeyboardEvent = useCallback( (keys: number[], modifiers: number[]) => { @@ -18,11 +16,28 @@ export default function useKeyboard() { const accModifier = modifiers.reduce((acc, val) => acc + val, 0); send("keyboardReport", { keys, modifier: accModifier }); - + //TODO would be nice if the keyboardReport rpc call returned the current state like keypressReport does // We do this for the info bar to display the currently pressed keys for the user - updateActiveKeysAndModifiers({ keys: keys, modifiers: modifiers }); + setKeysDownState({ keys: keys, modifier: accModifier }); }, - [rpcDataChannel?.readyState, send, updateActiveKeysAndModifiers], + [rpcDataChannel?.readyState, send, setKeysDownState], + ); + + const sendKeypressEvent = useCallback( + (key: number, press: boolean) => { + if (rpcDataChannel?.readyState !== "open") return; + + send("keypressReport", { key, press }, (resp: JsonRpcResponse) => { + if ("error" in resp) { + console.error("Failed to send keypress:", resp.error); + } else { + const keyDownState = resp.result as KeysDownState; + // We do this for the info bar to display the currently pressed keys for the user + setKeysDownState(keyDownState); + } + }); + }, + [rpcDataChannel?.readyState, send, setKeysDownState], ); const resetKeyboardState = useCallback(() => { @@ -52,5 +67,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..7122254 100644 --- a/ui/src/keyboardMappings.ts +++ b/ui/src/keyboardMappings.ts @@ -14,7 +14,7 @@ export const keys = { CapsLock: 0x39, Comma: 0x36, Compose: 0x65, - ContextMenu: 0, + ContextMenu: 0x65, // same as Compose Delete: 0x4c, Digit0: 0x27, Digit1: 0x1e, @@ -42,6 +42,7 @@ export const keys = { F10: 0x43, F11: 0x44, F12: 0x45, + F13: 0x68, F14: 0x69, F15: 0x6a, F16: 0x6b, @@ -120,6 +121,14 @@ export const keys = { Space: 0x2c, SystemRequest: 0x9a, Tab: 0x2b, + ControlLeft: 0xe0, + ControlRight: 0xe4, + ShiftLeft: 0xe1, + ShiftRight: 0xe5, + AltLeft: 0xe2, + AltRight: 0xe6, + MetaLeft: 0xe3, + MetaRight: 0xe7, } as Record; export const modifiers = { 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.sidebarView); + const sidebarView = useUiStore((state: UIState) => state.sidebarView); const [queryParams, setQueryParams] = useSearchParams(); - const setIsTurnServerInUse = useRTCStore(state => state.setTurnServerInUse); - const peerConnection = useRTCStore(state => state.peerConnection); - const setPeerConnectionState = useRTCStore(state => state.setPeerConnectionState); - const peerConnectionState = useRTCStore(state => state.peerConnectionState); - const setMediaMediaStream = useRTCStore(state => state.setMediaStream); - const setPeerConnection = useRTCStore(state => state.setPeerConnection); - const setDiskChannel = useRTCStore(state => state.setDiskChannel); - const setRpcDataChannel = useRTCStore(state => state.setRpcDataChannel); - const setTransceiver = useRTCStore(state => state.setTransceiver); + const setIsTurnServerInUse = useRTCStore((state: RTCState) => state.setTurnServerInUse); + const peerConnection = useRTCStore((state: RTCState) => state.peerConnection); + const setPeerConnectionState = useRTCStore((state: RTCState) => state.setPeerConnectionState); + const peerConnectionState = useRTCStore((state: RTCState) => state.peerConnectionState); + const setMediaMediaStream = useRTCStore((state: RTCState) => state.setMediaStream); + const setPeerConnection = useRTCStore((state: RTCState) => state.setPeerConnection); + const setDiskChannel = useRTCStore((state: RTCState) => state.setDiskChannel); + const setRpcDataChannel = useRTCStore((state: RTCState) => state.setRpcDataChannel); + const setTransceiver = useRTCStore((state: RTCState) => state.setTransceiver); const location = useLocation(); const isLegacySignalingEnabled = useRef(false); @@ -512,9 +517,9 @@ export default function KvmIdRoute() { }, [peerConnectionState, cleanupAndStopReconnecting]); // Cleanup effect - const clearInboundRtpStats = useRTCStore(state => state.clearInboundRtpStats); - const clearCandidatePairStats = useRTCStore(state => state.clearCandidatePairStats); - const setSidebarView = useUiStore(state => state.setSidebarView); + const clearInboundRtpStats = useRTCStore((state: RTCState) => state.clearInboundRtpStats); + const clearCandidatePairStats = useRTCStore((state: RTCState) => state.clearCandidatePairStats); + const setSidebarView = useUiStore((state: UIState) => state.setSidebarView); useEffect(() => { return () => { @@ -549,7 +554,7 @@ export default function KvmIdRoute() { }, [peerConnectionState, setIsTurnServerInUse]); // TURN server usage reporting - const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse); + const isTurnServerInUse = useRTCStore((state: RTCState) => state.isTurnServerInUse); const lastBytesReceived = useRef(0); const lastBytesSent = useRef(0); @@ -582,15 +587,16 @@ export default function KvmIdRoute() { }); }, 10000); - const setNetworkState = useNetworkStateStore(state => state.setNetworkState); + const setNetworkState = useNetworkStateStore((state: NetworkState) => state.setNetworkState); - const setUsbState = useHidStore(state => state.setUsbState); - const setHdmiState = useVideoStore(state => state.setHdmiState); + const setUsbState = useHidStore((state: HidState) => state.setUsbState); + const setHdmiState = useVideoStore((state: VideoState) => state.setHdmiState); - const keyboardLedState = useHidStore(state => state.keyboardLedState); - const setKeyboardLedState = useHidStore(state => state.setKeyboardLedState); + const keyboardLedState = useHidStore((state: HidState) => state.keyboardLedState); + const setKeyboardLedState = useHidStore((state: HidState) => state.setKeyboardLedState); - const setKeyboardLedStateSyncAvailable = useHidStore(state => state.setKeyboardLedStateSyncAvailable); + const keysDownState = useHidStore((state: HidState) => state.keysDownState); + const setKeysDownState = useHidStore((state: HidState) => state.setKeysDownState); const [hasUpdated, setHasUpdated] = useState(false); const { navigateTo } = useDeviceUiNavigation(); @@ -617,7 +623,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") { @@ -645,12 +656,12 @@ export default function KvmIdRoute() { } } - const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); + const rpcDataChannel = useRTCStore((state: RTCState) => state.rpcDataChannel); const [send] = useJsonRpc(onJsonRpcRequest); useEffect(() => { if (rpcDataChannel?.readyState !== "open") return; - send("getVideoState", {}, resp => { + send("getVideoState", {}, (resp: JsonRpcResponse) => { if ("error" in resp) return; setHdmiState(resp.result as Parameters[0]); }); @@ -662,22 +673,31 @@ export default function KvmIdRoute() { if (keyboardLedState !== undefined) return; console.log("Requesting keyboard led state"); - send("getKeyboardLedState", {}, resp => { + send("getKeyboardLedState", {}, (resp: JsonRpcResponse) => { 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: JsonRpcResponse) => { + 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(() => { @@ -686,8 +706,8 @@ export default function KvmIdRoute() { } }, [navigate, navigateTo, queryParams, setModalView, setQueryParams]); - const diskChannel = useRTCStore(state => state.diskChannel)!; - const file = useMountMediaStore(state => state.localFile)!; + const diskChannel = useRTCStore((state: RTCState) => state.diskChannel)!; + const file = useMountMediaStore((state: MountMediaState) => state.localFile)!; useEffect(() => { if (!diskChannel || !file) return; diskChannel.onmessage = async e => { @@ -707,7 +727,7 @@ export default function KvmIdRoute() { }, [diskChannel, file]); // System update - const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap); + const disableVideoFocusTrap = useUiStore((state: UIState) => state.disableVideoFocusTrap); const [kvmTerminal, setKvmTerminal] = useState(null); const [serialConsole, setSerialConsole] = useState(null); @@ -728,14 +748,14 @@ export default function KvmIdRoute() { if (location.pathname !== "/other-session") navigateTo("/"); }, [navigateTo, location.pathname]); - const appVersion = useDeviceStore(state => state.appVersion); - const setAppVersion = useDeviceStore(state => state.setAppVersion); - const setSystemVersion = useDeviceStore(state => state.setSystemVersion); + const appVersion = useDeviceStore((state: DeviceState) => state.appVersion); + const setAppVersion = useDeviceStore((state: DeviceState) => state.setAppVersion); + const setSystemVersion = useDeviceStore((state: DeviceState) => state.setSystemVersion); useEffect(() => { if (appVersion) return; - send("getUpdateStatus", {}, async resp => { + send("getUpdateStatus", {}, async (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error(`Failed to get device version: ${resp.error}`); return diff --git a/usb.go b/usb.go index f777f89..5c8036c 100644 --- a/usb.go +++ b/usb.go @@ -31,21 +31,31 @@ 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 rpcAbsMouseReport(x, y int, buttons uint8) error { +func rpcKeypressReport(key byte, press bool) (usbgadget.KeysDownState, error) { + return gadget.KeypressReport(key, press) +} + +func rpcAbsMouseReport(x int, y int, buttons uint8) error { return gadget.AbsMouseReport(x, y, buttons) } -func rpcRelMouseReport(dx, dy int8, buttons uint8) error { +func rpcRelMouseReport(dx int8, dy int8, buttons uint8) error { return gadget.RelMouseReport(dx, dy, 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 diff --git a/wol.go b/wol.go index 02b5c96..c3d0de2 100644 --- a/wol.go +++ b/wol.go @@ -65,7 +65,7 @@ func createMagicPacket(mac net.HardwareAddr) []byte { buf.Write(bytes.Repeat([]byte{0xFF}, 6)) // Write the target MAC address 16 times - for i := 0; i < 16; i++ { + for range 16 { _ = binary.Write(&buf, binary.BigEndian, mac) }