Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions ui/src/components/InfoBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -116,6 +118,20 @@ export default function InfoBar() {
Relayed by Cloudflare
</div>
)}

{keyboardLedStateSyncAvailable ? (
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
keyboardLedSync !== "browser"
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
title={"Your keyboard LED state is managed by" + (keyboardLedSync === "browser" ? " the browser" : " the host")}
>
{keyboardLedSync === "browser" ? "Browser" : "Host"}
</div>
) : null}
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
Expand Down
24 changes: 21 additions & 3 deletions ui/src/components/VirtualKeyboard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useShallow } from "zustand/react/shallow";
import { ChevronDownIcon } from "@heroicons/react/16/solid";
import { AnimatePresence, motion } from "framer-motion";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import Keyboard from "react-simple-keyboard";

import Card from "@components/Card";
Expand All @@ -13,7 +13,7 @@ import "react-simple-keyboard/build/css/index.css";
import AttachIconRaw from "@/assets/attach-icon.svg";
import DetachIconRaw from "@/assets/detach-icon.svg";
import { cx } from "@/cva.config";
import { useHidStore, useUiStore } from "@/hooks/stores";
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
import useKeyboard from "@/hooks/useKeyboard";
import { keyDisplayMap, keys, modifiers } from "@/keyboardMappings";

Expand Down Expand Up @@ -44,6 +44,16 @@ function KeyboardWrapper() {

const isCapsLockActive = useHidStore(useShallow(state => state.keyboardLedState?.caps_lock));

// HID related states
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
const isKeyboardLedManagedByHost = useMemo(() =>
keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable,
[keyboardLedSync, keyboardLedStateSyncAvailable],
);

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;
Expand Down Expand Up @@ -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 =
Expand All @@ -178,7 +196,7 @@ function KeyboardWrapper() {

setTimeout(resetKeyboardState, 100);
},
[isCapsLockActive, sendKeyboardEvent, resetKeyboardState],
[isCapsLockActive, isKeyboardLedManagedByHost, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive],
);

const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
Expand Down
32 changes: 32 additions & 0 deletions ui/src/components/WebRTCVideo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -382,6 +400,10 @@ export default function WebRTCVideo() {
[
handleModifierKeys,
sendKeyboardEvent,
isKeyboardLedManagedByHost,
setIsNumLockActive,
setIsCapsLockActive,
setIsScrollLockActive,
],
);

Expand All @@ -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);

Expand All @@ -404,6 +432,10 @@ export default function WebRTCVideo() {
[
handleModifierKeys,
sendKeyboardEvent,
isKeyboardLedManagedByHost,
setIsNumLockActive,
setIsCapsLockActive,
setIsScrollLockActive,
],
);

Expand Down
43 changes: 41 additions & 2 deletions ui/src/hooks/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ export const useVideoStore = create<VideoState>(set => ({
},
}));

export type KeyboardLedSync = "auto" | "browser" | "host";

interface SettingsState {
isCursorHidden: boolean;
setCursorVisibility: (enabled: boolean) => void;
Expand All @@ -305,6 +307,9 @@ interface SettingsState {

keyboardLayout: string;
setKeyboardLayout: (layout: string) => void;

keyboardLedSync: KeyboardLedSync;
setKeyboardLedSync: (sync: KeyboardLedSync) => void;
}

export const useSettingsStore = create(
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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[];
Expand All @@ -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;
Expand All @@ -444,7 +465,7 @@ export interface HidState {
setUsbState: (state: HidState["usbState"]) => void;
}

export const useHidStore = create<HidState>(set => ({
export const useHidStore = create<HidState>((set, get) => ({
activeKeys: [],
activeModifiers: [],
updateActiveKeysAndModifiers: ({ keys, modifiers }) => {
Expand All @@ -461,6 +482,24 @@ export const useHidStore = create<HidState>(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 }),
Expand Down
30 changes: 28 additions & 2 deletions ui/src/routes/devices.$id.settings.keyboard.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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();

Expand Down Expand Up @@ -47,7 +56,7 @@ export default function SettingsKeyboardRoute() {
<div className="space-y-4">
<SettingsPageHeader
title="Keyboard"
description="Configure keyboard layout settings for your device"
description="Configure keyboard settings for your device"
/>

<div className="space-y-4">
Expand All @@ -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.
</p>
</div>

<div className="space-y-4">
{ /* this menu item could be renamed to plain "Keyboard layout" in the future, when also the virtual keyboard layout mappings are being implemented */ }
<SettingsItem
title="LED state synchronization"
description="Synchronize the LED state of the keyboard with the target device"
>
<SelectMenuBasic
size="SM"
label=""
fullWidth
value={keyboardLedSync}
onChange={e => setKeyboardLedSync(e.target.value as KeyboardLedSync)}
options={ledSyncOptions}
/>
</SettingsItem>
</div>
</div>
);
}
18 changes: 16 additions & 2 deletions ui/src/routes/devices.$id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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") {
Expand Down Expand Up @@ -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(() => {
Expand Down
Loading