From 0dd709882181cebf5c6feb40dd3220bc54173dcc Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 13 Aug 2025 18:10:31 -0500 Subject: [PATCH] Use the KeysDownState for the infobar Strong typed in the typescript realm. --- internal/usbgadget/hid_keyboard.go | 24 ++-- internal/usbgadget/usbgadget.go | 8 +- ui/src/components/InfoBar.tsx | 57 ++++++---- ui/src/hooks/stores.ts | 170 ++++++++++++++--------------- ui/src/hooks/useJsonRpc.ts | 4 +- ui/src/hooks/useKeyboard.ts | 31 ++---- ui/src/routes/devices.$id.tsx | 64 ++++++----- usb.go | 4 +- 8 files changed, 177 insertions(+), 185 deletions(-) diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go index 7be48d4..6793da8 100644 --- a/internal/usbgadget/hid_keyboard.go +++ b/internal/usbgadget/hid_keyboard.go @@ -100,7 +100,7 @@ func getKeyboardState(b byte) KeyboardState { } } -func (u *UsbGadget) updateKeyboardState(state byte) { +func (u *UsbGadget) updateKeyboardState(state uint8) { u.keyboardStateLock.Lock() defer u.keyboardStateLock.Unlock() @@ -185,7 +185,7 @@ func (u *UsbGadget) listenKeyboardEvents() { l.Trace().Msg("starting") go func() { - buf := make([]byte, hidReadBufferSize) + buf := make([]uint8, hidReadBufferSize) for { select { case <-u.keyboardStateCtx.Done(): @@ -209,7 +209,7 @@ func (u *UsbGadget) listenKeyboardEvents() { } u.resetLogSuppressionCounter("keyboardHidFileRead") - l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard") + l.Trace().Int("n", n).Uints8("buf", buf).Msg("got data from keyboard") if n != 1 { l.Trace().Int("n", n).Msg("expected 1 byte, got") continue @@ -245,12 +245,12 @@ func (u *UsbGadget) OpenKeyboardHidFile() error { return u.openKeyboardHidFile() } -func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error { +func (u *UsbGadget) keyboardWriteHidFile(modifier uint8, keys []uint8) error { if err := u.openKeyboardHidFile(); err != nil { return err } - _, err := u.keyboardHidFile.Write(append([]byte{modifier, 0x00}, keys[:]...)) + _, err := u.keyboardHidFile.Write(append([]uint8{modifier, 0x00}, keys[:hidKeyBufferSize]...)) if err != nil { u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0") u.keyboardHidFile.Close() @@ -261,7 +261,7 @@ func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error { return nil } -func (u *UsbGadget) KeyboardReport(modifier byte, keys []byte) error { +func (u *UsbGadget) KeyboardReport(modifier uint8, keys []uint8) error { u.keyboardLock.Lock() defer u.keyboardLock.Unlock() defer u.resetUserInputTime() @@ -270,7 +270,7 @@ func (u *UsbGadget) KeyboardReport(modifier byte, keys []byte) error { keys = keys[:hidKeyBufferSize] } if len(keys) < hidKeyBufferSize { - keys = append(keys, make([]byte, hidKeyBufferSize-len(keys))...) + keys = append(keys, make([]uint8, hidKeyBufferSize-len(keys))...) } return u.keyboardWriteHidFile(modifier, keys) @@ -289,12 +289,6 @@ const ( RightSuper = 0xE7 // Right GUI (e.g. Windows key, Apple Command key) ) -// KeyCodeMask maps a key code to its corresponding bit mask -type KeyCodeMask struct { - KeyCode byte - Mask byte -} - // KeyCodeToMaskMap is a slice of KeyCodeMask for quick lookup var KeyCodeToMaskMap = map[uint8]uint8{ LeftControl: ModifierMaskLeftControl, @@ -307,7 +301,7 @@ var KeyCodeToMaskMap = map[uint8]uint8{ RightSuper: ModifierMaskRightSuper, } -func (u *UsbGadget) KeypressReport(key byte, press bool) (KeysDownState, error) { +func (u *UsbGadget) KeypressReport(key uint8, press bool) (KeysDownState, error) { u.keyboardLock.Lock() defer u.keyboardLock.Unlock() defer u.resetUserInputTime() @@ -364,7 +358,7 @@ func (u *UsbGadget) KeypressReport(key byte, press bool) (KeysDownState, error) } if err := u.keyboardWriteHidFile(modifier, keys); err != nil { - u.log.Warn().Uint8("modifier", modifier).Bytes("keys", keys).Msg("Could not write keypress report to hidg0") + u.log.Warn().Uint8("modifier", modifier).Uints8("keys", keys).Msg("Could not write keypress report to hidg0") } state.Modifier = modifier diff --git a/internal/usbgadget/usbgadget.go b/internal/usbgadget/usbgadget.go index 0e0604f..343ff54 100644 --- a/internal/usbgadget/usbgadget.go +++ b/internal/usbgadget/usbgadget.go @@ -42,8 +42,8 @@ var defaultUsbGadgetDevices = Devices{ } type KeysDownState struct { - Modifier byte `json:"modifier"` - Keys []byte `json:"keys"` + Modifier uint8 `json:"modifier"` + Keys []uint8 `json:"keys"` } // UsbGadget is a struct that represents a USB gadget. @@ -65,7 +65,7 @@ type UsbGadget struct { relMouseHidFile *os.File relMouseLock sync.Mutex - keyboardState byte // keyboard latched state (NumLock, CapsLock, ScrollLock, Compose, Kana) + keyboardState uint8 // keyboard latched state (NumLock, CapsLock, ScrollLock, Compose, Kana) keysDownState KeysDownState // keyboard dynamic state (modifier keys and pressed keys) keyboardStateLock sync.Mutex @@ -131,7 +131,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: []uint8{0, 0, 0, 0, 0, 0}}, // must be initialized to hidKeyBufferSize (6) zero bytes enabledDevices: *enabledDevices, lastUserInput: time.Now(), log: logger, diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx index 02ff170..8d3b40e 100644 --- a/ui/src/components/InfoBar.tsx +++ b/ui/src/components/InfoBar.tsx @@ -1,47 +1,65 @@ -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { cx } from "@/cva.config"; import { + HidState, + KeysDownState, + MouseState, + RTCState, + SettingsState, useHidStore, useMouseStore, useRTCStore, useSettingsStore, useVideoStore, + VideoState, } from "@/hooks/stores"; import { keys, modifiers } from "@/keyboardMappings"; export default function InfoBar() { - const activeKeys = useHidStore(state => state.activeKeys); - const activeModifiers = useHidStore(state => state.activeModifiers); - const mouseX = useMouseStore(state => state.mouseX); - const mouseY = useMouseStore(state => state.mouseY); - const mouseMove = useMouseStore(state => state.mouseMove); + const keysDownState = useHidStore((state: HidState) => state.keysDownState); + const mouseX = useMouseStore((state: MouseState) => state.mouseX); + const mouseY = useMouseStore((state: MouseState) => state.mouseY); + const mouseMove = useMouseStore((state: MouseState) => state.mouseMove); const videoClientSize = useVideoStore( - state => `${Math.round(state.clientWidth)}x${Math.round(state.clientHeight)}`, + (state: VideoState) => `${Math.round(state.clientWidth)}x${Math.round(state.clientHeight)}`, ); const videoSize = useVideoStore( - state => `${Math.round(state.width)}x${Math.round(state.height)}`, + (state: VideoState) => `${Math.round(state.width)}x${Math.round(state.height)}`, ); - const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); + const rpcDataChannel = useRTCStore((state: RTCState) => state.rpcDataChannel); const settings = useSettingsStore(); - const showPressedKeys = useSettingsStore(state => state.showPressedKeys); + const showPressedKeys = useSettingsStore((state: SettingsState) => state.showPressedKeys); useEffect(() => { if (!rpcDataChannel) return; rpcDataChannel.onclose = () => console.log("rpcDataChannel has closed"); - rpcDataChannel.onerror = e => + rpcDataChannel.onerror = (e: Event) => console.log(`Error on DataChannel '${rpcDataChannel.label}': ${e}`); }, [rpcDataChannel]); - const keyboardLedState = useHidStore(state => state.keyboardLedState); - const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse); + const keyboardLedState = useHidStore((state: HidState) => state.keyboardLedState); + const isTurnServerInUse = useRTCStore((state: RTCState) => state.isTurnServerInUse); - const usbState = useHidStore(state => state.usbState); - const hdmiState = useVideoStore(state => state.hdmiState); + const usbState = useHidStore((state: HidState) => state.usbState); + const hdmiState = useVideoStore((state: VideoState) => state.hdmiState); + + const displayKeys = useMemo(() => { + if (!showPressedKeys || !keysDownState) + return ""; + + const state = keysDownState as KeysDownState; + const activeModifierMask = state.modifier || 0; + const keysDown = state.keys || []; + const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name); + const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name); + + return [...modifierNames,...keyNames].join(", "); + }, [keysDownState, showPressedKeys]); return (
@@ -99,14 +117,7 @@ export default function InfoBar() {
Keys:

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

)} diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index e788faa..223d994 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -47,12 +47,12 @@ export interface User { picture?: string; } -interface UserState { +export interface UserState { user: User | null; setUser: (user: User | null) => void; } -interface UIState { +export interface UIState { sidebarView: AvailableSidebarViews | null; setSidebarView: (view: AvailableSidebarViews | null) => void; @@ -68,21 +68,21 @@ interface UIState { setAttachedVirtualKeyboardVisibility: (enabled: boolean) => void; terminalType: AvailableTerminalTypes; - setTerminalType: (enabled: UIState["terminalType"]) => void; + setTerminalType: (type: UIState["terminalType"]) => void; } export const useUiStore = create(set => ({ terminalType: "none", - setTerminalType: type => set({ terminalType: type }), + setTerminalType: (type: UIState["terminalType"]) => set({ terminalType: type }), sidebarView: null, - setSidebarView: view => set({ sidebarView: view }), + setSidebarView: (view: AvailableSidebarViews | null) => set({ sidebarView: view }), disableVideoFocusTrap: false, - setDisableVideoFocusTrap: enabled => set({ disableVideoFocusTrap: enabled }), + setDisableVideoFocusTrap: (enabled: boolean) => set({ disableVideoFocusTrap: enabled }), isWakeOnLanModalVisible: false, - setWakeOnLanModalVisibility: enabled => set({ isWakeOnLanModalVisible: enabled }), + setWakeOnLanModalVisibility: (enabled: boolean) => set({ isWakeOnLanModalVisible: enabled }), toggleSidebarView: view => set(state => { @@ -94,11 +94,11 @@ export const useUiStore = create(set => ({ }), isAttachedVirtualKeyboardVisible: true, - setAttachedVirtualKeyboardVisibility: enabled => + setAttachedVirtualKeyboardVisibility: (enabled: boolean) => set({ isAttachedVirtualKeyboardVisible: enabled }), })); -interface RTCState { +export interface RTCState { peerConnection: RTCPeerConnection | null; setPeerConnection: (pc: RTCState["peerConnection"]) => void; @@ -118,18 +118,18 @@ interface RTCState { setMediaStream: (stream: MediaStream) => void; videoStreamStats: RTCInboundRtpStreamStats | null; - appendVideoStreamStats: (state: RTCInboundRtpStreamStats) => void; + appendVideoStreamStats: (stats: RTCInboundRtpStreamStats) => void; videoStreamStatsHistory: Map; isTurnServerInUse: boolean; setTurnServerInUse: (inUse: boolean) => void; inboundRtpStats: Map; - appendInboundRtpStats: (state: RTCInboundRtpStreamStats) => void; + appendInboundRtpStats: (stats: RTCInboundRtpStreamStats) => void; clearInboundRtpStats: () => void; candidatePairStats: Map; - appendCandidatePairStats: (pair: RTCIceCandidatePairStats) => void; + appendCandidatePairStats: (stats: RTCIceCandidatePairStats) => void; clearCandidatePairStats: () => void; // Remote ICE candidates stat type doesn't exist as of today @@ -141,7 +141,7 @@ interface RTCState { // Disk data channel stats type doesn't exist as of today diskDataChannelStats: Map; - appendDiskDataChannelStats: (stat: RTCDataChannelStats) => void; + appendDiskDataChannelStats: (stats: RTCDataChannelStats) => void; terminalChannel: RTCDataChannel | null; setTerminalChannel: (channel: RTCDataChannel) => void; @@ -149,78 +149,78 @@ interface RTCState { export const useRTCStore = create(set => ({ peerConnection: null, - setPeerConnection: pc => set({ peerConnection: pc }), + setPeerConnection: (pc: RTCState["peerConnection"]) => set({ peerConnection: pc }), rpcDataChannel: null, - setRpcDataChannel: channel => set({ rpcDataChannel: channel }), + setRpcDataChannel: (channel: RTCDataChannel) => set({ rpcDataChannel: channel }), transceiver: null, - setTransceiver: transceiver => set({ transceiver }), + setTransceiver: (transceiver: RTCRtpTransceiver) => set({ transceiver }), peerConnectionState: null, - setPeerConnectionState: state => set({ peerConnectionState: state }), + setPeerConnectionState: (state: RTCPeerConnectionState) => set({ peerConnectionState: state }), diskChannel: null, - setDiskChannel: channel => set({ diskChannel: channel }), + setDiskChannel: (channel: RTCDataChannel) => set({ diskChannel: channel }), mediaStream: null, - setMediaStream: stream => set({ mediaStream: stream }), + setMediaStream: (stream: MediaStream) => set({ mediaStream: stream }), videoStreamStats: null, - appendVideoStreamStats: stats => set({ videoStreamStats: stats }), + appendVideoStreamStats: (stats: RTCInboundRtpStreamStats) => set({ videoStreamStats: stats }), videoStreamStatsHistory: new Map(), isTurnServerInUse: false, - setTurnServerInUse: inUse => set({ isTurnServerInUse: inUse }), + setTurnServerInUse: (inUse: boolean) => set({ isTurnServerInUse: inUse }), inboundRtpStats: new Map(), - appendInboundRtpStats: newStat => { + appendInboundRtpStats: (stats: RTCInboundRtpStreamStats) => { set(prevState => ({ - inboundRtpStats: appendStatToMap(newStat, prevState.inboundRtpStats), + inboundRtpStats: appendStatToMap(stats, prevState.inboundRtpStats), })); }, clearInboundRtpStats: () => set({ inboundRtpStats: new Map() }), candidatePairStats: new Map(), - appendCandidatePairStats: newStat => { + appendCandidatePairStats: (stats: RTCIceCandidatePairStats) => { set(prevState => ({ - candidatePairStats: appendStatToMap(newStat, prevState.candidatePairStats), + candidatePairStats: appendStatToMap(stats, prevState.candidatePairStats), })); }, clearCandidatePairStats: () => set({ candidatePairStats: new Map() }), localCandidateStats: new Map(), - appendLocalCandidateStats: newStat => { + appendLocalCandidateStats: (stats: RTCIceCandidateStats) => { set(prevState => ({ - localCandidateStats: appendStatToMap(newStat, prevState.localCandidateStats), + localCandidateStats: appendStatToMap(stats, prevState.localCandidateStats), })); }, remoteCandidateStats: new Map(), - appendRemoteCandidateStats: newStat => { + appendRemoteCandidateStats: (stats: RTCIceCandidateStats) => { set(prevState => ({ - remoteCandidateStats: appendStatToMap(newStat, prevState.remoteCandidateStats), + remoteCandidateStats: appendStatToMap(stats, prevState.remoteCandidateStats), })); }, diskDataChannelStats: new Map(), - appendDiskDataChannelStats: newStat => { + appendDiskDataChannelStats: (stats: RTCDataChannelStats) => { set(prevState => ({ - diskDataChannelStats: appendStatToMap(newStat, prevState.diskDataChannelStats), + diskDataChannelStats: appendStatToMap(stats, prevState.diskDataChannelStats), })); }, // Add these new properties to the store implementation terminalChannel: null, - setTerminalChannel: channel => set({ terminalChannel: channel }), + setTerminalChannel: (channel: RTCDataChannel) => set({ terminalChannel: channel }), })); -interface MouseMove { +export interface MouseMove { x: number; y: number; buttons: number; } -interface MouseState { +export interface MouseState { mouseX: number; mouseY: number; mouseMove?: MouseMove; @@ -232,9 +232,14 @@ export const useMouseStore = create(set => ({ mouseX: 0, mouseY: 0, setMouseMove: (move?: MouseMove) => set({ mouseMove: move }), - setMousePosition: (x, y) => set({ mouseX: x, mouseY: y }), + setMousePosition: (x: number, y: number) => set({ mouseX: x, mouseY: y }), })); +export interface HdmiState { + ready: boolean; + error?: Extract; +} + export interface VideoState { width: number; height: number; @@ -263,13 +268,13 @@ export const useVideoStore = create(set => ({ clientHeight: 0, // The video element's client size - setClientSize: (clientWidth, clientHeight) => set({ clientWidth, clientHeight }), + setClientSize: (clientWidth: number, clientHeight: number) => set({ clientWidth, clientHeight }), // Resolution - setSize: (width, height) => set({ width, height }), + setSize: (width: number, height: number) => set({ width, height }), hdmiState: "connecting", - setHdmiState: state => { + setHdmiState: (state: HdmiState) => { if (!state) return; const { ready, error } = state; @@ -283,7 +288,7 @@ export const useVideoStore = create(set => ({ }, })); -interface SettingsState { +export interface SettingsState { isCursorHidden: boolean; setCursorVisibility: (enabled: boolean) => void; @@ -325,17 +330,17 @@ export const useSettingsStore = create( persist( set => ({ isCursorHidden: false, - setCursorVisibility: enabled => set({ isCursorHidden: enabled }), + setCursorVisibility: (enabled: boolean) => set({ isCursorHidden: enabled }), mouseMode: "absolute", - setMouseMode: mode => set({ mouseMode: mode }), + setMouseMode: (mode: string) => set({ mouseMode: mode }), debugMode: import.meta.env.DEV, - setDebugMode: enabled => set({ debugMode: enabled }), + setDebugMode: (enabled: boolean) => set({ debugMode: enabled }), // Add developer mode with default value developerMode: false, - setDeveloperMode: enabled => set({ developerMode: enabled }), + setDeveloperMode: (enabled: boolean) => set({ developerMode: enabled }), displayRotation: "270", setDisplayRotation: (rotation: string) => set({ displayRotation: rotation }), @@ -349,21 +354,21 @@ export const useSettingsStore = create( set({ backlightSettings: settings }), keyboardLayout: "en-US", - setKeyboardLayout: layout => set({ keyboardLayout: layout }), + setKeyboardLayout: (layout: string) => set({ keyboardLayout: layout }), scrollThrottling: 0, - setScrollThrottling: value => set({ scrollThrottling: value }), + setScrollThrottling: (value: number) => set({ scrollThrottling: value }), showPressedKeys: true, - setShowPressedKeys: show => set({ showPressedKeys: show }), + setShowPressedKeys: (show: boolean) => set({ showPressedKeys: show }), // Video enhancement settings with default values (1.0 = normal) videoSaturation: 1.0, - setVideoSaturation: value => set({ videoSaturation: value }), + setVideoSaturation: (value: number) => set({ videoSaturation: value }), videoBrightness: 1.0, - setVideoBrightness: value => set({ videoBrightness: value }), + setVideoBrightness: (value: number) => set({ videoBrightness: value }), videoContrast: 1.0, - setVideoContrast: value => set({ videoContrast: value }), + setVideoContrast: (value: number) => set({ videoContrast: value }), }), { name: "settings", @@ -403,23 +408,23 @@ export interface MountMediaState { export const useMountMediaStore = create(set => ({ localFile: null, - setLocalFile: file => set({ localFile: file }), + setLocalFile: (file: MountMediaState["localFile"]) => set({ localFile: file }), remoteVirtualMediaState: null, - setRemoteVirtualMediaState: state => set({ remoteVirtualMediaState: state }), + setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => set({ remoteVirtualMediaState: state }), modalView: "mode", - setModalView: view => set({ modalView: view }), + setModalView: (view: MountMediaState["modalView"]) => set({ modalView: view }), isMountMediaDialogOpen: false, - setIsMountMediaDialogOpen: isOpen => set({ isMountMediaDialogOpen: isOpen }), + setIsMountMediaDialogOpen: (isOpen: MountMediaState["isMountMediaDialogOpen"]) => set({ isMountMediaDialogOpen: isOpen }), uploadedFiles: [], - addUploadedFile: file => + addUploadedFile: (file: { name: string; size: string; uploadedAt: string }) => set(state => ({ uploadedFiles: [...state.uploadedFiles, file] })), errorMessage: null, - setErrorMessage: message => set({ errorMessage: message }), + setErrorMessage: (message: string | null) => set({ errorMessage: message }), })); export interface KeyboardLedState { @@ -437,14 +442,6 @@ export interface KeysDownState { } export interface HidState { - activeKeys: number[]; - activeModifiers: number[]; - - updateActiveKeysAndModifiers: (keysAndModifiers: { - keys: number[]; - modifiers: number[]; - }) => void; - altGrArmed: boolean; setAltGrArmed: (armed: boolean) => void; @@ -470,38 +467,31 @@ export interface HidState { setUsbState: (state: HidState["usbState"]) => void; } -export const useHidStore = create((set) => ({ - activeKeys: [], - activeModifiers: [], - - updateActiveKeysAndModifiers: ({ keys, modifiers }) => { - return set({ activeKeys: keys, activeModifiers: modifiers }); - }, - +export const useHidStore = create(set => ({ altGrArmed: false, - setAltGrArmed: armed => set({ altGrArmed: armed }), + setAltGrArmed: (armed: boolean): void => set({ altGrArmed: armed }), altGrTimer: 0, - setAltGrTimer: timeout => set({ altGrTimer: timeout }), + setAltGrTimer: (timeout: number | null): void => set({ altGrTimer: timeout }), altGrCtrlTime: 0, - setAltGrCtrlTime: time => set({ altGrCtrlTime: time }), + setAltGrCtrlTime: (time: number): void => set({ altGrCtrlTime: time }), keyboardLedState: undefined, - setKeyboardLedState: ledState => set({ keyboardLedState: ledState }), + setKeyboardLedState: (ledState: KeyboardLedState): void => set({ keyboardLedState: ledState }), keysDownState: undefined, - setKeysDownState: state => set({ keysDownState: state }), + setKeysDownState: (state: KeysDownState): void => set({ keysDownState: state }), isVirtualKeyboardEnabled: false, - setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }), + setVirtualKeyboardEnabled: (enabled: boolean): void => set({ isVirtualKeyboardEnabled: enabled }), isPasteModeEnabled: false, - setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }), + setPasteModeEnabled: (enabled: boolean): void => set({ isPasteModeEnabled: enabled }), // Add these new properties for USB state usbState: "not attached", - setUsbState: state => set({ usbState: state }), + setUsbState: (state: HidState["usbState"]) => set({ usbState: state }), })); export const useUserStore = create(set => ({ @@ -559,7 +549,7 @@ export interface UpdateState { export const useUpdateStore = create(set => ({ isUpdatePending: false, - setIsUpdatePending: isPending => set({ isUpdatePending: isPending }), + setIsUpdatePending: (isPending: boolean) => set({ isUpdatePending: isPending }), setOtaState: state => set({ otaState: state }), otaState: { @@ -583,12 +573,12 @@ export const useUpdateStore = create(set => ({ }, updateDialogHasBeenMinimized: false, - setUpdateDialogHasBeenMinimized: hasBeenMinimized => + setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => set({ updateDialogHasBeenMinimized: hasBeenMinimized }), modalView: "loading", - setModalView: view => set({ modalView: view }), + setModalView: (view: UpdateState["modalView"]) => set({ modalView: view }), updateErrorMessage: null, - setUpdateErrorMessage: errorMessage => set({ updateErrorMessage: errorMessage }), + setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }), })); interface UsbConfigModalState { @@ -609,8 +599,8 @@ export interface UsbConfigState { export const useUsbConfigModalStore = create(set => ({ modalView: "updateUsbConfig", errorMessage: null, - setModalView: view => set({ modalView: view }), - setErrorMessage: message => set({ errorMessage: message }), + setModalView: (view: UsbConfigModalState["modalView"]) => set({ modalView: view }), + setErrorMessage: (message: string | null) => set({ errorMessage: message }), })); interface LocalAuthModalState { @@ -626,7 +616,7 @@ interface LocalAuthModalState { export const useLocalAuthModalStore = create(set => ({ modalView: "createPassword", - setModalView: view => set({ modalView: view }), + setModalView: (view: LocalAuthModalState["modalView"]) => set({ modalView: view }), })); export interface DeviceState { @@ -641,8 +631,8 @@ export const useDeviceStore = create(set => ({ appVersion: null, systemVersion: null, - setAppVersion: version => set({ appVersion: version }), - setSystemVersion: version => set({ systemVersion: version }), + setAppVersion: (version: string) => set({ appVersion: version }), + setSystemVersion: (version: string) => set({ systemVersion: version }), })); export interface DhcpLease { @@ -808,7 +798,7 @@ export const useMacrosStore = create((set, get) => ({ try { await new Promise((resolve, reject) => { - sendFn("getKeyboardMacros", {}, response => { + sendFn("getKeyboardMacros", {}, (response: JsonRpcResponse) => { if (response.error) { console.error("Error loading macros:", response.error); reject(new Error(response.error.message)); @@ -888,7 +878,7 @@ export const useMacrosStore = create((set, get) => ({ sendFn( "setKeyboardMacros", { params: { macros: macrosWithSortOrder } }, - response => { + (response: JsonRpcResponse) => { resolve(response); }, ); diff --git a/ui/src/hooks/useJsonRpc.ts b/ui/src/hooks/useJsonRpc.ts index 92b56ff..5f088dc 100644 --- a/ui/src/hooks/useJsonRpc.ts +++ b/ui/src/hooks/useJsonRpc.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect } from "react"; -import { useRTCStore } from "@/hooks/stores"; +import { RTCState, useRTCStore } from "@/hooks/stores"; export interface JsonRpcRequest { jsonrpc: string; @@ -33,7 +33,7 @@ const callbackStore = new Map void>( let requestCounter = 0; export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) { - const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); + const rpcDataChannel = useRTCStore((state: RTCState) => state.rpcDataChannel); const send = useCallback( (method: string, params: unknown, callback?: (resp: JsonRpcResponse) => void) => { diff --git a/ui/src/hooks/useKeyboard.ts b/ui/src/hooks/useKeyboard.ts index 7861dd9..8aa58ab 100644 --- a/ui/src/hooks/useKeyboard.ts +++ b/ui/src/hooks/useKeyboard.ts @@ -1,16 +1,14 @@ import { useCallback } from "react"; -import { KeysDownState, useHidStore, useRTCStore } from "@/hooks/stores"; -import { useJsonRpc } from "@/hooks/useJsonRpc"; +import { KeysDownState, HidState, useHidStore, RTCState, useRTCStore } from "@/hooks/stores"; +import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { keys, modifiers } from "@/keyboardMappings"; export default function useKeyboard() { const [send] = useJsonRpc(); - const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); - const updateActiveKeysAndModifiers = useHidStore( - state => state.updateActiveKeysAndModifiers, - ); + const rpcDataChannel = useRTCStore((state: RTCState) => state.rpcDataChannel); + const setKeysDownState = useHidStore((state: HidState) => state.setKeysDownState); const sendKeyboardEvent = useCallback( (keys: number[], modifiers: number[]) => { @@ -18,35 +16,30 @@ export default function useKeyboard() { const accModifier = modifiers.reduce((acc, val) => acc + val, 0); send("keyboardReport", { keys, modifier: accModifier }); - + //TODO would be nice if the keyboardReport rpc call returned the current state like keypressReport does // We do this for the info bar to display the currently pressed keys for the user - updateActiveKeysAndModifiers({ keys: keys, modifiers: modifiers }); + setKeysDownState({ keys: keys, modifier: accModifier }); }, - [rpcDataChannel?.readyState, send, updateActiveKeysAndModifiers], + [rpcDataChannel?.readyState, send, setKeysDownState], ); - const modifiersFromModifierMask = (activeModifiers: number): number[] => { - return Object.values(modifiers).filter(m => (activeModifiers & m) !== 0); - } - const sendKeypressEvent = useCallback( (key: number, press: boolean) => { if (rpcDataChannel?.readyState !== "open") return; - send("keypressReport", { key, press }, resp => { + send("keypressReport", { key, press }, (resp: JsonRpcResponse) => { if ("error" in resp) { console.error("Failed to send keypress:", resp.error); } else { + console.log(resp); const keyDownState = resp.result as KeysDownState; - const keysDown = keyDownState.keys; - const activeModifiers = modifiersFromModifierMask(keyDownState.modifier) - + console.log("Key Down State:", keyDownState); // We do this for the info bar to display the currently pressed keys for the user - updateActiveKeysAndModifiers({ keys: keysDown, modifiers: activeModifiers }); + setKeysDownState(keyDownState); } }); }, - [rpcDataChannel?.readyState, send, updateActiveKeysAndModifiers], + [rpcDataChannel?.readyState, send, setKeysDownState], ); const resetKeyboardState = useCallback(() => { diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index d5753ab..d924bd6 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -18,10 +18,14 @@ import useWebSocket from "react-use-websocket"; import { cx } from "@/cva.config"; import { + DeviceState, HidState, KeyboardLedState, KeysDownState, + MountMediaState, NetworkState, + RTCState, + UIState, UpdateState, useDeviceStore, useHidStore, @@ -38,7 +42,7 @@ import WebRTCVideo from "@components/WebRTCVideo"; import { checkAuth, isInCloud, isOnDevice } from "@/main"; import DashboardNavbar from "@components/Header"; import ConnectionStatsSidebar from "@/components/sidebar/connectionStats"; -import { JsonRpcRequest, useJsonRpc } from "@/hooks/useJsonRpc"; +import { JsonRpcRequest, JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import Terminal from "@components/Terminal"; import { CLOUD_API, DEVICE_API } from "@/ui.config"; @@ -128,18 +132,18 @@ export default function KvmIdRoute() { const authMode = "authMode" in loaderResp ? loaderResp.authMode : null; const params = useParams() as { id: string }; - const sidebarView = useUiStore(state => state.sidebarView); + const sidebarView = useUiStore((state: UIState) => state.sidebarView); const [queryParams, setQueryParams] = useSearchParams(); - const setIsTurnServerInUse = useRTCStore(state => state.setTurnServerInUse); - const peerConnection = useRTCStore(state => state.peerConnection); - const setPeerConnectionState = useRTCStore(state => state.setPeerConnectionState); - const peerConnectionState = useRTCStore(state => state.peerConnectionState); - const setMediaMediaStream = useRTCStore(state => state.setMediaStream); - const setPeerConnection = useRTCStore(state => state.setPeerConnection); - const setDiskChannel = useRTCStore(state => state.setDiskChannel); - const setRpcDataChannel = useRTCStore(state => state.setRpcDataChannel); - const setTransceiver = useRTCStore(state => state.setTransceiver); + const setIsTurnServerInUse = useRTCStore((state: RTCState) => state.setTurnServerInUse); + const peerConnection = useRTCStore((state: RTCState) => state.peerConnection); + const setPeerConnectionState = useRTCStore((state: RTCState) => state.setPeerConnectionState); + const peerConnectionState = useRTCStore((state: RTCState) => state.peerConnectionState); + const setMediaMediaStream = useRTCStore((state: RTCState) => state.setMediaStream); + const setPeerConnection = useRTCStore((state: RTCState) => state.setPeerConnection); + const setDiskChannel = useRTCStore((state: RTCState) => state.setDiskChannel); + const setRpcDataChannel = useRTCStore((state: RTCState) => state.setRpcDataChannel); + const setTransceiver = useRTCStore((state: RTCState) => state.setTransceiver); const location = useLocation(); const isLegacySignalingEnabled = useRef(false); @@ -513,9 +517,9 @@ export default function KvmIdRoute() { }, [peerConnectionState, cleanupAndStopReconnecting]); // Cleanup effect - const clearInboundRtpStats = useRTCStore(state => state.clearInboundRtpStats); - const clearCandidatePairStats = useRTCStore(state => state.clearCandidatePairStats); - const setSidebarView = useUiStore(state => state.setSidebarView); + const clearInboundRtpStats = useRTCStore((state: RTCState) => state.clearInboundRtpStats); + const clearCandidatePairStats = useRTCStore((state: RTCState) => state.clearCandidatePairStats); + const setSidebarView = useUiStore((state: UIState) => state.setSidebarView); useEffect(() => { return () => { @@ -550,7 +554,7 @@ export default function KvmIdRoute() { }, [peerConnectionState, setIsTurnServerInUse]); // TURN server usage reporting - const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse); + const isTurnServerInUse = useRTCStore((state: RTCState) => state.isTurnServerInUse); const lastBytesReceived = useRef(0); const lastBytesSent = useRef(0); @@ -583,16 +587,16 @@ export default function KvmIdRoute() { }); }, 10000); - const setNetworkState = useNetworkStateStore(state => state.setNetworkState); + const setNetworkState = useNetworkStateStore((state: NetworkState) => state.setNetworkState); - const setUsbState = useHidStore(state => state.setUsbState); - const setHdmiState = useVideoStore(state => state.setHdmiState); + const setUsbState = useHidStore((state: HidState) => state.setUsbState); + const setHdmiState = useVideoStore((state: VideoState) => state.setHdmiState); - const keyboardLedState = useHidStore(state => state.keyboardLedState); - const setKeyboardLedState = useHidStore(state => state.setKeyboardLedState); + const keyboardLedState = useHidStore((state: HidState) => state.keyboardLedState); + const setKeyboardLedState = useHidStore((state: HidState) => state.setKeyboardLedState); - const keysDownState = useHidStore(state => state.keysDownState); - const setKeysDownState = useHidStore(state => state.setKeysDownState); + const keysDownState = useHidStore((state: HidState) => state.keysDownState); + const setKeysDownState = useHidStore((state: HidState) => state.setKeysDownState); const [hasUpdated, setHasUpdated] = useState(false); const { navigateTo } = useDeviceUiNavigation(); @@ -652,7 +656,7 @@ export default function KvmIdRoute() { } } - const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); + const rpcDataChannel = useRTCStore((state: RTCState) => state.rpcDataChannel); const [send] = useJsonRpc(onJsonRpcRequest); useEffect(() => { @@ -702,8 +706,8 @@ export default function KvmIdRoute() { } }, [navigate, navigateTo, queryParams, setModalView, setQueryParams]); - const diskChannel = useRTCStore(state => state.diskChannel)!; - const file = useMountMediaStore(state => state.localFile)!; + const diskChannel = useRTCStore((state: RTCState) => state.diskChannel)!; + const file = useMountMediaStore((state: MountMediaState) => state.localFile)!; useEffect(() => { if (!diskChannel || !file) return; diskChannel.onmessage = async e => { @@ -723,7 +727,7 @@ export default function KvmIdRoute() { }, [diskChannel, file]); // System update - const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap); + const disableVideoFocusTrap = useUiStore((state: UIState) => state.disableVideoFocusTrap); const [kvmTerminal, setKvmTerminal] = useState(null); const [serialConsole, setSerialConsole] = useState(null); @@ -744,14 +748,14 @@ export default function KvmIdRoute() { if (location.pathname !== "/other-session") navigateTo("/"); }, [navigateTo, location.pathname]); - const appVersion = useDeviceStore(state => state.appVersion); - const setAppVersion = useDeviceStore(state => state.setAppVersion); - const setSystemVersion = useDeviceStore(state => state.setSystemVersion); + const appVersion = useDeviceStore((state: DeviceState) => state.appVersion); + const setAppVersion = useDeviceStore((state: DeviceState) => state.setAppVersion); + const setSystemVersion = useDeviceStore((state: DeviceState) => state.setSystemVersion); useEffect(() => { if (appVersion) return; - send("getUpdateStatus", {}, async resp => { + send("getUpdateStatus", {}, async (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error(`Failed to get device version: ${resp.error}`); return diff --git a/usb.go b/usb.go index 3cf6908..d4d2551 100644 --- a/usb.go +++ b/usb.go @@ -43,11 +43,11 @@ func initUsbGadget() { } } -func rpcKeyboardReport(modifier byte, keys []byte) error { +func rpcKeyboardReport(modifier uint8, keys []uint8) error { return gadget.KeyboardReport(modifier, keys) } -func rpcKeypressReport(key byte, press bool) (usbgadget.KeysDownState, error) { +func rpcKeypressReport(key uint8, press bool) (usbgadget.KeysDownState, error) { return gadget.KeypressReport(key, press) }