From 0c5c69f2d39681c92741ec8551c4dbf0f82e2709 Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Fri, 23 May 2025 00:12:18 +0200 Subject: [PATCH 01/11] feat: sync keyboard led status (#502) --- internal/usbgadget/hid_keyboard.go | 142 ++++++++++++++++++++++++-- internal/usbgadget/usbgadget.go | 39 ++++--- jsonrpc.go | 1 + ui/src/components/InfoBar.tsx | 20 ++-- ui/src/components/VirtualKeyboard.tsx | 27 ++--- ui/src/components/WebRTCVideo.tsx | 18 ---- ui/src/hooks/stores.ts | 27 ++--- ui/src/routes/devices.$id.tsx | 22 ++++ usb.go | 15 +++ 9 files changed, 236 insertions(+), 75 deletions(-) diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go index de007e4..12b0de9 100644 --- a/internal/usbgadget/hid_keyboard.go +++ b/internal/usbgadget/hid_keyboard.go @@ -1,8 +1,11 @@ package usbgadget import ( + "context" "fmt" "os" + "reflect" + "time" ) var keyboardConfig = gadgetConfigItem{ @@ -36,6 +39,7 @@ var keyboardReportDesc = []byte{ 0x81, 0x03, /* INPUT (Cnst,Var,Abs) */ 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) */ @@ -54,13 +58,139 @@ var keyboardReportDesc = []byte{ 0xc0, /* END_COLLECTION */ } -func (u *UsbGadget) keyboardWriteHidFile(data []byte) error { - if u.keyboardHidFile == nil { - var err error - u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666) - if err != nil { - return fmt.Errorf("failed to open hidg0: %w", err) +const ( + hidReadBufferSize = 8 + // https://www.usb.org/sites/default/files/documents/hid1_11.pdf + // https://www.usb.org/sites/default/files/hut1_2.pdf + KeyboardLedMaskNumLock = 1 << 0 + KeyboardLedMaskCapsLock = 1 << 1 + KeyboardLedMaskScrollLock = 1 << 2 + KeyboardLedMaskCompose = 1 << 3 + KeyboardLedMaskKana = 1 << 4 + ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana +) + +// Synchronization between LED states and CAPS LOCK, NUM LOCK, SCROLL LOCK, +// COMPOSE, and KANA events is maintained by the host and NOT the keyboard. If +// using the keyboard descriptor in Appendix B, LED states are set by sending a +// 5-bit absolute report to the keyboard via a Set_Report(Output) request. +type KeyboardState struct { + NumLock bool `json:"num_lock"` + CapsLock bool `json:"caps_lock"` + ScrollLock bool `json:"scroll_lock"` + Compose bool `json:"compose"` + Kana bool `json:"kana"` +} + +func getKeyboardState(b byte) KeyboardState { + // should we check if it's the correct usage page? + return KeyboardState{ + NumLock: b&KeyboardLedMaskNumLock != 0, + CapsLock: b&KeyboardLedMaskCapsLock != 0, + ScrollLock: b&KeyboardLedMaskScrollLock != 0, + Compose: b&KeyboardLedMaskCompose != 0, + Kana: b&KeyboardLedMaskKana != 0, + } +} + +func (u *UsbGadget) updateKeyboardState(b byte) { + u.keyboardStateLock.Lock() + defer u.keyboardStateLock.Unlock() + + if b&^ValidKeyboardLedMasks != 0 { + u.log.Trace().Uint8("b", b).Msg("contains invalid bits, ignoring") + return + } + + newState := getKeyboardState(b) + if reflect.DeepEqual(u.keyboardState, newState) { + return + } + u.log.Info().Interface("old", u.keyboardState).Interface("new", newState).Msg("keyboardState updated") + u.keyboardState = newState + + if u.onKeyboardStateChange != nil { + (*u.onKeyboardStateChange)(newState) + } +} + +func (u *UsbGadget) SetOnKeyboardStateChange(f func(state KeyboardState)) { + u.onKeyboardStateChange = &f +} + +func (u *UsbGadget) GetKeyboardState() KeyboardState { + u.keyboardStateLock.Lock() + defer u.keyboardStateLock.Unlock() + + return u.keyboardState +} + +func (u *UsbGadget) listenKeyboardEvents() { + var path string + if u.keyboardHidFile != nil { + path = u.keyboardHidFile.Name() + } + l := u.log.With().Str("listener", "keyboardEvents").Str("path", path).Logger() + l.Trace().Msg("starting") + + go func() { + buf := make([]byte, hidReadBufferSize) + for { + select { + case <-u.keyboardStateCtx.Done(): + l.Info().Msg("context done") + return + default: + l.Trace().Msg("reading from keyboard") + if u.keyboardHidFile == nil { + l.Error().Msg("keyboardHidFile is nil") + time.Sleep(time.Second) + continue + } + n, err := u.keyboardHidFile.Read(buf) + if err != nil { + l.Error().Err(err).Msg("failed to read") + continue + } + l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard") + if n != 1 { + l.Trace().Int("n", n).Msg("expected 1 byte, got") + continue + } + u.updateKeyboardState(buf[0]) + } } + }() +} + +func (u *UsbGadget) openKeyboardHidFile() error { + if u.keyboardHidFile != nil { + return nil + } + + var err error + u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666) + if err != nil { + return fmt.Errorf("failed to open hidg0: %w", err) + } + + if u.keyboardStateCancel != nil { + u.keyboardStateCancel() + } + + u.keyboardStateCtx, u.keyboardStateCancel = context.WithCancel(context.Background()) + u.listenKeyboardEvents() + + return nil +} + +func (u *UsbGadget) OpenKeyboardHidFile() error { + return u.openKeyboardHidFile() +} + +func (u *UsbGadget) keyboardWriteHidFile(data []byte) error { + if err := u.openKeyboardHidFile(); err != nil { + return err } _, err := u.keyboardHidFile.Write(data) diff --git a/internal/usbgadget/usbgadget.go b/internal/usbgadget/usbgadget.go index f8b2b3e..2eab822 100644 --- a/internal/usbgadget/usbgadget.go +++ b/internal/usbgadget/usbgadget.go @@ -3,6 +3,7 @@ package usbgadget import ( + "context" "os" "path" "sync" @@ -59,6 +60,11 @@ type UsbGadget struct { relMouseHidFile *os.File relMouseLock sync.Mutex + keyboardState KeyboardState + keyboardStateLock sync.Mutex + keyboardStateCtx context.Context + keyboardStateCancel context.CancelFunc + enabledDevices Devices strictMode bool // only intended for testing for now @@ -70,6 +76,8 @@ type UsbGadget struct { tx *UsbGadgetTransaction txLock sync.Mutex + onKeyboardStateChange *func(state KeyboardState) + log *zerolog.Logger } @@ -96,20 +104,25 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev config = &Config{isEmpty: true} } + keyboardCtx, keyboardCancel := context.WithCancel(context.Background()) + g := &UsbGadget{ - name: name, - kvmGadgetPath: path.Join(gadgetPath, name), - configC1Path: path.Join(gadgetPath, name, "configs/c.1"), - configMap: configMap, - customConfig: *config, - configLock: sync.Mutex{}, - keyboardLock: sync.Mutex{}, - absMouseLock: sync.Mutex{}, - relMouseLock: sync.Mutex{}, - txLock: sync.Mutex{}, - enabledDevices: *enabledDevices, - lastUserInput: time.Now(), - log: logger, + name: name, + kvmGadgetPath: path.Join(gadgetPath, name), + configC1Path: path.Join(gadgetPath, name, "configs/c.1"), + configMap: configMap, + customConfig: *config, + configLock: sync.Mutex{}, + keyboardLock: sync.Mutex{}, + absMouseLock: sync.Mutex{}, + relMouseLock: sync.Mutex{}, + txLock: sync.Mutex{}, + keyboardStateCtx: keyboardCtx, + keyboardStateCancel: keyboardCancel, + keyboardState: KeyboardState{}, + enabledDevices: *enabledDevices, + lastUserInput: time.Now(), + log: logger, strictMode: config.strictMode, diff --git a/jsonrpc.go b/jsonrpc.go index db25c6d..a32cab2 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -1017,6 +1017,7 @@ var rpcHandlers = map[string]RPCHandler{ "setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}}, "renewDHCPLease": {Func: rpcRenewDHCPLease}, "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, + "getKeyboardLedState": {Func: rpcGetKeyboardLedState}, "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx index aa00da7..4ee4149 100644 --- a/ui/src/components/InfoBar.tsx +++ b/ui/src/components/InfoBar.tsx @@ -36,9 +36,7 @@ export default function InfoBar() { console.log(`Error on DataChannel '${rpcDataChannel.label}': ${e}`); }, [rpcDataChannel]); - const isCapsLockActive = useHidStore(state => state.isCapsLockActive); - const isNumLockActive = useHidStore(state => state.isNumLockActive); - const isScrollLockActive = useHidStore(state => state.isScrollLockActive); + const keyboardLedState = useHidStore(state => state.keyboardLedState); const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse); @@ -121,7 +119,7 @@ export default function InfoBar() {
Scroll Lock
+ {keyboardLedState?.compose ? ( +
+ Compose +
+ ) : null} + {keyboardLedState?.kana ? ( +
+ Kana +
+ ) : null} diff --git a/ui/src/components/VirtualKeyboard.tsx b/ui/src/components/VirtualKeyboard.tsx index 03cb331..9566bca 100644 --- a/ui/src/components/VirtualKeyboard.tsx +++ b/ui/src/components/VirtualKeyboard.tsx @@ -1,7 +1,8 @@ +import { useShallow } from "zustand/react/shallow"; +import { ChevronDownIcon } from "@heroicons/react/16/solid"; +import { AnimatePresence, motion } from "framer-motion"; import { useCallback, useEffect, useRef, useState } from "react"; import Keyboard from "react-simple-keyboard"; -import { ChevronDownIcon } from "@heroicons/react/16/solid"; -import { motion, AnimatePresence } from "framer-motion"; import Card from "@components/Card"; // eslint-disable-next-line import/order @@ -9,12 +10,12 @@ import { Button } from "@components/Button"; import "react-simple-keyboard/build/css/index.css"; -import { useHidStore, useUiStore } from "@/hooks/stores"; -import { cx } from "@/cva.config"; -import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings"; -import useKeyboard from "@/hooks/useKeyboard"; -import DetachIconRaw from "@/assets/detach-icon.svg"; import AttachIconRaw from "@/assets/attach-icon.svg"; +import DetachIconRaw from "@/assets/detach-icon.svg"; +import { cx } from "@/cva.config"; +import { useHidStore, useUiStore } from "@/hooks/stores"; +import useKeyboard from "@/hooks/useKeyboard"; +import { keyDisplayMap, keys, modifiers } from "@/keyboardMappings"; export const DetachIcon = ({ className }: { className?: string }) => { return Detach Icon; @@ -40,8 +41,8 @@ function KeyboardWrapper() { const [isDragging, setIsDragging] = useState(false); const [position, setPosition] = useState({ x: 0, y: 0 }); const [newPosition, setNewPosition] = useState({ x: 0, y: 0 }); - const isCapsLockActive = useHidStore(state => state.isCapsLockActive); - const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive); + + const isCapsLockActive = useHidStore(useShallow(state => state.keyboardLedState?.caps_lock)); const startDrag = useCallback((e: MouseEvent | TouchEvent) => { if (!keyboardRef.current) return; @@ -157,17 +158,11 @@ function KeyboardWrapper() { toggleLayout(); if (isCapsLockActive) { - setIsCapsLockActive(false); sendKeyboardEvent([keys["CapsLock"]], []); return; } } - // Handle caps lock state change - if (isKeyCaps) { - setIsCapsLockActive(!isCapsLockActive); - } - // Collect new active keys and modifiers const newKeys = keys[cleanKey] ? [keys[cleanKey]] : []; const newModifiers = @@ -183,7 +178,7 @@ function KeyboardWrapper() { setTimeout(resetKeyboardState, 100); }, - [isCapsLockActive, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive], + [isCapsLockActive, sendKeyboardEvent, resetKeyboardState], ); const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled); diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 4fd4290..87f2d4e 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -55,10 +55,6 @@ export default function WebRTCVideo() { const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState); const isVideoLoading = !isPlaying; - // Keyboard related states - const { setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive } = - useHidStore(); - // Misc states and hooks const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap); const [send] = useJsonRpc(); @@ -355,10 +351,6 @@ export default function WebRTCVideo() { // console.log(document.activeElement); - setIsNumLockActive(e.getModifierState("NumLock")); - setIsCapsLockActive(e.getModifierState("CapsLock")); - setIsScrollLockActive(e.getModifierState("ScrollLock")); - if (code == "IntlBackslash" && ["`", "~"].includes(key)) { code = "Backquote"; } else if (code == "Backquote" && ["§", "±"].includes(key)) { @@ -388,9 +380,6 @@ export default function WebRTCVideo() { sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); }, [ - setIsNumLockActive, - setIsCapsLockActive, - setIsScrollLockActive, handleModifierKeys, sendKeyboardEvent, ], @@ -401,10 +390,6 @@ export default function WebRTCVideo() { e.preventDefault(); const prev = useHidStore.getState(); - setIsNumLockActive(e.getModifierState("NumLock")); - setIsCapsLockActive(e.getModifierState("CapsLock")); - setIsScrollLockActive(e.getModifierState("ScrollLock")); - // Filtering out the key that was just released (keys[e.code]) const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean); @@ -417,9 +402,6 @@ export default function WebRTCVideo() { sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); }, [ - setIsNumLockActive, - setIsCapsLockActive, - setIsScrollLockActive, handleModifierKeys, sendKeyboardEvent, ], diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 01d1257..1754a87 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -405,6 +405,14 @@ export const useMountMediaStore = create(set => ({ setErrorMessage: message => set({ errorMessage: message }), })); +export interface KeyboardLedState { + num_lock: boolean; + caps_lock: boolean; + scroll_lock: boolean; + compose: boolean; + kana: boolean; +} + export interface HidState { activeKeys: number[]; activeModifiers: number[]; @@ -423,18 +431,12 @@ export interface HidState { altGrCtrlTime: number; // _altGrCtrlTime setAltGrCtrlTime: (time: number) => void; - isNumLockActive: boolean; - setIsNumLockActive: (enabled: boolean) => void; - - isScrollLockActive: boolean; - setIsScrollLockActive: (enabled: boolean) => void; + keyboardLedState?: KeyboardLedState; + setKeyboardLedState: (state: KeyboardLedState) => void; isVirtualKeyboardEnabled: boolean; setVirtualKeyboardEnabled: (enabled: boolean) => void; - isCapsLockActive: boolean; - setIsCapsLockActive: (enabled: boolean) => void; - isPasteModeEnabled: boolean; setPasteModeEnabled: (enabled: boolean) => void; @@ -458,18 +460,11 @@ export const useHidStore = create(set => ({ altGrCtrlTime: 0, setAltGrCtrlTime: time => set({ altGrCtrlTime: time }), - isNumLockActive: false, - setIsNumLockActive: enabled => set({ isNumLockActive: enabled }), - - isScrollLockActive: false, - setIsScrollLockActive: enabled => set({ isScrollLockActive: enabled }), + setKeyboardLedState: ledState => set({ keyboardLedState: ledState }), isVirtualKeyboardEnabled: false, setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }), - isCapsLockActive: false, - setIsCapsLockActive: enabled => set({ isCapsLockActive: enabled }), - isPasteModeEnabled: false, setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }), diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index d35915b..a6be368 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -19,6 +19,7 @@ import useWebSocket from "react-use-websocket"; import { cx } from "@/cva.config"; import { HidState, + KeyboardLedState, NetworkState, UpdateState, useDeviceStore, @@ -586,6 +587,9 @@ export default function KvmIdRoute() { const setUsbState = useHidStore(state => state.setUsbState); const setHdmiState = useVideoStore(state => state.setHdmiState); + const keyboardLedState = useHidStore(state => state.keyboardLedState); + const setKeyboardLedState = useHidStore(state => state.setKeyboardLedState); + const [hasUpdated, setHasUpdated] = useState(false); const { navigateTo } = useDeviceUiNavigation(); @@ -607,6 +611,12 @@ export default function KvmIdRoute() { setNetworkState(resp.params as NetworkState); } + if (resp.method === "keyboardLedState") { + const ledState = resp.params as KeyboardLedState; + console.log("Setting keyboard led state", ledState); + setKeyboardLedState(ledState); + } + if (resp.method === "otaState") { const otaState = resp.params as UpdateState["otaState"]; setOtaState(otaState); @@ -643,6 +653,18 @@ export default function KvmIdRoute() { }); }, [rpcDataChannel?.readyState, send, setHdmiState]); + // request keyboard led state from the device + useEffect(() => { + if (rpcDataChannel?.readyState !== "open") return; + if (keyboardLedState !== undefined) return; + console.log("Requesting keyboard led state"); + send("getKeyboardLedState", {}, resp => { + if ("error" in resp) return; + console.log("Keyboard led state", resp.result); + setKeyboardLedState(resp.result as KeyboardLedState); + }); + }, [rpcDataChannel?.readyState, send, setKeyboardLedState, keyboardLedState]); + // When the update is successful, we need to refresh the client javascript and show a success modal useEffect(() => { if (queryParams.get("updateSuccess")) { diff --git a/usb.go b/usb.go index 91674c9..f777f89 100644 --- a/usb.go +++ b/usb.go @@ -24,6 +24,17 @@ func initUsbGadget() { time.Sleep(500 * time.Millisecond) } }() + + gadget.SetOnKeyboardStateChange(func(state usbgadget.KeyboardState) { + if currentSession != nil { + writeJSONRPCEvent("keyboardLedState", state, currentSession) + } + }) + + // open the keyboard hid file to listen for keyboard events + if err := gadget.OpenKeyboardHidFile(); err != nil { + usbLogger.Error().Err(err).Msg("failed to open keyboard hid file") + } } func rpcKeyboardReport(modifier uint8, keys []uint8) error { @@ -42,6 +53,10 @@ func rpcWheelReport(wheelY int8) error { return gadget.AbsMouseWheelReport(wheelY) } +func rpcGetKeyboardLedState() (state usbgadget.KeyboardState) { + return gadget.GetKeyboardState() +} + var usbState = "unknown" func rpcGetUSBState() (state string) { From 019934d33e074701e9197d514bca3349c8ae477d Mon Sep 17 00:00:00 2001 From: adammkelly Date: Thu, 22 May 2025 23:56:50 +0100 Subject: [PATCH 02/11] chore(ui): Allow mac address copying (#504) (#506) --- ui/src/routes/devices.$id.settings.network.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index 16804b5..0905db5 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -228,7 +228,6 @@ export default function SettingsNetworkRoute() { size="SM" value={networkState?.mac_address} error={""} - disabled={true} readOnly={true} className="dark:!text-opacity-60" /> From c1d771cced7e0ca6cbdbe2da0d44b1105f330caf Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Fri, 23 May 2025 00:59:02 +0200 Subject: [PATCH 03/11] feat: allow user to disable keyboard LED synchronization (#507) * feat: allow user to disable keyboard LED synchronization * Update ui/src/hooks/stores.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ui/src/components/InfoBar.tsx | 16 +++++++ ui/src/components/VirtualKeyboard.tsx | 24 +++++++++-- ui/src/components/WebRTCVideo.tsx | 32 ++++++++++++++ ui/src/hooks/stores.ts | 43 ++++++++++++++++++- .../routes/devices.$id.settings.keyboard.tsx | 30 ++++++++++++- ui/src/routes/devices.$id.tsx | 18 +++++++- 6 files changed, 154 insertions(+), 9 deletions(-) diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx index 4ee4149..b865985 100644 --- a/ui/src/components/InfoBar.tsx +++ b/ui/src/components/InfoBar.tsx @@ -37,6 +37,8 @@ export default function InfoBar() { }, [rpcDataChannel]); const keyboardLedState = useHidStore(state => state.keyboardLedState); + const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable); + const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse); @@ -116,6 +118,20 @@ export default function InfoBar() { Relayed by Cloudflare )} + + {keyboardLedStateSyncAvailable ? ( +
+ {keyboardLedSync === "browser" ? "Browser" : "Host"} +
+ ) : null}
state.keyboardLedState?.caps_lock)); + // HID related states + const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable); + const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); + const isKeyboardLedManagedByHost = useMemo(() => + keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable, + [keyboardLedSync, keyboardLedStateSyncAvailable], + ); + + const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive); + const startDrag = useCallback((e: MouseEvent | TouchEvent) => { if (!keyboardRef.current) return; if (e instanceof TouchEvent && e.touches.length > 1) return; @@ -158,11 +168,19 @@ function KeyboardWrapper() { toggleLayout(); if (isCapsLockActive) { + if (!isKeyboardLedManagedByHost) { + setIsCapsLockActive(false); + } sendKeyboardEvent([keys["CapsLock"]], []); return; } } + // Handle caps lock state change + if (isKeyCaps && !isKeyboardLedManagedByHost) { + setIsCapsLockActive(!isCapsLockActive); + } + // Collect new active keys and modifiers const newKeys = keys[cleanKey] ? [keys[cleanKey]] : []; const newModifiers = @@ -178,7 +196,7 @@ function KeyboardWrapper() { setTimeout(resetKeyboardState, 100); }, - [isCapsLockActive, sendKeyboardEvent, resetKeyboardState], + [isCapsLockActive, isKeyboardLedManagedByHost, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive], ); const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled); diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 87f2d4e..ca4db08 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -47,6 +47,18 @@ export default function WebRTCVideo() { clientHeight: videoClientHeight, } = useVideoStore(); + // HID related states + const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable); + const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); + const isKeyboardLedManagedByHost = useMemo(() => + keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable, + [keyboardLedSync, keyboardLedStateSyncAvailable], + ); + + const setIsNumLockActive = useHidStore(state => state.setIsNumLockActive); + const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive); + const setIsScrollLockActive = useHidStore(state => state.setIsScrollLockActive); + // RTC related states const peerConnection = useRTCStore(state => state.peerConnection); @@ -351,6 +363,12 @@ export default function WebRTCVideo() { // console.log(document.activeElement); + if (!isKeyboardLedManagedByHost) { + setIsNumLockActive(e.getModifierState("NumLock")); + setIsCapsLockActive(e.getModifierState("CapsLock")); + setIsScrollLockActive(e.getModifierState("ScrollLock")); + } + if (code == "IntlBackslash" && ["`", "~"].includes(key)) { code = "Backquote"; } else if (code == "Backquote" && ["§", "±"].includes(key)) { @@ -382,6 +400,10 @@ export default function WebRTCVideo() { [ handleModifierKeys, sendKeyboardEvent, + isKeyboardLedManagedByHost, + setIsNumLockActive, + setIsCapsLockActive, + setIsScrollLockActive, ], ); @@ -390,6 +412,12 @@ export default function WebRTCVideo() { e.preventDefault(); const prev = useHidStore.getState(); + if (!isKeyboardLedManagedByHost) { + setIsNumLockActive(e.getModifierState("NumLock")); + setIsCapsLockActive(e.getModifierState("CapsLock")); + setIsScrollLockActive(e.getModifierState("ScrollLock")); + } + // Filtering out the key that was just released (keys[e.code]) const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean); @@ -404,6 +432,10 @@ export default function WebRTCVideo() { [ handleModifierKeys, sendKeyboardEvent, + isKeyboardLedManagedByHost, + setIsNumLockActive, + setIsCapsLockActive, + setIsScrollLockActive, ], ); diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 1754a87..52ef89d 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -283,6 +283,8 @@ export const useVideoStore = create(set => ({ }, })); +export type KeyboardLedSync = "auto" | "browser" | "host"; + interface SettingsState { isCursorHidden: boolean; setCursorVisibility: (enabled: boolean) => void; @@ -305,6 +307,9 @@ interface SettingsState { keyboardLayout: string; setKeyboardLayout: (layout: string) => void; + + keyboardLedSync: KeyboardLedSync; + setKeyboardLedSync: (sync: KeyboardLedSync) => void; } export const useSettingsStore = create( @@ -336,6 +341,9 @@ export const useSettingsStore = create( keyboardLayout: "en-US", setKeyboardLayout: layout => set({ keyboardLayout: layout }), + + keyboardLedSync: "auto", + setKeyboardLedSync: sync => set({ keyboardLedSync: sync }), }), { name: "settings", @@ -411,7 +419,14 @@ export interface KeyboardLedState { scroll_lock: boolean; compose: boolean; kana: boolean; -} +}; +const defaultKeyboardLedState: KeyboardLedState = { + num_lock: false, + caps_lock: false, + scroll_lock: false, + compose: false, + kana: false, +}; export interface HidState { activeKeys: number[]; @@ -433,6 +448,12 @@ export interface HidState { keyboardLedState?: KeyboardLedState; setKeyboardLedState: (state: KeyboardLedState) => void; + setIsNumLockActive: (active: boolean) => void; + setIsCapsLockActive: (active: boolean) => void; + setIsScrollLockActive: (active: boolean) => void; + + keyboardLedStateSyncAvailable: boolean; + setKeyboardLedStateSyncAvailable: (available: boolean) => void; isVirtualKeyboardEnabled: boolean; setVirtualKeyboardEnabled: (enabled: boolean) => void; @@ -444,7 +465,7 @@ export interface HidState { setUsbState: (state: HidState["usbState"]) => void; } -export const useHidStore = create(set => ({ +export const useHidStore = create((set, get) => ({ activeKeys: [], activeModifiers: [], updateActiveKeysAndModifiers: ({ keys, modifiers }) => { @@ -461,6 +482,24 @@ export const useHidStore = create(set => ({ setAltGrCtrlTime: time => set({ altGrCtrlTime: time }), setKeyboardLedState: ledState => set({ keyboardLedState: ledState }), + setIsNumLockActive: active => { + const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) }; + keyboardLedState.num_lock = active; + set({ keyboardLedState }); + }, + setIsCapsLockActive: active => { + const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) }; + keyboardLedState.caps_lock = active; + set({ keyboardLedState }); + }, + setIsScrollLockActive: active => { + const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) }; + keyboardLedState.scroll_lock = active; + set({ keyboardLedState }); + }, + + keyboardLedStateSyncAvailable: false, + setKeyboardLedStateSyncAvailable: available => set({ keyboardLedStateSyncAvailable: available }), isVirtualKeyboardEnabled: false, setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }), diff --git a/ui/src/routes/devices.$id.settings.keyboard.tsx b/ui/src/routes/devices.$id.settings.keyboard.tsx index c311a62..8849e61 100644 --- a/ui/src/routes/devices.$id.settings.keyboard.tsx +++ b/ui/src/routes/devices.$id.settings.keyboard.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect } from "react"; -import { useSettingsStore } from "@/hooks/stores"; +import { KeyboardLedSync, useSettingsStore } from "@/hooks/stores"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import notifications from "@/notifications"; import { SettingsPageHeader } from "@components/SettingsPageheader"; @@ -12,11 +12,20 @@ import { SettingsItem } from "./devices.$id.settings"; export default function SettingsKeyboardRoute() { const keyboardLayout = useSettingsStore(state => state.keyboardLayout); + const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); const setKeyboardLayout = useSettingsStore( state => state.setKeyboardLayout, ); + const setKeyboardLedSync = useSettingsStore( + state => state.setKeyboardLedSync, + ); const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } }) + const ledSyncOptions = [ + { value: "auto", label: "Automatic" }, + { value: "browser", label: "Browser Only" }, + { value: "host", label: "Host Only" }, + ]; const [send] = useJsonRpc(); @@ -47,7 +56,7 @@ export default function SettingsKeyboardRoute() {
@@ -69,6 +78,23 @@ export default function SettingsKeyboardRoute() { Pasting text sends individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in JetKVM matches the settings in the operating system.

+ +
+ { /* this menu item could be renamed to plain "Keyboard layout" in the future, when also the virtual keyboard layout mappings are being implemented */ } + + setKeyboardLedSync(e.target.value as KeyboardLedSync)} + options={ledSyncOptions} + /> + +
); } diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index a6be368..c9aac36 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -590,6 +590,8 @@ export default function KvmIdRoute() { const keyboardLedState = useHidStore(state => state.keyboardLedState); const setKeyboardLedState = useHidStore(state => state.setKeyboardLedState); + const setKeyboardLedStateSyncAvailable = useHidStore(state => state.setKeyboardLedStateSyncAvailable); + const [hasUpdated, setHasUpdated] = useState(false); const { navigateTo } = useDeviceUiNavigation(); @@ -615,6 +617,7 @@ export default function KvmIdRoute() { const ledState = resp.params as KeyboardLedState; console.log("Setting keyboard led state", ledState); setKeyboardLedState(ledState); + setKeyboardLedStateSyncAvailable(true); } if (resp.method === "otaState") { @@ -658,12 +661,23 @@ export default function KvmIdRoute() { if (rpcDataChannel?.readyState !== "open") return; if (keyboardLedState !== undefined) return; console.log("Requesting keyboard led state"); + send("getKeyboardLedState", {}, resp => { - if ("error" in resp) return; + if ("error" in resp) { + // -32601 means the method is not supported + if (resp.error.code === -32601) { + setKeyboardLedStateSyncAvailable(false); + console.error("Failed to get keyboard led state, disabling sync", resp.error); + } else { + console.error("Failed to get keyboard led state", resp.error); + } + return; + } console.log("Keyboard led state", resp.result); setKeyboardLedState(resp.result as KeyboardLedState); + setKeyboardLedStateSyncAvailable(true); }); - }, [rpcDataChannel?.readyState, send, setKeyboardLedState, keyboardLedState]); + }, [rpcDataChannel?.readyState, send, setKeyboardLedState, setKeyboardLedStateSyncAvailable, keyboardLedState]); // When the update is successful, we need to refresh the client javascript and show a success modal useEffect(() => { From 1b5062c504bd8db351dc91fe62ad3bbee40cfaa2 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Fri, 23 May 2025 06:21:53 -0500 Subject: [PATCH 04/11] fix(ui): Default the keyboardLayout to en-US if not set (#512) The recent fix to PasteModal will silently fail a paste if the keyboardLayout hasn't been selected in the settings yet, then when you look in Settings it looks like it's set to Belgian, but it's really just blank. Set it to default to en-US in both these places so it works like it did previously. Fixes #492 --- ui/src/components/popovers/PasteModal.tsx | 42 +++++++++++-------- .../routes/devices.$id.settings.keyboard.tsx | 11 ++++- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/ui/src/components/popovers/PasteModal.tsx b/ui/src/components/popovers/PasteModal.tsx index 26e4b82..70b3067 100644 --- a/ui/src/components/popovers/PasteModal.tsx +++ b/ui/src/components/popovers/PasteModal.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { LuCornerDownLeft } from "react-icons/lu"; import { ExclamationCircleIcon } from "@heroicons/react/16/solid"; import { useClose } from "@headlessui/react"; @@ -39,6 +39,13 @@ export default function PasteModal() { state => state.setKeyboardLayout, ); + // this ensures we always get the original en-US if it hasn't been set yet + const safeKeyboardLayout = useMemo(() => { + if (keyboardLayout && keyboardLayout.length > 0) + return keyboardLayout; + return "en-US"; + }, [keyboardLayout]); + useEffect(() => { send("getKeyboardLayout", {}, resp => { if ("error" in resp) return; @@ -56,29 +63,28 @@ export default function PasteModal() { setPasteMode(false); setDisableVideoFocusTrap(false); if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return; - if (!keyboardLayout) return; - if (!chars[keyboardLayout]) return; - + if (!safeKeyboardLayout) return; + if (!chars[safeKeyboardLayout]) return; const text = TextAreaRef.current.value; try { for (const char of text) { - const { key, shift, altRight, deadKey, accentKey } = chars[keyboardLayout][char] + const { key, shift, altRight, deadKey, accentKey } = chars[safeKeyboardLayout][char] if (!key) continue; - const keyz = [ keys[key] ]; - const modz = [ modifierCode(shift, altRight) ]; + const keyz = [ keys[key] ]; + const modz = [ modifierCode(shift, altRight) ]; - if (deadKey) { + if (deadKey) { keyz.push(keys["Space"]); modz.push(noModifier); - } - if (accentKey) { + } + if (accentKey) { keyz.unshift(keys[accentKey.key]) modz.unshift(modifierCode(accentKey.shift, accentKey.altRight)) - } + } - for (const [index, kei] of keyz.entries()) { + for (const [index, kei] of keyz.entries()) { await new Promise((resolve, reject) => { send( "keyboardReport", @@ -92,13 +98,13 @@ export default function PasteModal() { }, ); }); - } + } } } catch (error) { console.error(error); notifications.error("Failed to paste text"); } - }, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, keyboardLayout]); + }, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, safeKeyboardLayout]); useEffect(() => { if (TextAreaRef.current) { @@ -148,7 +154,7 @@ export default function PasteModal() { // @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments [...new Intl.Segmenter().segment(value)] .map(x => x.segment) - .filter(char => !chars[keyboardLayout][char]), + .filter(char => !chars[safeKeyboardLayout][char]), ), ]; @@ -167,11 +173,11 @@ export default function PasteModal() { )}
-
+

- Sending text using keyboard layout: {layouts[keyboardLayout]} + Sending text using keyboard layout: {layouts[safeKeyboardLayout]}

-
+
diff --git a/ui/src/routes/devices.$id.settings.keyboard.tsx b/ui/src/routes/devices.$id.settings.keyboard.tsx index 8849e61..0f266e9 100644 --- a/ui/src/routes/devices.$id.settings.keyboard.tsx +++ b/ui/src/routes/devices.$id.settings.keyboard.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from "react"; +import { useCallback, useEffect, useMemo } from "react"; import { KeyboardLedSync, useSettingsStore } from "@/hooks/stores"; import { useJsonRpc } from "@/hooks/useJsonRpc"; @@ -20,6 +20,13 @@ export default function SettingsKeyboardRoute() { state => state.setKeyboardLedSync, ); + // this ensures we always get the original en-US if it hasn't been set yet + const safeKeyboardLayout = useMemo(() => { + if (keyboardLayout && keyboardLayout.length > 0) + return keyboardLayout; + return "en-US"; + }, [keyboardLayout]); + const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } }) const ledSyncOptions = [ { value: "auto", label: "Automatic" }, @@ -69,7 +76,7 @@ export default function SettingsKeyboardRoute() { size="SM" label="" fullWidth - value={keyboardLayout} + value={safeKeyboardLayout} onChange={onKeyboardLayoutChange} options={layoutOptions} /> From 7e64a529f89364a732b9f76c44949bbb7d52bf76 Mon Sep 17 00:00:00 2001 From: Alex Goodkind Date: Fri, 23 May 2025 05:38:15 -0700 Subject: [PATCH 05/11] chore: add VSCode extensions for improved development environment (#509) --- .devcontainer/devcontainer.json | 15 ++++++++++++++- .vscode/settings.json | 3 +++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7da2970..aa803f6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,6 +9,19 @@ }, "mounts": [ "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached" - ] + ], + "customizations": { + "vscode": { + "extensions": [ + "bradlc.vscode-tailwindcss", + "GitHub.vscode-pull-request-github", + "dbaeumer.vscode-eslint", + "golang.go", + "ms-vscode.makefile-tools", + "esbenp.prettier-vscode", + "github.vscode-github-actions" + ] + } + } } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..de91a5d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "tailwindCSS.classFunctions": ["cva", "cx"] +} \ No newline at end of file From 2ec061b3a8ba2659f55c6b5717b97d2ca5609e23 Mon Sep 17 00:00:00 2001 From: ariedel87 <127566745+ariedel87@users.noreply.github.com> Date: Sun, 25 May 2025 11:09:48 +0200 Subject: [PATCH 06/11] feat(Keyboard): Hide Pressed Keys (#518) --- ui/src/components/InfoBar.tsx | 29 ++++++++++--------- ui/src/hooks/stores.ts | 6 ++++ .../routes/devices.$id.settings.keyboard.tsx | 17 +++++++++++ 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx index b865985..7ce67a4 100644 --- a/ui/src/components/InfoBar.tsx +++ b/ui/src/components/InfoBar.tsx @@ -28,6 +28,7 @@ export default function InfoBar() { const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); const settings = useSettingsStore(); + const showPressedKeys = useSettingsStore(state => state.showPressedKeys); useEffect(() => { if (!rpcDataChannel) return; @@ -97,19 +98,21 @@ 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(", ")} -

-
+ {showPressedKeys && ( +
+ 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(", ")} +

+
+ )}
diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 52ef89d..1c52572 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -310,6 +310,9 @@ interface SettingsState { keyboardLedSync: KeyboardLedSync; setKeyboardLedSync: (sync: KeyboardLedSync) => void; + + showPressedKeys: boolean; + setShowPressedKeys: (show: boolean) => void; } export const useSettingsStore = create( @@ -344,6 +347,9 @@ export const useSettingsStore = create( keyboardLedSync: "auto", setKeyboardLedSync: sync => set({ keyboardLedSync: sync }), + + showPressedKeys: true, + setShowPressedKeys: show => set({ showPressedKeys: show }), }), { name: "settings", diff --git a/ui/src/routes/devices.$id.settings.keyboard.tsx b/ui/src/routes/devices.$id.settings.keyboard.tsx index 0f266e9..12ed6f2 100644 --- a/ui/src/routes/devices.$id.settings.keyboard.tsx +++ b/ui/src/routes/devices.$id.settings.keyboard.tsx @@ -5,6 +5,7 @@ import { useJsonRpc } from "@/hooks/useJsonRpc"; import notifications from "@/notifications"; import { SettingsPageHeader } from "@components/SettingsPageheader"; import { layouts } from "@/keyboardLayouts"; +import { Checkbox } from "@/components/Checkbox"; import { SelectMenuBasic } from "../components/SelectMenuBasic"; @@ -13,12 +14,16 @@ import { SettingsItem } from "./devices.$id.settings"; export default function SettingsKeyboardRoute() { const keyboardLayout = useSettingsStore(state => state.keyboardLayout); const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); + const showPressedKeys = useSettingsStore(state => state.showPressedKeys); const setKeyboardLayout = useSettingsStore( state => state.setKeyboardLayout, ); const setKeyboardLedSync = useSettingsStore( state => state.setKeyboardLedSync, ); + const setShowPressedKeys = useSettingsStore( + state => state.setShowPressedKeys, + ); // this ensures we always get the original en-US if it hasn't been set yet const safeKeyboardLayout = useMemo(() => { @@ -102,6 +107,18 @@ export default function SettingsKeyboardRoute() { />
+ +
+ + setShowPressedKeys(e.target.checked)} + /> + +
); } From a28676cd94d64aad156dee7c7ba779acdca0d3ec Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Sun, 25 May 2025 11:09:58 +0200 Subject: [PATCH 07/11] feat(websecure): add support for ed25519 certificates (#513) --- internal/websecure/ed25519_test.go | 55 ++++++++++++++++++++++++++++++ internal/websecure/utils.go | 7 +++- 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 internal/websecure/ed25519_test.go diff --git a/internal/websecure/ed25519_test.go b/internal/websecure/ed25519_test.go new file mode 100644 index 0000000..0753be0 --- /dev/null +++ b/internal/websecure/ed25519_test.go @@ -0,0 +1,55 @@ +package websecure + +import ( + "os" + "testing" +) + +var ( + fixtureEd25519Certificate = `-----BEGIN CERTIFICATE----- +MIIBQDCB86ADAgECAhQdB4qB6dV0/u1lwhJofQgkmjjV1zAFBgMrZXAwLzELMAkG +A1UEBhMCREUxIDAeBgNVBAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMB4XDTI1 +MDUyMzEyNTkyN1oXDTI3MDQyMzEyNTkyN1owLzELMAkGA1UEBhMCREUxIDAeBgNV +BAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMCowBQYDK2VwAyEA9tLyoulJn7Ev +bf8kuD1ZGdA092773pCRjFEDKpXHonyjITAfMB0GA1UdDgQWBBRkmrVMfsLY57iy +r/0POP0S4QxCADAFBgMrZXADQQBfTRvqavLHDYQiKQTgbGod+Yn+fIq2lE584+1U +C4wh9peIJDFocLBEAYTQpEMKxa4s0AIRxD+a7aCS5oz0e/0I +-----END CERTIFICATE-----` + + fixtureEd25519PrivateKey = `-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIKV08xUsLRHBfMXqZwxVRzIbViOp8G7aQGjPvoRFjujB +-----END PRIVATE KEY-----` + + certStore *CertStore + certSigner *SelfSigner +) + +func TestMain(m *testing.M) { + tlsStorePath, err := os.MkdirTemp("", "jktls.*") + if err != nil { + defaultLogger.Fatal().Err(err).Msg("failed to create temp directory") + } + + certStore = NewCertStore(tlsStorePath, nil) + certStore.LoadCertificates() + + certSigner = NewSelfSigner( + certStore, + nil, + "ci.jetkvm.com", + "JetKVM", + "JetKVM", + "JetKVM", + ) + + m.Run() + + os.RemoveAll(tlsStorePath) +} + +func TestSaveEd25519Certificate(t *testing.T) { + err, _ := certStore.ValidateAndSaveCertificate("ed25519-test.jetkvm.com", fixtureEd25519Certificate, fixtureEd25519PrivateKey, true) + if err != nil { + t.Fatalf("failed to save certificate: %v", err) + } +} diff --git a/internal/websecure/utils.go b/internal/websecure/utils.go index b0038c0..b333bf9 100644 --- a/internal/websecure/utils.go +++ b/internal/websecure/utils.go @@ -2,6 +2,7 @@ package websecure import ( "crypto/ecdsa" + "crypto/ed25519" "crypto/rand" "crypto/rsa" "crypto/tls" @@ -37,11 +38,15 @@ func keyToFile(cert *tls.Certificate, filename string) error { if e != nil { return fmt.Errorf("failed to marshal EC private key: %v", e) } - keyBlock = pem.Block{ Type: "EC PRIVATE KEY", Bytes: b, } + case ed25519.PrivateKey: + keyBlock = pem.Block{ + Type: "ED25519 PRIVATE KEY", + Bytes: k, + } default: return fmt.Errorf("unknown private key type: %T", k) } From 55d7f22c4749dea0d761a5ea553d0b8bd3cb2360 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Sun, 25 May 2025 07:19:31 -0500 Subject: [PATCH 08/11] chore(ui): Removed unused DeviceSettingState (#496) Now that we don't do any mouse/trackpad sensitivity settings, this whole interface is unused. --- ui/src/hooks/stores.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 1c52572..31bf4ce 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -358,17 +358,6 @@ export const useSettingsStore = create( ), ); -export interface DeviceSettingsState { - trackpadSensitivity: number; - mouseSensitivity: number; - clampMin: number; - clampMax: number; - blockDelay: number; - trackpadThreshold: number; - scrollSensitivity: "low" | "default" | "high"; - setScrollSensitivity: (sensitivity: DeviceSettingsState["scrollSensitivity"]) => void; -} - export interface RemoteVirtualMediaState { source: "WebRTC" | "HTTP" | "Storage" | null; mode: "CDROM" | "Disk" | null; From 1f7c5c94d8c39ae713a31a17645c4f35326ff759 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Sun, 25 May 2025 07:19:42 -0500 Subject: [PATCH 09/11] feat(ui): Add Ctrl+Alt+Del to the action bar (#498) Since this is the sort of thing we do all the time, make it one-click away --- ui/src/components/ActionBar.tsx | 24 ++++++++++++-- ui/src/hooks/stores.ts | 6 ++++ .../devices.$id.settings.ctrlaltdel.tsx | 28 ++++++++++++++++ .../routes/devices.$id.settings.hardware.tsx | 32 ++++++++++++++++++- 4 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 ui/src/routes/devices.$id.settings.ctrlaltdel.tsx diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx index 83ae509..7de4571 100644 --- a/ui/src/components/ActionBar.tsx +++ b/ui/src/components/ActionBar.tsx @@ -1,6 +1,6 @@ import { MdOutlineContentPasteGo } from "react-icons/md"; import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu"; -import { FaKeyboard } from "react-icons/fa6"; +import { FaKeyboard, FaLock} from "react-icons/fa6"; import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; import { Fragment, useCallback, useRef } from "react"; import { CommandLineIcon } from "@heroicons/react/20/solid"; @@ -19,6 +19,8 @@ import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index"; import MountPopopover from "@/components/popovers/MountPopover"; import ExtensionPopover from "@/components/popovers/ExtensionPopover"; import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; +import useKeyboard from "@/hooks/useKeyboard"; +import { keys, modifiers } from "@/keyboardMappings"; export default function Actionbar({ requestFullscreen, @@ -56,6 +58,8 @@ export default function Actionbar({ [setDisableFocusTrap], ); + const { sendKeyboardEvent, resetKeyboardState } = useKeyboard(); + return (
- + {useSettingsStore().actionBarCtrlAltDel && ( +
+
+ )}