From deb258b7178af0c541f30d5be8b6602a936b4ced Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 24 Sep 2025 19:20:51 -0500 Subject: [PATCH 1/9] Reduce traffic during pastes Suspend KeyDownMessages while processing a macro. Make sure we don't emit huge debugging traces. Allow 30 seconds for RPC to finish (not ideal) Reduced default delay between keys (and allow as low as 0) Move the HID keyboard descriptor LED state as it seems to interfere with boot mode Run paste/macros in background on their own queue and return a token for cancellation. Fixed error in length check for macro key state. Removed redundant clear operation. Use Once instead of init() Add a time limit for each message type/queue. Update ui/src/components/popovers/PasteModal.tsx --- cloud.go | 2 +- hidrpc.go | 42 +++++- internal/hidrpc/hidrpc.go | 40 ++++-- internal/hidrpc/message.go | 72 +++++++++- internal/usbgadget/hid_keyboard.go | 61 +++++++-- jsonrpc.go | 153 +++++++++++++++------- ui/package-lock.json | 10 +- ui/package.json | 1 + ui/src/components/popovers/PasteModal.tsx | 2 +- ui/src/hooks/hidRpc.ts | 27 +++- ui/src/hooks/useHidRpc.ts | 2 +- ui/src/hooks/useKeyboard.ts | 19 ++- web.go | 2 +- webrtc.go | 56 ++++---- 14 files changed, 367 insertions(+), 122 deletions(-) diff --git a/cloud.go b/cloud.go index dbbd3bbc..479ef2b0 100644 --- a/cloud.go +++ b/cloud.go @@ -478,7 +478,7 @@ func handleSessionRequest( cloudLogger.Trace().Interface("session", session).Msg("new session accepted") // Cancel any ongoing keyboard macro when session changes - cancelKeyboardMacro() + cancelAllRunningKeyboardMacros() currentSession = session _ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd}) diff --git a/hidrpc.go b/hidrpc.go index ebe03daa..9e993016 100644 --- a/hidrpc.go +++ b/hidrpc.go @@ -26,20 +26,45 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) { return } session.hidRPCAvailable = true + case hidrpc.TypeKeypressReport, hidrpc.TypeKeyboardReport: rpcErr = handleHidRPCKeyboardInput(message) + case hidrpc.TypeKeyboardMacroReport: keyboardMacroReport, err := message.KeyboardMacroReport() if err != nil { logger.Warn().Err(err).Msg("failed to get keyboard macro report") return } - rpcErr = rpcExecuteKeyboardMacro(keyboardMacroReport.Steps) + token := rpcExecuteKeyboardMacro(keyboardMacroReport.IsPaste, keyboardMacroReport.Steps) + logger.Debug().Str("token", token.String()).Msg("started keyboard macro") + message, err := hidrpc.NewKeyboardMacroTokenMessage(token).Marshal() + + if err != nil { + logger.Warn().Err(err).Msg("failed to marshal running macro token message") + return + } + if err := session.HidChannel.Send(message); err != nil { + logger.Warn().Err(err).Msg("failed to send running macro token message") + return + } + case hidrpc.TypeCancelKeyboardMacroReport: rpcCancelKeyboardMacro() return + + case hidrpc.TypeKeyboardMacroTokenState: + tokenState, err := message.KeyboardMacroTokenState() + if err != nil { + logger.Warn().Err(err).Msg("failed to get keyboard macro token") + return + } + rpcCancelKeyboardMacroByToken(tokenState.Token) + return + case hidrpc.TypeKeypressKeepAliveReport: rpcErr = handleHidRPCKeypressKeepAlive(session) + case hidrpc.TypePointerReport: pointerReport, err := message.PointerReport() if err != nil { @@ -47,6 +72,7 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) { return } rpcErr = rpcAbsMouseReport(pointerReport.X, pointerReport.Y, pointerReport.Button) + case hidrpc.TypeMouseReport: mouseReport, err := message.MouseReport() if err != nil { @@ -54,6 +80,7 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) { return } rpcErr = rpcRelMouseReport(mouseReport.DX, mouseReport.DY, mouseReport.Button) + default: logger.Warn().Uint8("type", uint8(message.Type())).Msg("unknown HID RPC message type") } @@ -65,15 +92,18 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) { func onHidMessage(msg hidQueueMessage, session *Session) { data := msg.Data + dataLen := len(data) scopedLogger := hidRPCLogger.With(). Str("channel", msg.channel). - Bytes("data", data). + Dur("timelimit", msg.timelimit). + Int("data_len", dataLen). + Bytes("data", data[:min(dataLen, 32)]). Logger() scopedLogger.Debug().Msg("HID RPC message received") - if len(data) < 1 { - scopedLogger.Warn().Int("length", len(data)).Msg("received empty data in HID RPC message handler") + if dataLen < 1 { + scopedLogger.Warn().Msg("received empty data in HID RPC message handler") return } @@ -96,7 +126,7 @@ func onHidMessage(msg hidQueueMessage, session *Session) { r <- nil }() select { - case <-time.After(1 * time.Second): + case <-time.After(msg.timelimit * time.Second): scopedLogger.Warn().Msg("HID RPC message timed out") case <-r: scopedLogger.Debug().Dur("duration", time.Since(t)).Msg("HID RPC message handled") @@ -212,6 +242,8 @@ func reportHidRPC(params any, session *Session) { message, err = hidrpc.NewKeydownStateMessage(params).Marshal() case hidrpc.KeyboardMacroState: message, err = hidrpc.NewKeyboardMacroStateMessage(params.State, params.IsPaste).Marshal() + case hidrpc.KeyboardMacroTokenState: + message, err = hidrpc.NewKeyboardMacroTokenMessage(params.Token).Marshal() default: err = fmt.Errorf("unknown HID RPC message type: %T", params) } diff --git a/internal/hidrpc/hidrpc.go b/internal/hidrpc/hidrpc.go index 7313e3b5..e861fe32 100644 --- a/internal/hidrpc/hidrpc.go +++ b/internal/hidrpc/hidrpc.go @@ -2,7 +2,9 @@ package hidrpc import ( "fmt" + "time" + "github.com/google/uuid" "github.com/jetkvm/kvm/internal/usbgadget" ) @@ -22,26 +24,34 @@ const ( TypeKeyboardLedState MessageType = 0x32 TypeKeydownState MessageType = 0x33 TypeKeyboardMacroState MessageType = 0x34 + TypeKeyboardMacroTokenState MessageType = 0x35 ) +type QueueIndex int + const ( - Version byte = 0x01 // Version of the HID RPC protocol + Version byte = 0x01 // Version of the HID RPC protocol + HandshakeQueue int = 0 // Queue index for handshake messages + KeyboardQueue int = 1 // Queue index for keyboard messages + MouseQueue int = 2 // Queue index for mouse messages + MacroQueue int = 3 // Queue index for macro messages + OtherQueue int = 4 // Queue index for other messages ) // GetQueueIndex returns the index of the queue to which the message should be enqueued. -func GetQueueIndex(messageType MessageType) int { +func GetQueueIndex(messageType MessageType) (int, time.Duration) { switch messageType { case TypeHandshake: - return 0 - case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardMacroReport, TypeKeyboardLedState, TypeKeydownState, TypeKeyboardMacroState: - return 1 + return HandshakeQueue, 1 + case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardLedState, TypeKeydownState, TypeKeyboardMacroState: + return KeyboardQueue, 1 case TypePointerReport, TypeMouseReport, TypeWheelReport: - return 2 - // we don't want to block the queue for this message - case TypeCancelKeyboardMacroReport: - return 3 + return MouseQueue, 1 + // we don't want to block the queue for these messages + case TypeKeyboardMacroReport, TypeCancelKeyboardMacroReport, TypeKeyboardMacroTokenState: + return MacroQueue, 60 // 1 minute timeout default: - return 3 + return OtherQueue, 5 } } @@ -121,3 +131,13 @@ func NewKeyboardMacroStateMessage(state bool, isPaste bool) *Message { d: data, } } + +// NewKeyboardMacroTokenMessage creates a new keyboard macro token message. +func NewKeyboardMacroTokenMessage(token uuid.UUID) *Message { + data, _ := token.MarshalBinary() + + return &Message{ + t: TypeKeyboardMacroTokenState, + d: data, + } +} diff --git a/internal/hidrpc/message.go b/internal/hidrpc/message.go index 3f3506f7..381801f4 100644 --- a/internal/hidrpc/message.go +++ b/internal/hidrpc/message.go @@ -3,6 +3,8 @@ package hidrpc import ( "encoding/binary" "fmt" + + "github.com/google/uuid" ) // Message .. @@ -23,6 +25,9 @@ func (m *Message) Type() MessageType { func (m *Message) String() string { switch m.t { case TypeHandshake: + if len(m.d) != 0 { + return fmt.Sprintf("Handshake{Malformed: %v}", m.d) + } return "Handshake" case TypeKeypressReport: if len(m.d) < 2 { @@ -45,12 +50,45 @@ func (m *Message) String() string { } return fmt.Sprintf("MouseReport{DX: %d, DY: %d, Button: %d}", m.d[0], m.d[1], m.d[2]) case TypeKeypressKeepAliveReport: + if len(m.d) != 0 { + return fmt.Sprintf("KeypressKeepAliveReport{Malformed: %v}", m.d) + } return "KeypressKeepAliveReport" + case TypeWheelReport: + if len(m.d) < 3 { + return fmt.Sprintf("WheelReport{Malformed: %v}", m.d) + } + return fmt.Sprintf("WheelReport{Vertical: %d, Horizontal: %d}", int8(m.d[0]), int8(m.d[1])) case TypeKeyboardMacroReport: if len(m.d) < 5 { return fmt.Sprintf("KeyboardMacroReport{Malformed: %v}", m.d) } return fmt.Sprintf("KeyboardMacroReport{IsPaste: %v, Length: %d}", m.d[0] == uint8(1), binary.BigEndian.Uint32(m.d[1:5])) + case TypeCancelKeyboardMacroReport: + if len(m.d) != 0 { + return fmt.Sprintf("CancelKeyboardMacroReport{Malformed: %v}", m.d) + } + return "CancelKeyboardMacroReport" + case TypeKeyboardMacroTokenState: + if len(m.d) != 16 { + return fmt.Sprintf("KeyboardMacroTokenState{Malformed: %v}", m.d) + } + return fmt.Sprintf("KeyboardMacroTokenState{Token: %s}", uuid.Must(uuid.FromBytes(m.d)).String()) + case TypeKeyboardLedState: + if len(m.d) < 1 { + return fmt.Sprintf("KeyboardLedState{Malformed: %v}", m.d) + } + return fmt.Sprintf("KeyboardLedState{State: %d}", m.d[0]) + case TypeKeydownState: + if len(m.d) < 1 { + return fmt.Sprintf("KeydownState{Malformed: %v}", m.d) + } + return fmt.Sprintf("KeydownState{State: %d}", m.d[0]) + case TypeKeyboardMacroState: + if len(m.d) < 2 { + return fmt.Sprintf("KeyboardMacroState{Malformed: %v}", m.d) + } + return fmt.Sprintf("KeyboardMacroState{State: %v, IsPaste: %v}", m.d[0] == uint8(1), m.d[1] == uint8(1)) default: return fmt.Sprintf("Unknown{Type: %d, Data: %v}", m.t, m.d) } @@ -67,7 +105,9 @@ func (m *Message) KeypressReport() (KeypressReport, error) { if m.t != TypeKeypressReport { return KeypressReport{}, fmt.Errorf("invalid message type: %d", m.t) } - + if len(m.d) < 2 { + return KeypressReport{}, fmt.Errorf("invalid message data length: %d", len(m.d)) + } return KeypressReport{ Key: m.d[0], Press: m.d[1] == uint8(1), @@ -95,7 +135,7 @@ func (m *Message) KeyboardReport() (KeyboardReport, error) { // Macro .. type KeyboardMacroStep struct { Modifier byte // 1 byte - Keys []byte // 6 bytes: hidKeyBufferSize + Keys []byte // 6 bytes: HidKeyBufferSize Delay uint16 // 2 bytes } type KeyboardMacroReport struct { @@ -105,7 +145,7 @@ type KeyboardMacroReport struct { } // HidKeyBufferSize is the size of the keys buffer in the keyboard report. -const HidKeyBufferSize = 6 +const HidKeyBufferSize int = 6 // KeyboardMacroReport returns the keyboard macro report from the message. func (m *Message) KeyboardMacroReport() (KeyboardMacroReport, error) { @@ -205,3 +245,29 @@ func (m *Message) KeyboardMacroState() (KeyboardMacroState, error) { IsPaste: m.d[1] == uint8(1), }, nil } + +type KeyboardMacroTokenState struct { + Token uuid.UUID +} + +// KeyboardMacroTokenState returns the keyboard macro token UUID from the message. +func (m *Message) KeyboardMacroTokenState() (KeyboardMacroTokenState, error) { + if m.t != TypeKeyboardMacroTokenState { + return KeyboardMacroTokenState{}, fmt.Errorf("invalid message type: %d", m.t) + } + + if len(m.d) == 0 { + return KeyboardMacroTokenState{Token: uuid.Nil}, nil + } + + if len(m.d) != 16 { + return KeyboardMacroTokenState{}, fmt.Errorf("invalid UUID length: %d", len(m.d)) + } + + token, err := uuid.FromBytes(m.d) + if err != nil { + return KeyboardMacroTokenState{}, fmt.Errorf("invalid UUID: %v", err) + } + + return KeyboardMacroTokenState{Token: token}, nil +} diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go index 74cf76f9..05895206 100644 --- a/internal/usbgadget/hid_keyboard.go +++ b/internal/usbgadget/hid_keyboard.go @@ -31,6 +31,8 @@ var keyboardReportDesc = []byte{ 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ 0x09, 0x06, /* USAGE (Keyboard) */ 0xa1, 0x01, /* COLLECTION (Application) */ + + /* 8 modifier bits */ 0x05, 0x07, /* USAGE_PAGE (Keyboard) */ 0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */ 0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */ @@ -39,27 +41,47 @@ var keyboardReportDesc = []byte{ 0x75, 0x01, /* REPORT_SIZE (1) */ 0x95, 0x08, /* REPORT_COUNT (8) */ 0x81, 0x02, /* INPUT (Data,Var,Abs) */ + + /* 8 bits of padding */ 0x95, 0x01, /* REPORT_COUNT (1) */ 0x75, 0x08, /* REPORT_SIZE (8) */ 0x81, 0x03, /* INPUT (Cnst,Var,Abs) */ + + /* 6 key codes for the 104 key keyboard */ + 0x95, 0x06, /* REPORT_COUNT (6) */ + 0x75, 0x08, /* REPORT_SIZE (8) */ + 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ + 0x25, 0xE7, /* LOGICAL_MAXIMUM (104-key HID) */ + 0x05, 0x07, /* USAGE_PAGE (Keyboard) */ + 0x19, 0x00, /* USAGE_MINIMUM (Reserved) */ + 0x29, 0xE7, /* USAGE_MAXIMUM (Keyboard Right GUI) */ + 0x81, 0x00, /* INPUT (Data,Ary,Abs) */ + + /* LED report 5 bits for Num Lock through Kana */ 0x95, 0x05, /* REPORT_COUNT (5) */ 0x75, 0x01, /* REPORT_SIZE (1) */ - 0x05, 0x08, /* USAGE_PAGE (LEDs) */ 0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */ 0x29, 0x05, /* USAGE_MAXIMUM (Kana) */ 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ + + /* 1 bit of padding for the Power LED (ignored) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x75, 0x03, /* REPORT_SIZE (3) */ + 0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */ + + /* LED report 1 bit for Shift */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x05, 0x08, /* USAGE_PAGE (LEDs) */ + 0x19, 0x07, /* USAGE_MINIMUM (Shift) */ + 0x29, 0x07, /* USAGE_MAXIMUM (Shift) */ + 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ + + /* 1 bit of padding for the rest of the byte */ 0x95, 0x01, /* REPORT_COUNT (1) */ 0x75, 0x03, /* REPORT_SIZE (3) */ 0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */ - 0x95, 0x06, /* REPORT_COUNT (6) */ - 0x75, 0x08, /* REPORT_SIZE (8) */ - 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ - 0x25, 0x65, /* LOGICAL_MAXIMUM (101) */ - 0x05, 0x07, /* USAGE_PAGE (Keyboard) */ - 0x19, 0x00, /* USAGE_MINIMUM (Reserved) */ - 0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */ - 0x81, 0x00, /* INPUT (Data,Ary,Abs) */ 0xc0, /* END_COLLECTION */ } @@ -153,6 +175,16 @@ func (u *UsbGadget) SetOnKeysDownChange(f func(state KeysDownState)) { u.onKeysDownChange = &f } +var suspendedKeyDownMessages bool = false + +func (u *UsbGadget) SuspendKeyDownMessages() { + suspendedKeyDownMessages = true +} + +func (u *UsbGadget) ResumeSuspendKeyDownMessages() { + suspendedKeyDownMessages = false +} + func (u *UsbGadget) SetOnKeepAliveReset(f func()) { u.onKeepAliveReset = &f } @@ -169,9 +201,9 @@ func (u *UsbGadget) scheduleAutoRelease(key byte) { } // TODO: make this configurable - // We currently hardcode the duration to 100ms + // We currently hardcode the duration to the default of 100ms // However, it should be the same as the duration of the keep-alive reset called baseExtension. - u.kbdAutoReleaseTimers[key] = time.AfterFunc(100*time.Millisecond, func() { + u.kbdAutoReleaseTimers[key] = time.AfterFunc(DefaultAutoReleaseDuration, func() { u.performAutoRelease(key) }) } @@ -314,6 +346,7 @@ var keyboardWriteHidFileLock sync.Mutex func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error { keyboardWriteHidFileLock.Lock() defer keyboardWriteHidFileLock.Unlock() + if err := u.openKeyboardHidFile(); err != nil { return err } @@ -353,7 +386,7 @@ func (u *UsbGadget) UpdateKeysDown(modifier byte, keys []byte) KeysDownState { u.keysDownState = state u.keyboardStateLock.Unlock() - if u.onKeysDownChange != nil { + if u.onKeysDownChange != nil && !suspendedKeyDownMessages { (*u.onKeysDownChange)(state) // this enques to the outgoing hidrpc queue via usb.go → currentSession.enqueueKeysDownState(...) } return state @@ -484,6 +517,10 @@ func (u *UsbGadget) keypressReport(key byte, press bool) (KeysDownState, error) } err := u.keyboardWriteHidFile(modifier, keys) + if err != nil { + u.log.Warn().Uint8("modifier", modifier).Uints8("keys", keys).Msg("Could not write keyboard report to hidg0") + } + return u.UpdateKeysDown(modifier, keys), err } diff --git a/jsonrpc.go b/jsonrpc.go index 5ed90a7a..b39c5206 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -1,7 +1,6 @@ package kvm import ( - "bytes" "context" "encoding/json" "errors" @@ -14,6 +13,7 @@ import ( "sync" "time" + "github.com/google/uuid" "github.com/pion/webrtc/v4" "github.com/rs/zerolog" "go.bug.st/serial" @@ -1063,91 +1063,154 @@ func rpcSetLocalLoopbackOnly(enabled bool) error { return nil } +type RunningMacro struct { + cancel context.CancelFunc + isPaste bool +} + var ( - keyboardMacroCancel context.CancelFunc - keyboardMacroLock sync.Mutex + keyboardMacroCancelMap map[uuid.UUID]RunningMacro + keyboardMacroLock sync.Mutex + keyboardMacroOnce sync.Once ) -// cancelKeyboardMacro cancels any ongoing keyboard macro execution -func cancelKeyboardMacro() { +func getKeyboardMacroCancelMap() map[uuid.UUID]RunningMacro { + keyboardMacroOnce.Do(func() { + keyboardMacroCancelMap = make(map[uuid.UUID]RunningMacro) + }) + return keyboardMacroCancelMap +} + +func addKeyboardMacro(isPaste bool, cancel context.CancelFunc) uuid.UUID { keyboardMacroLock.Lock() defer keyboardMacroLock.Unlock() + cancelMap := getKeyboardMacroCancelMap() - if keyboardMacroCancel != nil { - keyboardMacroCancel() - logger.Info().Msg("canceled keyboard macro") - keyboardMacroCancel = nil + token := uuid.New() // Generate a unique token + cancelMap[token] = RunningMacro{ + isPaste: isPaste, + cancel: cancel, + } + return token +} + +func removeRunningKeyboardMacro(token uuid.UUID) { + keyboardMacroLock.Lock() + defer keyboardMacroLock.Unlock() + cancelMap := getKeyboardMacroCancelMap() + + delete(cancelMap, token) +} + +func cancelRunningKeyboardMacro(token uuid.UUID) { + keyboardMacroLock.Lock() + defer keyboardMacroLock.Unlock() + cancelMap := getKeyboardMacroCancelMap() + + if runningMacro, exists := cancelMap[token]; exists { + runningMacro.cancel() + delete(cancelMap, token) + logger.Info().Interface("token", token).Msg("canceled keyboard macro by token") + } else { + logger.Debug().Interface("token", token).Msg("no running keyboard macro found for token") } } -func setKeyboardMacroCancel(cancel context.CancelFunc) { +func cancelAllRunningKeyboardMacros() { keyboardMacroLock.Lock() defer keyboardMacroLock.Unlock() + cancelMap := getKeyboardMacroCancelMap() - keyboardMacroCancel = cancel + for token, runningMacro := range cancelMap { + runningMacro.cancel() + delete(cancelMap, token) + logger.Info().Interface("token", token).Msg("cancelled keyboard macro") + } } -func rpcExecuteKeyboardMacro(macro []hidrpc.KeyboardMacroStep) error { - cancelKeyboardMacro() +func reportRunningMacrosState() { + if currentSession != nil { + keyboardMacroLock.Lock() + defer keyboardMacroLock.Unlock() + cancelMap := getKeyboardMacroCancelMap() + isPaste := false + for _, runningMacro := range cancelMap { + if runningMacro.isPaste { + isPaste = true + break + } + } + + state := hidrpc.KeyboardMacroState{ + State: len(cancelMap) > 0, + IsPaste: isPaste, + } + + currentSession.reportHidRPCKeyboardMacroState(state) + } +} + +func rpcExecuteKeyboardMacro(isPaste bool, macro []hidrpc.KeyboardMacroStep) uuid.UUID { ctx, cancel := context.WithCancel(context.Background()) - setKeyboardMacroCancel(cancel) + token := addKeyboardMacro(isPaste, cancel) + reportRunningMacrosState() - s := hidrpc.KeyboardMacroState{ - State: true, - IsPaste: true, - } + go func() { + defer reportRunningMacrosState() // this executes last, so the map is already updated + defer removeRunningKeyboardMacro(token) // this executes first, to update the map - if currentSession != nil { - currentSession.reportHidRPCKeyboardMacroState(s) - } + err := executeKeyboardMacro(ctx, isPaste, macro) + if err != nil { + logger.Error().Err(err).Interface("token", token).Bool("isPaste", isPaste).Msg("keyboard macro execution failed") + } + }() - err := rpcDoExecuteKeyboardMacro(ctx, macro) - - setKeyboardMacroCancel(nil) - - s.State = false - if currentSession != nil { - currentSession.reportHidRPCKeyboardMacroState(s) - } - - return err + return token } func rpcCancelKeyboardMacro() { - cancelKeyboardMacro() + defer reportRunningMacrosState() + cancelAllRunningKeyboardMacros() } -var keyboardClearStateKeys = make([]byte, hidrpc.HidKeyBufferSize) +func rpcCancelKeyboardMacroByToken(token uuid.UUID) { + defer reportRunningMacrosState() -func isClearKeyStep(step hidrpc.KeyboardMacroStep) bool { - return step.Modifier == 0 && bytes.Equal(step.Keys, keyboardClearStateKeys) + if token == uuid.Nil { + cancelAllRunningKeyboardMacros() + } else { + cancelRunningKeyboardMacro(token) + } } -func rpcDoExecuteKeyboardMacro(ctx context.Context, macro []hidrpc.KeyboardMacroStep) error { - logger.Debug().Interface("macro", macro).Msg("Executing keyboard macro") +func executeKeyboardMacro(ctx context.Context, isPaste bool, macro []hidrpc.KeyboardMacroStep) error { + logger.Debug(). + Int("macro_steps", len(macro)). + Bool("isPaste", isPaste). + Msg("Executing keyboard macro") + + // don't report keyboard state changes while executing the macro + gadget.SuspendKeyDownMessages() + defer gadget.ResumeSuspendKeyDownMessages() for i, step := range macro { delay := time.Duration(step.Delay) * time.Millisecond err := rpcKeyboardReport(step.Modifier, step.Keys) if err != nil { - logger.Warn().Err(err).Msg("failed to execute keyboard macro") + logger.Warn().Err(err).Int("step", i).Msg("failed to execute keyboard macro") return err } - // notify the device that the keyboard state is being cleared - if isClearKeyStep(step) { - gadget.UpdateKeysDown(0, keyboardClearStateKeys) - } - // Use context-aware sleep that can be cancelled select { case <-time.After(delay): // Sleep completed normally case <-ctx.Done(): - // make sure keyboard state is reset - err := rpcKeyboardReport(0, keyboardClearStateKeys) + // make sure keyboard state is reset and the client gets notified + gadget.ResumeSuspendKeyDownMessages() + err := rpcKeyboardReport(0, make([]byte, hidrpc.HidKeyBufferSize)) if err != nil { logger.Warn().Err(err).Msg("failed to reset keyboard state") } diff --git a/ui/package-lock.json b/ui/package-lock.json index c3f2ab35..25b17a53 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -38,6 +38,7 @@ "recharts": "^3.3.0", "tailwind-merge": "^3.3.1", "usehooks-ts": "^3.1.1", + "uuid": "^13.0.0", "validator": "^13.15.15", "zustand": "^4.5.2" }, @@ -7642,17 +7643,16 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "dev": true, + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/validator": { diff --git a/ui/package.json b/ui/package.json index 6c1016d6..215a86a0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -57,6 +57,7 @@ "recharts": "^3.3.0", "tailwind-merge": "^3.3.1", "usehooks-ts": "^3.1.1", + "uuid": "^13.0.0", "validator": "^13.15.15", "zustand": "^4.5.2" }, diff --git a/ui/src/components/popovers/PasteModal.tsx b/ui/src/components/popovers/PasteModal.tsx index ccc5d307..ab8de215 100644 --- a/ui/src/components/popovers/PasteModal.tsx +++ b/ui/src/components/popovers/PasteModal.tsx @@ -196,7 +196,7 @@ export default function PasteModal() { setDelayValue(parseInt(e.target.value, 10)); }} /> - {delayValue < 50 || delayValue > 65534 && ( + {(delayValue < defaultDelay || delayValue > 65534) && (
diff --git a/ui/src/hooks/hidRpc.ts b/ui/src/hooks/hidRpc.ts index a429f053..dbdbfde1 100644 --- a/ui/src/hooks/hidRpc.ts +++ b/ui/src/hooks/hidRpc.ts @@ -1,3 +1,5 @@ +import { parse as uuidParse , stringify as uuidStringify } from "uuid"; + import { hidKeyBufferSize, KeyboardLedState, KeysDownState } from "./stores"; export const HID_RPC_MESSAGE_TYPES = { @@ -13,6 +15,7 @@ export const HID_RPC_MESSAGE_TYPES = { KeyboardLedState: 0x32, KeysDownState: 0x33, KeyboardMacroState: 0x34, + CancelKeyboardMacroByTokenReport: 0x35, } export type HidRpcMessageType = typeof HID_RPC_MESSAGE_TYPES[keyof typeof HID_RPC_MESSAGE_TYPES]; @@ -300,7 +303,7 @@ export class KeyboardMacroStateMessage extends RpcMessage { } public static unmarshal(data: Uint8Array): KeyboardMacroStateMessage | undefined { - if (data.length < 1) { + if (data.length < 2) { throw new Error(`Invalid keyboard macro state report message length: ${data.length}`); } @@ -379,13 +382,30 @@ export class PointerReportMessage extends RpcMessage { } export class CancelKeyboardMacroReportMessage extends RpcMessage { + token: string; - constructor() { + constructor(token: string) { super(HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroReport); + this.token = (token == null || token === undefined || token === "") + ? "00000000-0000-0000-0000-000000000000" + : token; } marshal(): Uint8Array { - return new Uint8Array([this.messageType]); + const tokenBytes = uuidParse(this.token); + return new Uint8Array([this.messageType, ...tokenBytes]); + } + + public static unmarshal(data: Uint8Array): CancelKeyboardMacroReportMessage | undefined { + if (data.length == 0) { + return new CancelKeyboardMacroReportMessage("00000000-0000-0000-0000-000000000000"); + } + + if (data.length != 16) { + throw new Error(`Invalid cancel message length: ${data.length}`); + } + + return new CancelKeyboardMacroReportMessage(uuidStringify(data.slice(0, 16))); } } @@ -431,6 +451,7 @@ export const messageRegistry = { [HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroReport]: CancelKeyboardMacroReportMessage, [HID_RPC_MESSAGE_TYPES.KeyboardMacroState]: KeyboardMacroStateMessage, [HID_RPC_MESSAGE_TYPES.KeypressKeepAliveReport]: KeypressKeepAliveMessage, + [HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroByTokenReport]: CancelKeyboardMacroReportMessage, } export const unmarshalHidRpcMessage = (data: Uint8Array): RpcMessage | undefined => { diff --git a/ui/src/hooks/useHidRpc.ts b/ui/src/hooks/useHidRpc.ts index 2db8279f..9d677fb3 100644 --- a/ui/src/hooks/useHidRpc.ts +++ b/ui/src/hooks/useHidRpc.ts @@ -142,7 +142,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) { const cancelOngoingKeyboardMacro = useCallback( () => { - sendMessage(new CancelKeyboardMacroReportMessage()); + sendMessage(new CancelKeyboardMacroReportMessage("")); }, [sendMessage], ); diff --git a/ui/src/hooks/useKeyboard.ts b/ui/src/hooks/useKeyboard.ts index 4c4d2d43..1e5f4219 100644 --- a/ui/src/hooks/useKeyboard.ts +++ b/ui/src/hooks/useKeyboard.ts @@ -287,13 +287,11 @@ export default function useKeyboard() { async (steps: MacroSteps) => { const macro: KeyboardMacroStep[] = []; - for (const [_, step] of steps.entries()) { - const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean); - const modifierMask: number = (step.modifiers || []) - - .map(mod => modifiers[mod]) - - .reduce((acc, val) => acc + val, 0); + for (const [_, step] of steps.entries()) { + const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean); + const modifierMask: number = (step.modifiers || []) + .map(mod => modifiers[mod]) + .reduce((acc, val) => acc + val, 0); // If the step has keys and/or modifiers, press them and hold for the delay if (keyValues.length > 0 || modifierMask > 0) { @@ -302,10 +300,9 @@ export default function useKeyboard() { } } - sendKeyboardMacroEventHidRpc(macro); - }, - [sendKeyboardMacroEventHidRpc], - ); + sendKeyboardMacroEventHidRpc(macro); + }, [sendKeyboardMacroEventHidRpc]); + const executeMacroClientSide = useCallback( async (steps: MacroSteps) => { diff --git a/web.go b/web.go index 0fd968b8..41b5ef9c 100644 --- a/web.go +++ b/web.go @@ -230,7 +230,7 @@ func handleWebRTCSession(c *gin.Context) { } // Cancel any ongoing keyboard macro when session changes - cancelKeyboardMacro() + cancelAllRunningKeyboardMacros() currentSession = session c.JSON(http.StatusOK, gin.H{"sd": sd}) diff --git a/webrtc.go b/webrtc.go index 37488f77..5519ee34 100644 --- a/webrtc.go +++ b/webrtc.go @@ -34,7 +34,7 @@ type Session struct { lastTimerResetTime time.Time // Track when auto-release timer was last reset keepAliveJitterLock sync.Mutex // Protect jitter compensation timing state hidQueueLock sync.Mutex - hidQueue []chan hidQueueMessage + hidQueues []chan hidQueueMessage keysDownStateQueue chan usbgadget.KeysDownState } @@ -76,7 +76,8 @@ func (s *Session) resetKeepAliveTime() { type hidQueueMessage struct { webrtc.DataChannelMessage - channel string + channel string + timelimit time.Duration } type SessionConfig struct { @@ -121,19 +122,20 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) { return base64.StdEncoding.EncodeToString(localDescription), nil } -func (s *Session) initQueues() { +func (s *Session) initHidQueues() { s.hidQueueLock.Lock() defer s.hidQueueLock.Unlock() - s.hidQueue = make([]chan hidQueueMessage, 0) - for i := 0; i < 4; i++ { - q := make(chan hidQueueMessage, 256) - s.hidQueue = append(s.hidQueue, q) - } + s.hidQueues = make([]chan hidQueueMessage, hidrpc.OtherQueue+1) + s.hidQueues[hidrpc.HandshakeQueue] = make(chan hidQueueMessage, 2) // we don't really want to queue many handshake messages + s.hidQueues[hidrpc.KeyboardQueue] = make(chan hidQueueMessage, 256) + s.hidQueues[hidrpc.MouseQueue] = make(chan hidQueueMessage, 256) + s.hidQueues[hidrpc.MacroQueue] = make(chan hidQueueMessage, 10) // macros can be long, but we don't want to queue too many + s.hidQueues[hidrpc.OtherQueue] = make(chan hidQueueMessage, 256) } -func (s *Session) handleQueues(index int) { - for msg := range s.hidQueue[index] { +func (s *Session) handleQueue(queue chan hidQueueMessage) { + for msg := range queue { onHidMessage(msg, s) } } @@ -188,17 +190,18 @@ func getOnHidMessageHandler(session *Session, scopedLogger *zerolog.Logger, chan l.Trace().Msg("received data in HID RPC message handler") // Enqueue to ensure ordered processing - queueIndex := hidrpc.GetQueueIndex(hidrpc.MessageType(msg.Data[0])) - if queueIndex >= len(session.hidQueue) || queueIndex < 0 { + queueIndex, timelimit := hidrpc.GetQueueIndex(hidrpc.MessageType(msg.Data[0])) + if queueIndex >= len(session.hidQueues) || queueIndex < 0 { l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue index not found") - queueIndex = 3 + queueIndex = hidrpc.OtherQueue } - queue := session.hidQueue[queueIndex] + queue := session.hidQueues[queueIndex] if queue != nil { queue <- hidQueueMessage{ DataChannelMessage: msg, channel: channel, + timelimit: timelimit, } } else { l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue is nil") @@ -248,7 +251,7 @@ func newSession(config SessionConfig) (*Session, error) { session := &Session{peerConnection: peerConnection} session.rpcQueue = make(chan webrtc.DataChannelMessage, 256) - session.initQueues() + session.initHidQueues() session.initKeysDownStateQueue() go func() { @@ -258,8 +261,8 @@ func newSession(config SessionConfig) (*Session, error) { } }() - for i := 0; i < len(session.hidQueue); i++ { - go session.handleQueues(i) + for queue := range session.hidQueues { + go session.handleQueue(session.hidQueues[queue]) } peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { @@ -284,7 +287,11 @@ func newSession(config SessionConfig) (*Session, error) { session.RPCChannel = d d.OnMessage(func(msg webrtc.DataChannelMessage) { // Enqueue to ensure ordered processing - session.rpcQueue <- msg + if session.rpcQueue != nil { + session.rpcQueue <- msg + } else { + scopedLogger.Warn().Msg("RPC message received but rpcQueue is nil") + } }) triggerOTAStateUpdate() triggerVideoStateUpdate() @@ -352,22 +359,23 @@ func newSession(config SessionConfig) (*Session, error) { _ = peerConnection.Close() } if connectionState == webrtc.ICEConnectionStateClosed { - scopedLogger.Debug().Msg("ICE Connection State is closed, unmounting virtual media") + scopedLogger.Debug().Msg("ICE Connection State is closed, tearing down session") if session == currentSession { // Cancel any ongoing keyboard report multi when session closes - cancelKeyboardMacro() + cancelAllRunningKeyboardMacros() currentSession = nil } + // Stop RPC processor if session.rpcQueue != nil { close(session.rpcQueue) session.rpcQueue = nil } - // Stop HID RPC processor - for i := 0; i < len(session.hidQueue); i++ { - close(session.hidQueue[i]) - session.hidQueue[i] = nil + // Stop HID RPC processors + for i := 0; i < len(session.hidQueues); i++ { + close(session.hidQueues[i]) + session.hidQueues[i] = nil } close(session.keysDownStateQueue) From f9dcee1377cafcf378522b4629778b3aad5bdd85 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Sat, 1 Nov 2025 02:42:04 +0000 Subject: [PATCH 2/9] Clean up the keyboard HID report Was declaring too much output breaking boot keyboard on some devices. Got rid of the lockec-Shift LED. Added a short snooze when we get errors reading the LED state. --- internal/usbgadget/hid_keyboard.go | 88 +++++------ ui/package-lock.json | 225 ++++++++++++++++------------- ui/src/components/InfoBar.tsx | 4 - ui/src/hooks/stores.ts | 3 +- ui/src/keyboardMappings.ts | 74 ++++++---- 5 files changed, 210 insertions(+), 184 deletions(-) diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go index 05895206..439548b5 100644 --- a/internal/usbgadget/hid_keyboard.go +++ b/internal/usbgadget/hid_keyboard.go @@ -28,61 +28,50 @@ var keyboardConfig = gadgetConfigItem{ // Source: https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt var keyboardReportDesc = []byte{ - 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ - 0x09, 0x06, /* USAGE (Keyboard) */ - 0xa1, 0x01, /* COLLECTION (Application) */ + /* boot mode descriptor */ + 0x05, 0x01, /* USAGE_PAGE-global (Generic Desktop) */ + 0x09, 0x06, /* USAGE-local (Keyboard) */ + 0xA1, 0x01, /* COLLECTION-main (Application) */ /* 8 modifier bits */ - 0x05, 0x07, /* USAGE_PAGE (Keyboard) */ - 0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */ - 0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */ - 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ - 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */ - 0x75, 0x01, /* REPORT_SIZE (1) */ - 0x95, 0x08, /* REPORT_COUNT (8) */ - 0x81, 0x02, /* INPUT (Data,Var,Abs) */ + 0x05, 0x07, /* USAGE_PAGE-global (Keyboard) */ + 0x19, 0xe0, /* USAGE_MINIMUM-local 0xE0 (Keyboard LeftControl) */ + 0x29, 0xe7, /* USAGE_MAXIMUM-local 0xE7 (Keyboard Right GUI) */ + 0x15, 0x00, /* LOGICAL_MINIMUM-global (0) Modifier bit off) */ + 0x25, 0x01, /* LOGICAL_MAXIMUM-global (1) Modifier bit on) */ + 0x75, 0x01, /* REPORT_SIZE-global (1) one bit per modifier */ + 0x95, 0x08, /* REPORT_COUNT-global (8) 8 total bits */ + 0x81, 0x02, /* INPUT-main (Data,Var,Abs) Modifier bits 0-7 */ /* 8 bits of padding */ - 0x95, 0x01, /* REPORT_COUNT (1) */ - 0x75, 0x08, /* REPORT_SIZE (8) */ - 0x81, 0x03, /* INPUT (Cnst,Var,Abs) */ + 0x95, 0x01, /* REPORT_COUNT-global (1) one field */ + 0x75, 0x08, /* REPORT_SIZE-global (8) */ + 0x81, 0x03, /* INPUT-main (Cnst,Var,Abs) reserved byte */ /* 6 key codes for the 104 key keyboard */ - 0x95, 0x06, /* REPORT_COUNT (6) */ - 0x75, 0x08, /* REPORT_SIZE (8) */ - 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ - 0x25, 0xE7, /* LOGICAL_MAXIMUM (104-key HID) */ - 0x05, 0x07, /* USAGE_PAGE (Keyboard) */ - 0x19, 0x00, /* USAGE_MINIMUM (Reserved) */ - 0x29, 0xE7, /* USAGE_MAXIMUM (Keyboard Right GUI) */ - 0x81, 0x00, /* INPUT (Data,Ary,Abs) */ + 0x95, 0x06, /* REPORT_COUNT-global (6) keycodes */ + 0x75, 0x08, /* REPORT_SIZE-global (8) bits each (a byte) */ + 0x15, 0x00, /* LOGICAL_MINIMUM-global (0) */ + 0x25, 0xDF, /* LOGICAL_MAXIMUM-global 0xDF (104-key HID codes) */ + 0x05, 0x07, /* USAGE_PAGE-global (Keyboard) */ + 0x19, 0x00, /* USAGE_MINIMUM-local (Reserved/0) no key */ + 0x29, 0xE7, /* USAGE_MAXIMUM-local (Keyboard Right GUI) */ + 0x81, 0x00, /* INPUT-main (Data,Ary,Abs) array of keycodes */ /* LED report 5 bits for Num Lock through Kana */ - 0x95, 0x05, /* REPORT_COUNT (5) */ - 0x75, 0x01, /* REPORT_SIZE (1) */ - 0x05, 0x08, /* USAGE_PAGE (LEDs) */ - 0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */ - 0x29, 0x05, /* USAGE_MAXIMUM (Kana) */ - 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ + 0x95, 0x05, /* REPORT_COUNT-global (5) 5 LED bits */ + 0x75, 0x01, /* REPORT_SIZE-global (1) each 1 bit */ + 0x05, 0x08, /* USAGE_PAGE-global (LEDs) */ + 0x19, 0x01, /* USAGE_MINIMUM-local (Num Lock) */ + 0x29, 0x05, /* USAGE_MAXIMUM-local (Kana) */ + 0x91, 0x02, /* OUTPUT-main (Data,Var,Abs) bits 0-4 */ - /* 1 bit of padding for the Power LED (ignored) */ - 0x95, 0x01, /* REPORT_COUNT (1) */ - 0x75, 0x03, /* REPORT_SIZE (3) */ - 0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */ + /* 3 bits of padding for the rest of the byte */ + 0x95, 0x01, /* REPORT_COUNT-global (1) one field */ + 0x75, 0x03, /* REPORT_SIZE-global (3) of three bits */ + 0x91, 0x03, /* OUTPUT-main (Cnst,Var,Abs) bit 7 pad */ - /* LED report 1 bit for Shift */ - 0x95, 0x01, /* REPORT_COUNT (1) */ - 0x75, 0x01, /* REPORT_SIZE (1) */ - 0x05, 0x08, /* USAGE_PAGE (LEDs) */ - 0x19, 0x07, /* USAGE_MINIMUM (Shift) */ - 0x29, 0x07, /* USAGE_MAXIMUM (Shift) */ - 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ - - /* 1 bit of padding for the rest of the byte */ - 0x95, 0x01, /* REPORT_COUNT (1) */ - 0x75, 0x03, /* REPORT_SIZE (3) */ - 0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */ - 0xc0, /* END_COLLECTION */ + 0xC0, /* END_COLLECTION */ } const ( @@ -96,9 +85,7 @@ const ( KeyboardLedMaskScrollLock = 1 << 2 KeyboardLedMaskCompose = 1 << 3 KeyboardLedMaskKana = 1 << 4 - // power on/off LED is 5 - KeyboardLedMaskShift = 1 << 6 - ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana | KeyboardLedMaskShift + ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana ) // Synchronization between LED states and CAPS LOCK, NUM LOCK, SCROLL LOCK, @@ -111,7 +98,6 @@ 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 raw byte } @@ -128,7 +114,6 @@ func getKeyboardState(b byte) KeyboardState { ScrollLock: b&KeyboardLedMaskScrollLock != 0, Compose: b&KeyboardLedMaskCompose != 0, Kana: b&KeyboardLedMaskKana != 0, - Shift: b&KeyboardLedMaskShift != 0, raw: b, } } @@ -295,12 +280,13 @@ func (u *UsbGadget) listenKeyboardEvents() { time.Sleep(time.Second) continue } - // reset the counter + // reset the suppression counter u.resetLogSuppressionCounter("keyboardHidFileNil") n, err := u.keyboardHidFile.Read(buf) if err != nil { u.logWithSuppression("keyboardHidFileRead", 100, &l, err, "failed to read") + time.Sleep(100 * time.Millisecond) // Small backoff on read errors to avoid tight looping continue } u.resetLogSuppressionCounter("keyboardHidFileRead") diff --git a/ui/package-lock.json b/ui/package-lock.json index 25b17a53..821bc63e 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -848,21 +848,21 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", - "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0" + "@eslint/core": "^0.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -910,7 +910,6 @@ "version": "9.39.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz", "integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==", - "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -929,12 +928,12 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", - "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -1164,6 +1163,20 @@ "node": ">=18.0.0" } }, + "node_modules/@inlang/sdk/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1233,6 +1246,20 @@ "node": ">=18" } }, + "node_modules/@lix-js/sdk/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@lix-js/server-protocol-schema": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@lix-js/server-protocol-schema/-/server-protocol-schema-0.1.1.tgz", @@ -1743,9 +1770,9 @@ "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.21.tgz", - "integrity": "sha512-umBaSb65O1v6Lt8RV3o5srw0nKr25amf/yRIGFPug63sAerL9n2UkmfGywA1l1aN81W7faXIynF0JmlQ2wPSdw==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.14.0.tgz", + "integrity": "sha512-oExhY90bes5pDTVrei0xlMVosTxwd/NMafIpqsC4dMbRYZ5KB981l/CX8tMnGsagTplj/RcG9BeRYmV6/J5m3w==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -1761,16 +1788,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.13.21", - "@swc/core-darwin-x64": "1.13.21", - "@swc/core-linux-arm-gnueabihf": "1.13.21", - "@swc/core-linux-arm64-gnu": "1.13.21", - "@swc/core-linux-arm64-musl": "1.13.21", - "@swc/core-linux-x64-gnu": "1.13.21", - "@swc/core-linux-x64-musl": "1.13.21", - "@swc/core-win32-arm64-msvc": "1.13.21", - "@swc/core-win32-ia32-msvc": "1.13.21", - "@swc/core-win32-x64-msvc": "1.13.21" + "@swc/core-darwin-arm64": "1.14.0", + "@swc/core-darwin-x64": "1.14.0", + "@swc/core-linux-arm-gnueabihf": "1.14.0", + "@swc/core-linux-arm64-gnu": "1.14.0", + "@swc/core-linux-arm64-musl": "1.14.0", + "@swc/core-linux-x64-gnu": "1.14.0", + "@swc/core-linux-x64-musl": "1.14.0", + "@swc/core-win32-arm64-msvc": "1.14.0", + "@swc/core-win32-ia32-msvc": "1.14.0", + "@swc/core-win32-x64-msvc": "1.14.0" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -1782,9 +1809,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.21.tgz", - "integrity": "sha512-0jaz9r7f0PDK8OyyVooadv8dkFlQmVmBK6DtAnWSRjkCbNt4sdqsc9ZkyEDJXaxOVcMQ3pJx/Igniyw5xqACLw==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.14.0.tgz", + "integrity": "sha512-uHPC8rlCt04nvYNczWzKVdgnRhxCa3ndKTBBbBpResOZsRmiwRAvByIGh599j+Oo6Z5eyTPrgY+XfJzVmXnN7Q==", "cpu": [ "arm64" ], @@ -1799,9 +1826,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.21.tgz", - "integrity": "sha512-pLeZn+NTGa7oW/ysD6oM82BjKZl71WNJR9BKXRsOhrNQeUWv55DCoZT2P4DzeU5Xgjmos+iMoDLg/9R6Ngc0PA==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.14.0.tgz", + "integrity": "sha512-2SHrlpl68vtePRknv9shvM9YKKg7B9T13tcTg9aFCwR318QTYo+FzsKGmQSv9ox/Ua0Q2/5y2BNjieffJoo4nA==", "cpu": [ "x64" ], @@ -1816,9 +1843,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.21.tgz", - "integrity": "sha512-p9aYzTmP7qVDPkXxnbekOfbT11kxnPiuLrUbgpN/vn6sxXDCObMAiY63WlDR0IauBK571WUdmgb04goe/xTQWw==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.14.0.tgz", + "integrity": "sha512-SMH8zn01dxt809svetnxpeg/jWdpi6dqHKO3Eb11u4OzU2PK7I5uKS6gf2hx5LlTbcJMFKULZiVwjlQLe8eqtg==", "cpu": [ "arm" ], @@ -1833,9 +1860,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.21.tgz", - "integrity": "sha512-yRqFoGlCwEX1nS7OajBE23d0LPeONmFAgoe4rgRYvaUb60qGxIJoMMdvF2g3dum9ZyVDYAb3kP09hbXFbMGr4A==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.14.0.tgz", + "integrity": "sha512-q2JRu2D8LVqGeHkmpVCljVNltG0tB4o4eYg+dElFwCS8l2Mnt9qurMCxIeo9mgoqz0ax+k7jWtIRHktnVCbjvQ==", "cpu": [ "arm64" ], @@ -1850,9 +1877,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.21.tgz", - "integrity": "sha512-wu5EGA86gtdYMW69eU80jROzArzD3/6G6zzK0VVR+OFt/0zqbajiiszIpaniOVACObLfJEcShQ05B3q0+CpUEg==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.14.0.tgz", + "integrity": "sha512-uofpVoPCEUjYIv454ZEZ3sLgMD17nIwlz2z7bsn7rl301Kt/01umFA7MscUovFfAK2IRGck6XB+uulMu6aFhKQ==", "cpu": [ "arm64" ], @@ -1867,9 +1894,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.21.tgz", - "integrity": "sha512-AoGGVPNXH3C4S7WlJOxN1nGW5nj//J9uKysS7CIBotRmHXfHO4wPK3TVFRTA4cuouAWBBn7O8m3A99p/GR+iaw==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.14.0.tgz", + "integrity": "sha512-quTTx1Olm05fBfv66DEBuOsOgqdypnZ/1Bh3yGXWY7ANLFeeRpCDZpljD9BSjdsNdPOlwJmEUZXMHtGm3v1TZQ==", "cpu": [ "x64" ], @@ -1884,9 +1911,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.21.tgz", - "integrity": "sha512-cBy2amuDuxMZnEq16MqGu+DUlEFqI+7F/OACNlk7zEJKq48jJKGEMqJz3X2ucJE5jqUIg6Pos6Uo/y+vuWQymQ==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.14.0.tgz", + "integrity": "sha512-caaNAu+aIqT8seLtCf08i8C3/UC5ttQujUjejhMcuS1/LoCKtNiUs4VekJd2UGt+pyuuSrQ6dKl8CbCfWvWeXw==", "cpu": [ "x64" ], @@ -1901,9 +1928,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.21.tgz", - "integrity": "sha512-2xfR5gnqBGOMOlY3s1QiFTXZaivTILMwX67FD2uzT6OCbT/3lyAM/4+3BptBXD8pUkkOGMFLsdeHw4fbO1GrpQ==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.14.0.tgz", + "integrity": "sha512-EeW3jFlT3YNckJ6V/JnTfGcX7UHGyh6/AiCPopZ1HNaGiXVCKHPpVQZicmtyr/UpqxCXLrTgjHOvyMke7YN26A==", "cpu": [ "arm64" ], @@ -1918,9 +1945,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.21.tgz", - "integrity": "sha512-0pkpgKlBDwUImWTQxLakKbzZI6TIGVVAxk658oxrY8VK+hxRy2iezFY6m5Urmeds47M/cnW3dO+OY4C2caOF8A==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.14.0.tgz", + "integrity": "sha512-dPai3KUIcihV5hfoO4QNQF5HAaw8+2bT7dvi8E5zLtecW2SfL3mUZipzampXq5FHll0RSCLzlrXnSx+dBRZIIQ==", "cpu": [ "ia32" ], @@ -1935,9 +1962,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.21.tgz", - "integrity": "sha512-DAnIw2J95TOW4Kr7NBx12vlZPW3QndbpFMmuC7x+fPoozoLpEscaDkiYhk7/sTtY9pubPMfHFPBORlbqyQCfOQ==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.14.0.tgz", + "integrity": "sha512-nm+JajGrTqUA6sEHdghDlHMNfH1WKSiuvljhdmBACW4ta4LC3gKurX2qZuiBARvPkephW9V/i5S8QPY1PzFEqg==", "cpu": [ "x64" ], @@ -2489,9 +2516,9 @@ "license": "MIT" }, "node_modules/@types/validator": { - "version": "13.15.3", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", - "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", + "version": "13.15.4", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.4.tgz", + "integrity": "sha512-LSFfpSnJJY9wbC0LQxgvfb+ynbHftFo0tMsFOl/J4wexLnYMmDSPaj2ZyDv3TkfL1UePxPrxOWJfbiRS8mQv7A==", "dev": true, "license": "MIT" }, @@ -3137,9 +3164,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz", - "integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==", + "version": "2.8.22", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.22.tgz", + "integrity": "sha512-/tk9kky/d8T8CTXIQYASLyhAxR5VwL3zct1oAoVTaOUHwrmsGnfbRwNdEq+vOl2BN8i3PcDdP0o4Q+jjKQoFbQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3260,9 +3287,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001751", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", - "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "version": "1.0.30001752", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001752.tgz", + "integrity": "sha512-vKUk7beoukxE47P5gcVNKkDRzXdVofotshHwfR9vmpeFKxmI5PBpgOMC18LUJUA/DvJ70Y7RveasIBraqsyO/g==", "dev": true, "funding": [ { @@ -3613,9 +3640,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.18", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", - "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", "license": "MIT" }, "node_modules/debug": { @@ -3733,9 +3760,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.240", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.240.tgz", - "integrity": "sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==", + "version": "1.5.244", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", + "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==", "dev": true, "license": "ISC" }, @@ -4010,19 +4037,19 @@ } }, "node_modules/eslint": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", - "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", + "version": "9.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.0.tgz", + "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.1", - "@eslint/core": "^0.16.0", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.38.0", - "@eslint/plugin-kit": "^0.4.0", + "@eslint/js": "9.39.0", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -4597,12 +4624,12 @@ "license": "ISC" }, "node_modules/focus-trap": { - "version": "7.6.5", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.5.tgz", - "integrity": "sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==", + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.6.tgz", + "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==", "license": "MIT", "dependencies": { - "tabbable": "^6.2.0" + "tabbable": "^6.3.0" } }, "node_modules/focus-trap-react": { @@ -5009,9 +5036,9 @@ } }, "node_modules/immer": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", - "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", "funding": { "type": "opencollective", @@ -6023,9 +6050,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.26", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", - "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -6534,9 +6561,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.65.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz", - "integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==", + "version": "7.66.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", + "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -6628,9 +6655,9 @@ } }, "node_modules/react-simple-keyboard": { - "version": "3.8.131", - "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.131.tgz", - "integrity": "sha512-gICYtaV38AU/E1PTTwzJOF6s5fu6Nu3GZQwnaSNB4VGOO3UwOn8rioDEFBLvjMWpP8kwfWp2of8xywY647rTxA==", + "version": "3.8.132", + "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.132.tgz", + "integrity": "sha512-GoXK+6SRu72Jn8qT8fy+PxstIdZEACyIi/7zy0qXcrB6EJaN6zZk0/w3Sv3ALLwXqQd/3t3yUL4DQOwoNO1cbw==", "license": "MIT", "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", @@ -6919,9 +6946,9 @@ } }, "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, "node_modules/set-function-length": { @@ -7656,9 +7683,9 @@ } }, "node_modules/validator": { - "version": "13.15.15", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", - "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "version": "13.15.20", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", + "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", "license": "MIT", "engines": { "node": ">= 0.10" diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx index 2c4eb9e0..a40afbbf 100644 --- a/ui/src/components/InfoBar.tsx +++ b/ui/src/components/InfoBar.tsx @@ -166,10 +166,6 @@ export default function InfoBar() { {keyboardLedState.kana ? (
{m.info_kana()}
) : null} - - {keyboardLedState.shift ? ( -
{m.info_shift()}
- ) : null}
diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 06f29582..1b482658 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -473,7 +473,6 @@ export interface KeyboardLedState { scroll_lock: boolean; compose: boolean; kana: boolean; - shift: boolean; // Optional, as not all keyboards have a shift LED }; export const hidKeyBufferSize = 6; @@ -509,7 +508,7 @@ export interface HidState { } export const useHidStore = create(set => ({ - keyboardLedState: { num_lock: false, caps_lock: false, scroll_lock: false, compose: false, kana: false, shift: false } as KeyboardLedState, + keyboardLedState: { num_lock: false, caps_lock: false, scroll_lock: false, compose: false, kana: false } as KeyboardLedState, setKeyboardLedState: (ledState: KeyboardLedState): void => set({ keyboardLedState: ledState }), keysDownState: { modifier: 0, keys: [0, 0, 0, 0, 0, 0] } as KeysDownState, diff --git a/ui/src/keyboardMappings.ts b/ui/src/keyboardMappings.ts index 456a43c4..e35bb09d 100644 --- a/ui/src/keyboardMappings.ts +++ b/ui/src/keyboardMappings.ts @@ -3,12 +3,12 @@ // [Universal Serial Bus HID Usage Tables: Section 10](https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf) // These are all the key codes (not scan codes) that an 85/101/102 keyboard might have on it export const keys = { - Again: 0x79, + Again: 0x79, // aka Clear AlternateErase: 0x9d, AltGr: 0xe6, // aka AltRight AltLeft: 0xe2, AltRight: 0xe6, - Application: 0x65, + Application: 0x65, // aka ContextMenu ArrowDown: 0x51, ArrowLeft: 0x50, ArrowRight: 0x4f, @@ -25,11 +25,10 @@ export const keys = { ClearAgain: 0xa2, Comma: 0x36, Compose: 0xe3, - ContextMenu: 0x65, ControlLeft: 0xe0, ControlRight: 0xe4, Copy: 0x7c, - CrSel: 0xa3, + CrSel: 0xa3, // aka Props CurrencySubunit: 0xb5, CurrencyUnit: 0xb4, Cut: 0x7b, @@ -49,7 +48,7 @@ export const keys = { Enter: 0x28, Equal: 0x2e, Escape: 0x29, - Execute: 0x74, + Execute: 0x74, // aka Open ExSel: 0xa4, F1: 0x3a, F2: 0x3b, @@ -77,14 +76,14 @@ export const keys = { F24: 0x73, Find: 0x7e, Grave: 0x35, - HashTilde: 0x32, // non-US # and ~ + HashTilde: 0x32, // non-US # and ~ (typically near Enter key) Help: 0x75, Home: 0x4a, Insert: 0x49, International7: 0x8d, International8: 0x8e, International9: 0x8f, - IntlBackslash: 0x64, // non-US \ and | + IntlBackslash: 0x64, // non-US \ and | (typically near Left Shift key) KeyA: 0x04, KeyB: 0x05, KeyC: 0x06, @@ -111,17 +110,17 @@ export const keys = { KeyX: 0x1b, KeyY: 0x1c, KeyZ: 0x1d, - KeyRO: 0x87, - KatakanaHiragana: 0x88, - Yen: 0x89, - Henkan: 0x8a, - Muhenkan: 0x8b, - KPJPComma: 0x8c, - Hangeul: 0x90, - Hanja: 0x91, - Katakana: 0x92, - Hiragana: 0x93, - ZenkakuHankaku: 0x94, + RO: 0x87, // aka International1 + KatakanaHiragana: 0x88, // aka International2 + Yen: 0x89, // aka International3 + Henkan: 0x8a, // aka International4 + Muhenkan: 0x8b, // aka International5 + KPJPComma: 0x8c, // aka International6 + Hangeul: 0x90, // aka Lang1 + Hanja: 0x91, // aka Lang2 + Katakana: 0x92, // aka Lang3 + Hiragana: 0x93, // aka Lang4 + ZenkakuHankaku: 0x94, // aka Lang5 LockingCapsLock: 0x82, LockingNumLock: 0x83, LockingScrollLock: 0x84, @@ -129,9 +128,29 @@ export const keys = { Lang7: 0x96, Lang8: 0x97, Lang9: 0x98, + MediaBack: 0xF1, + MediaCalc: 0xFB, + MediaCoffee: 0xF9, + MediaEdit: 0xF7, + MediaEjectCD: 0xEC, + MediaFind: 0xF4, + MediaForward: 0xF2, + MediaMute: 0xEF, + MediaNextSong: 0xEB, + MediaPlayPause: 0xE8, + MediaPreviousSong: 0xEA, + MediaRefresh: 0xFA, + MediaScrollDown: 0xF6, + MediaScrollUp: 0xF5, + MediaSleep: 0xF8, + MediaStop: 0xF3, + MediaStopCD: 0xE9, + MediaVolumeDown: 0xEE, + MediaVolumeUp: 0xED, + MediaWWW: 0xF0, Menu: 0x76, - MetaLeft: 0xe3, - MetaRight: 0xe7, + MetaLeft: 0xe3, // aka LeftGUI + MetaRight: 0xe7, // aka RightGUI Minus: 0x2d, Mute: 0x7f, NumLock: 0x53, // and Clear @@ -157,9 +176,8 @@ export const keys = { NumpadClearEntry: 0xd9, NumpadColon: 0xcb, NumpadComma: 0x85, - NumpadDecimal: 0x63, // and Delete + NumpadDecimal: 0x63, // and NumpadDelete NumpadDecimalBase: 0xdc, - NumpadDelete: 0x63, NumpadDivide: 0x54, NumpadDownArrow: 0x5a, NumpadEnd: 0x59, @@ -211,14 +229,14 @@ export const keys = { PageUp: 0x4b, Paste: 0x7d, Pause: 0x48, - Period: 0x37, // aka Dot + Period: 0x37, // aka Dot Power: 0x66, - PrintScreen: 0x46, + PrintScreen: 0x46, // aka SysRq Prior: 0x9d, - Quote: 0x34, // aka Single Quote or Apostrophe + Quote: 0x34, // aka Single Quote or Apostrophe Return: 0x9e, - ScrollLock: 0x47, - Select: 0x77, + ScrollLock: 0x47, // aka ScrLk + Select: 0x77, // aka Front Semicolon: 0x33, Separator: 0x9f, ShiftLeft: 0xe1, @@ -240,7 +258,7 @@ export const deadKeys = { Breve: 0x02d8, Caron: 0x02c7, Cedilla: 0x00b8, - Circumflex: 0x005e, // or 0x02c6? + Circumflex: 0x02c6, Comma: 0x002c, Dot: 0x00b7, DoubleAcute: 0x02dd, From 57a7aa6a8bd7a4a7cdc4aeef90dd77a3e92af78c Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 5 Nov 2025 13:53:23 -0600 Subject: [PATCH 3/9] Protect suspension mutex Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/usbgadget/hid_keyboard.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go index 439548b5..8f7829ac 100644 --- a/internal/usbgadget/hid_keyboard.go +++ b/internal/usbgadget/hid_keyboard.go @@ -161,13 +161,18 @@ func (u *UsbGadget) SetOnKeysDownChange(f func(state KeysDownState)) { } var suspendedKeyDownMessages bool = false +var suspendedKeyDownMessagesLock sync.Mutex func (u *UsbGadget) SuspendKeyDownMessages() { + suspendedKeyDownMessagesLock.Lock() suspendedKeyDownMessages = true + suspendedKeyDownMessagesLock.Unlock() } func (u *UsbGadget) ResumeSuspendKeyDownMessages() { + suspendedKeyDownMessagesLock.Lock() suspendedKeyDownMessages = false + suspendedKeyDownMessagesLock.Unlock() } func (u *UsbGadget) SetOnKeepAliveReset(f func()) { From 05057cb6fa0f1492cb581948f76e1a6421294409 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 5 Nov 2025 13:53:55 -0600 Subject: [PATCH 4/9] Better loop name Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- webrtc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webrtc.go b/webrtc.go index 5519ee34..af280c56 100644 --- a/webrtc.go +++ b/webrtc.go @@ -261,8 +261,8 @@ func newSession(config SessionConfig) (*Session, error) { } }() - for queue := range session.hidQueues { - go session.handleQueue(session.hidQueues[queue]) + for queueIndex := range session.hidQueues { + go session.handleQueue(session.hidQueues[queueIndex]) } peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { From 49b9a35951787541d5f099ed1e482ceb05c476f3 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 5 Nov 2025 14:09:10 -0600 Subject: [PATCH 5/9] Be explicit about minimum and maximum delay Use range correctly in the UI element and error messaging. --- ui/src/components/popovers/PasteModal.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ui/src/components/popovers/PasteModal.tsx b/ui/src/components/popovers/PasteModal.tsx index ab8de215..d09341c8 100644 --- a/ui/src/components/popovers/PasteModal.tsx +++ b/ui/src/components/popovers/PasteModal.tsx @@ -19,6 +19,8 @@ import { TextAreaWithLabel } from "@components/TextArea"; // uint32 max value / 4 const pasteMaxLength = 1073741824; const defaultDelay = 20; +const minimumDelay = 10; +const maximumDelay = 65534; export default function PasteModal() { const TextAreaRef = useRef(null); @@ -31,7 +33,7 @@ export default function PasteModal() { const [invalidChars, setInvalidChars] = useState([]); const [delayValue, setDelayValue] = useState(defaultDelay); const delay = useMemo(() => { - if (delayValue < 0 || delayValue > 65534) { + if (delayValue < minimumDelay || delayValue > maximumDelay) { return defaultDelay; } return delayValue; @@ -189,18 +191,18 @@ export default function PasteModal() { type="number" label={m.paste_modal_delay_between_keys()} placeholder={m.paste_modal_delay_between_keys()} - min={50} - max={65534} + min={minimumDelay} + max={maximumDelay} value={delayValue} onChange={e => { setDelayValue(parseInt(e.target.value, 10)); }} /> - {(delayValue < defaultDelay || delayValue > 65534) && ( + {(delayValue < minimumDelay || delayValue > maximumDelay) && (
- {m.paste_modal_delay_out_of_range({ min: 50, max: 65534 })} + {m.paste_modal_delay_out_of_range({ min: minimumDelay, max: maximumDelay })}
)} From da8c82da34998575553941783da94aff5ec6d5de Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 5 Nov 2025 14:54:06 -0600 Subject: [PATCH 6/9] Remove unused translation for Shift. --- ui/localization/messages/da.json | 1 - ui/localization/messages/de.json | 1 - ui/localization/messages/en.json | 1 - ui/localization/messages/es.json | 1 - ui/localization/messages/fr.json | 1 - ui/localization/messages/it.json | 1 - ui/localization/messages/nb.json | 1 - ui/localization/messages/sv.json | 1 - ui/localization/messages/zh.json | 1 - ui/tools/find_duplicate_translations.py | 2 +- ui/tools/find_unused_messages.py | 2 +- 11 files changed, 2 insertions(+), 11 deletions(-) diff --git a/ui/localization/messages/da.json b/ui/localization/messages/da.json index dae6e906..be0c3c0d 100644 --- a/ui/localization/messages/da.json +++ b/ui/localization/messages/da.json @@ -331,7 +331,6 @@ "info_relayed_by_cloudflare": "Videresendt af Cloudflare", "info_resolution": "Opløsning:", "info_scroll_lock": "Scroll Lock", - "info_shift": "Flytte", "info_usb_state": "USB-tilstand:", "info_video_size": "Videostørrelse:", "input_disabled": "Input deaktiveret", diff --git a/ui/localization/messages/de.json b/ui/localization/messages/de.json index 04e25844..bc767b47 100644 --- a/ui/localization/messages/de.json +++ b/ui/localization/messages/de.json @@ -331,7 +331,6 @@ "info_relayed_by_cloudflare": "Weitergeleitet von Cloudflare", "info_resolution": "Auflösung:", "info_scroll_lock": "Rollen-Taste", - "info_shift": "Schicht", "info_usb_state": "USB-Status:", "info_video_size": "Videogröße:", "input_disabled": "Eingabe deaktiviert", diff --git a/ui/localization/messages/en.json b/ui/localization/messages/en.json index 0356e8e5..6a0c1ed9 100644 --- a/ui/localization/messages/en.json +++ b/ui/localization/messages/en.json @@ -331,7 +331,6 @@ "info_relayed_by_cloudflare": "Relayed by Cloudflare", "info_resolution": "Resolution:", "info_scroll_lock": "Scroll Lock", - "info_shift": "Shift", "info_usb_state": "USB State:", "info_video_size": "Video Size:", "input_disabled": "Input disabled", diff --git a/ui/localization/messages/es.json b/ui/localization/messages/es.json index a74e35ec..f9ccb3fe 100644 --- a/ui/localization/messages/es.json +++ b/ui/localization/messages/es.json @@ -331,7 +331,6 @@ "info_relayed_by_cloudflare": "Retransmitido por Cloudflare", "info_resolution": "Resolución:", "info_scroll_lock": "Bloq Despl", - "info_shift": "Cambio", "info_usb_state": "Estado USB:", "info_video_size": "Tamaño del vídeo:", "input_disabled": "Entrada deshabilitada", diff --git a/ui/localization/messages/fr.json b/ui/localization/messages/fr.json index eb7361d1..54b156c6 100644 --- a/ui/localization/messages/fr.json +++ b/ui/localization/messages/fr.json @@ -331,7 +331,6 @@ "info_relayed_by_cloudflare": "Relayé par Cloudflare", "info_resolution": "Résolution :", "info_scroll_lock": "Verrouillage du défilement", - "info_shift": "Maj", "info_usb_state": "État USB :", "info_video_size": "Taille de la vidéo :", "input_disabled": "Entrée désactivée", diff --git a/ui/localization/messages/it.json b/ui/localization/messages/it.json index b2aa5529..a1cc4156 100644 --- a/ui/localization/messages/it.json +++ b/ui/localization/messages/it.json @@ -331,7 +331,6 @@ "info_relayed_by_cloudflare": "Rilasciato da Cloudflare", "info_resolution": "Risoluzione:", "info_scroll_lock": "Blocco scorrimento", - "info_shift": "Spostare", "info_usb_state": "Stato USB:", "info_video_size": "Dimensioni video:", "input_disabled": "Input disabilitato", diff --git a/ui/localization/messages/nb.json b/ui/localization/messages/nb.json index 26b8584e..d4bc4cd2 100644 --- a/ui/localization/messages/nb.json +++ b/ui/localization/messages/nb.json @@ -331,7 +331,6 @@ "info_relayed_by_cloudflare": "Videresendt av Cloudflare", "info_resolution": "Oppløsning:", "info_scroll_lock": "Scroll Lock", - "info_shift": "Skifte", "info_usb_state": "USB-tilstand:", "info_video_size": "Videostørrelse:", "input_disabled": "Inndata deaktivert", diff --git a/ui/localization/messages/sv.json b/ui/localization/messages/sv.json index a4df3271..2d2b63e4 100644 --- a/ui/localization/messages/sv.json +++ b/ui/localization/messages/sv.json @@ -331,7 +331,6 @@ "info_relayed_by_cloudflare": "Vidarebefordras av Cloudflare", "info_resolution": "Upplösning:", "info_scroll_lock": "Scroll Lock", - "info_shift": "Flytta", "info_usb_state": "USB-status:", "info_video_size": "Videostorlek:", "input_disabled": "Inmatning inaktiverad", diff --git a/ui/localization/messages/zh.json b/ui/localization/messages/zh.json index 14a55883..caf066f6 100644 --- a/ui/localization/messages/zh.json +++ b/ui/localization/messages/zh.json @@ -331,7 +331,6 @@ "info_relayed_by_cloudflare": "由 Cloudflare 转发", "info_resolution": "分辨率:", "info_scroll_lock": "滚动锁定", - "info_shift": "Shift", "info_usb_state": "USB 状态:", "info_video_size": "视频大小:", "input_disabled": "输入禁用", diff --git a/ui/tools/find_duplicate_translations.py b/ui/tools/find_duplicate_translations.py index 0150d305..a76c227c 100644 --- a/ui/tools/find_duplicate_translations.py +++ b/ui/tools/find_duplicate_translations.py @@ -89,7 +89,7 @@ def main(argv): ) report = { - "generated_at": datetime.utcnow().isoformat() + "Z", + "generated_at": datetime.now().isoformat(), "en_json": str(en_path), "total_string_keys": total_keys, "duplicate_groups": sorted( diff --git a/ui/tools/find_unused_messages.py b/ui/tools/find_unused_messages.py index f83683bf..627b5d74 100644 --- a/ui/tools/find_unused_messages.py +++ b/ui/tools/find_unused_messages.py @@ -82,7 +82,7 @@ def main(): print(f"Generating report for {len(usages)} usages ...") report = { - "generated_at": datetime.utcnow().isoformat() + "Z", + "generated_at": datetime.now().isoformat(), "en_json": str(en_path), "src_root": args.src, "total_keys": len(keys), From 8de61db3d837ddd5de2ff8045d880db19cb0d693 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 5 Nov 2025 14:54:40 -0600 Subject: [PATCH 7/9] Return a duration with the queue (not a bare int) --- internal/hidrpc/hidrpc.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/hidrpc/hidrpc.go b/internal/hidrpc/hidrpc.go index e861fe32..88f584af 100644 --- a/internal/hidrpc/hidrpc.go +++ b/internal/hidrpc/hidrpc.go @@ -42,16 +42,17 @@ const ( func GetQueueIndex(messageType MessageType) (int, time.Duration) { switch messageType { case TypeHandshake: - return HandshakeQueue, 1 + return HandshakeQueue, 1 * time.Second case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardLedState, TypeKeydownState, TypeKeyboardMacroState: - return KeyboardQueue, 1 + return KeyboardQueue, 1 * time.Second case TypePointerReport, TypeMouseReport, TypeWheelReport: - return MouseQueue, 1 + return MouseQueue, 1 * time.Second // we don't want to block the queue for these messages case TypeKeyboardMacroReport, TypeCancelKeyboardMacroReport, TypeKeyboardMacroTokenState: - return MacroQueue, 60 // 1 minute timeout + return MacroQueue, 60 * time.Second default: - return OtherQueue, 5 + return OtherQueue, 5 * time.Second + } } From e12ddaff799d718afc393bd175f9c31e55f2b99d Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 5 Nov 2025 14:56:04 -0600 Subject: [PATCH 8/9] Use a single exported HidKeyBufferSize from hid_keyboard --- internal/hidrpc/message.go | 9 ++++----- internal/usbgadget/hid_keyboard.go | 16 ++++++++-------- internal/usbgadget/usbgadget.go | 2 +- jsonrpc.go | 2 +- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/internal/hidrpc/message.go b/internal/hidrpc/message.go index 381801f4..a42ca4e3 100644 --- a/internal/hidrpc/message.go +++ b/internal/hidrpc/message.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/google/uuid" + "github.com/jetkvm/kvm/internal/usbgadget" ) // Message .. @@ -135,18 +136,16 @@ func (m *Message) KeyboardReport() (KeyboardReport, error) { // Macro .. type KeyboardMacroStep struct { Modifier byte // 1 byte - Keys []byte // 6 bytes: HidKeyBufferSize + Keys []byte // 6 bytes: usbgadget.HidKeyBufferSize Delay uint16 // 2 bytes } + type KeyboardMacroReport struct { IsPaste bool StepCount uint32 Steps []KeyboardMacroStep } -// HidKeyBufferSize is the size of the keys buffer in the keyboard report. -const HidKeyBufferSize int = 6 - // KeyboardMacroReport returns the keyboard macro report from the message. func (m *Message) KeyboardMacroReport() (KeyboardMacroReport, error) { if m.t != TypeKeyboardMacroReport { @@ -171,7 +170,7 @@ func (m *Message) KeyboardMacroReport() (KeyboardMacroReport, error) { Delay: binary.BigEndian.Uint16(m.d[offset+7 : offset+9]), }) - offset += 1 + HidKeyBufferSize + 2 + offset += 1 + usbgadget.HidKeyBufferSize + 2 } return KeyboardMacroReport{ diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go index 8f7829ac..7a12f5ac 100644 --- a/internal/usbgadget/hid_keyboard.go +++ b/internal/usbgadget/hid_keyboard.go @@ -76,7 +76,7 @@ var keyboardReportDesc = []byte{ const ( hidReadBufferSize = 8 - hidKeyBufferSize = 6 + 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 @@ -342,7 +342,7 @@ func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error { return err } - _, err := u.writeWithTimeout(u.keyboardHidFile, append([]byte{modifier, 0x00}, keys[:hidKeyBufferSize]...)) + _, err := u.writeWithTimeout(u.keyboardHidFile, append([]byte{modifier, 0x00}, keys[:HidKeyBufferSize]...)) if err != nil { u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0") u.keyboardHidFile.Close() @@ -386,11 +386,11 @@ func (u *UsbGadget) UpdateKeysDown(modifier byte, keys []byte) KeysDownState { func (u *UsbGadget) KeyboardReport(modifier byte, keys []byte) error { defer u.resetUserInputTime() - if len(keys) > hidKeyBufferSize { - keys = keys[:hidKeyBufferSize] + if len(keys) > HidKeyBufferSize { + keys = keys[:HidKeyBufferSize] } - if len(keys) < hidKeyBufferSize { - keys = append(keys, make([]byte, hidKeyBufferSize-len(keys))...) + if len(keys) < HidKeyBufferSize { + keys = append(keys, make([]byte, HidKeyBufferSize-len(keys))...) } err := u.keyboardWriteHidFile(modifier, keys) @@ -473,7 +473,7 @@ func (u *UsbGadget) keypressReport(key byte, press bool) (KeysDownState, error) // 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 { + 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) @@ -484,7 +484,7 @@ func (u *UsbGadget) keypressReport(key byte, press bool) (KeysDownState, error) // 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 + keys[HidKeyBufferSize-1] = 0 // Clear the last byte } } overrun = false // We found a slot for the key diff --git a/internal/usbgadget/usbgadget.go b/internal/usbgadget/usbgadget.go index f01ae09d..7def29f1 100644 --- a/internal/usbgadget/usbgadget.go +++ b/internal/usbgadget/usbgadget.go @@ -135,7 +135,7 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev keyboardStateCtx: keyboardCtx, keyboardStateCancel: keyboardCancel, keyboardState: 0, - keysDownState: KeysDownState{Modifier: 0, Keys: []byte{0, 0, 0, 0, 0, 0}}, // must be initialized to hidKeyBufferSize (6) zero bytes + keysDownState: KeysDownState{Modifier: 0, Keys: []byte{0, 0, 0, 0, 0, 0}}, // must be initialized to usbgadget.HidKeyBufferSize (6) zero bytes kbdAutoReleaseTimers: make(map[byte]*time.Timer), enabledDevices: *enabledDevices, lastUserInput: time.Now(), diff --git a/jsonrpc.go b/jsonrpc.go index b39c5206..ebb6d672 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -1210,7 +1210,7 @@ func executeKeyboardMacro(ctx context.Context, isPaste bool, macro []hidrpc.Keyb case <-ctx.Done(): // make sure keyboard state is reset and the client gets notified gadget.ResumeSuspendKeyDownMessages() - err := rpcKeyboardReport(0, make([]byte, hidrpc.HidKeyBufferSize)) + err := rpcKeyboardReport(0, make([]byte, usbgadget.HidKeyBufferSize)) if err != nil { logger.Warn().Err(err).Msg("failed to reset keyboard state") } From 8e00b3d581c24ba29d126f8716dd591ea0741d10 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 5 Nov 2025 14:56:27 -0600 Subject: [PATCH 9/9] Fix CoPilot complaints --- ui/src/hooks/hidRpc.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/src/hooks/hidRpc.ts b/ui/src/hooks/hidRpc.ts index dbdbfde1..1b3a3829 100644 --- a/ui/src/hooks/hidRpc.ts +++ b/ui/src/hooks/hidRpc.ts @@ -386,7 +386,7 @@ export class CancelKeyboardMacroReportMessage extends RpcMessage { constructor(token: string) { super(HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroReport); - this.token = (token == null || token === undefined || token === "") + this.token = (token == null || token === "") ? "00000000-0000-0000-0000-000000000000" : token; } @@ -397,11 +397,11 @@ export class CancelKeyboardMacroReportMessage extends RpcMessage { } public static unmarshal(data: Uint8Array): CancelKeyboardMacroReportMessage | undefined { - if (data.length == 0) { + if (data.length === 0) { return new CancelKeyboardMacroReportMessage("00000000-0000-0000-0000-000000000000"); } - if (data.length != 16) { + if (data.length !== 16) { throw new Error(`Invalid cancel message length: ${data.length}`); }