From deb258b7178af0c541f30d5be8b6602a936b4ced Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 24 Sep 2025 19:20:51 -0500 Subject: [PATCH 1/2] 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/2] 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,