Skip to content

Commit 471dfb6

Browse files
williamjohnstoneNevexo
authored andcommitted
Early implementation of different keyboard layouts.
Store is functioning as expected, adding new layouts should be trivial and easily scalable. Implementation is different for each function that uses the keyboard (PasteModal vs Typing in the WebRTC window) these will all require their own testing.
1 parent 79aa296 commit 471dfb6

File tree

12 files changed

+428
-234
lines changed

12 files changed

+428
-234
lines changed

config.go

+3-13
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type Config struct {
2525
GoogleIdentity string `json:"google_identity"`
2626
JigglerEnabled bool `json:"jiggler_enabled"`
2727
AutoUpdateEnabled bool `json:"auto_update_enabled"`
28+
KeyboardLayout string `json:"keyboard_layout"`
2829
IncludePreRelease bool `json:"include_pre_release"`
2930
HashedPassword string `json:"hashed_password"`
3031
LocalAuthToken string `json:"local_auth_token"`
@@ -41,19 +42,8 @@ type Config struct {
4142
const configPath = "/userdata/kvm_config.json"
4243

4344
var defaultConfig = &Config{
44-
CloudURL: "https://api.jetkvm.com",
45-
AutoUpdateEnabled: true, // Set a default value
46-
DisplayMaxBrightness: 64,
47-
DisplayDimAfterSec: 120, // 2 minutes
48-
DisplayOffAfterSec: 1800, // 30 minutes
49-
VirtualMediaEnabled: true,
50-
UsbConfig: UsbConfig{
51-
VendorId: "0x1d6b", //The Linux Foundation
52-
ProductId: "0x0104", //Multifunction Composite Gadget
53-
SerialNumber: "",
54-
Manufacturer: "JetKVM",
55-
Product: "JetKVM USB Emulation Device",
56-
},
45+
CloudURL: "https://api.jetkvm.com",
46+
AutoUpdateEnabled: true, // Set a default value
5747
}
5848

5949
var config *Config

jsonrpc.go

+14
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ func rpcGetDeviceID() (string, error) {
7878
return GetDeviceID(), nil
7979
}
8080

81+
func rpcGetKeyboardLayout() (string, error) {
82+
return config.KeyboardLayout, nil
83+
}
84+
85+
func rpcSetKeyboardLayout(KeyboardLayout string) (string, error) {
86+
config.KeyboardLayout = KeyboardLayout
87+
if err := SaveConfig(); err != nil {
88+
return config.KeyboardLayout, fmt.Errorf("failed to save config: %w", err)
89+
}
90+
return KeyboardLayout, nil
91+
}
92+
8193
var streamFactor = 1.0
8294

8395
func rpcGetStreamQualityFactor() (float64, error) {
@@ -447,6 +459,8 @@ var rpcHandlers = map[string]*jsonrpc.RPCHandler{
447459
"setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}},
448460
"getJigglerState": {Func: rpcGetJigglerState},
449461
"sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}},
462+
"getKeyboardLayout": {Func: rpcGetKeyboardLayout},
463+
"setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"kbLayout"}},
450464
"getStreamQualityFactor": {Func: rpcGetStreamQualityFactor},
451465
"setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}},
452466
"getAutoUpdateState": {Func: rpcGetAutoUpdateState},

ui/src/components/InfoBar.tsx

+13-2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,21 @@ import {
66
useSettingsStore,
77
useVideoStore,
88
} from "@/hooks/stores";
9-
import { useEffect } from "react";
10-
import { keys, modifiers } from "@/keyboardMappings";
9+
import { useEffect, useState } from "react";
10+
import { keyboardMappingsStore } from "@/keyboardMappings/KeyboardMappingStore";
1111

1212
export default function InfoBar() {
13+
const [keys, setKeys] = useState(keyboardMappingsStore.keys);
14+
const [modifiers, setModifiers] = useState(keyboardMappingsStore.modifiers);
15+
16+
useEffect(() => {
17+
const unsubscribeKeyboardStore = keyboardMappingsStore.subscribe(() => {
18+
setKeys(keyboardMappingsStore.keys);
19+
setModifiers(keyboardMappingsStore.modifiers);
20+
});
21+
return unsubscribeKeyboardStore; // Cleanup on unmount
22+
}, []);
23+
1324
const activeKeys = useHidStore(state => state.activeKeys);
1425
const activeModifiers = useHidStore(state => state.activeModifiers);
1526
const mouseX = useMouseStore(state => state.mouseX);

ui/src/components/VirtualKeyboard.tsx

+13-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import "react-simple-keyboard/build/css/index.css";
77
import { useHidStore, useUiStore } from "@/hooks/stores";
88
import { Transition } from "@headlessui/react";
99
import { cx } from "@/cva.config";
10-
import { keys, modifiers } from "@/keyboardMappings";
10+
//import { keys, modifiers } from "@/keyboardMappings/KeyboardMappingStore";
11+
import { keyboardMappingsStore } from "@/keyboardMappings/KeyboardMappingStore";
1112
import useKeyboard from "@/hooks/useKeyboard";
1213
import DetachIconRaw from "@/assets/detach-icon.svg";
1314
import AttachIconRaw from "@/assets/attach-icon.svg";
@@ -21,6 +22,17 @@ const AttachIcon = ({ className }: { className?: string }) => {
2122
};
2223

2324
function KeyboardWrapper() {
25+
const [keys, setKeys] = useState(keyboardMappingsStore.keys);
26+
const [modifiers, setModifiers] = useState(keyboardMappingsStore.modifiers);
27+
28+
useEffect(() => {
29+
const unsubscribeKeyboardStore = keyboardMappingsStore.subscribe(() => {
30+
setKeys(keyboardMappingsStore.keys);
31+
setModifiers(keyboardMappingsStore.modifiers);
32+
});
33+
return unsubscribeKeyboardStore; // Cleanup on unmount
34+
}, []);
35+
2436
const [layoutName, setLayoutName] = useState("default");
2537

2638
const keyboardRef = useRef<HTMLDivElement>(null);

ui/src/components/WebRTCVideo.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
useUiStore,
88
useVideoStore,
99
} from "@/hooks/stores";
10-
import { keys, modifiers } from "@/keyboardMappings";
10+
import { keyboardMappingsStore } from "@/keyboardMappings/KeyboardMappingStore";
1111
import { useResizeObserver } from "@/hooks/useResizeObserver";
1212
import { cx } from "@/cva.config";
1313
import VirtualKeyboard from "@components/VirtualKeyboard";
@@ -18,6 +18,17 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
1818
import { ConnectionErrorOverlay, HDMIErrorOverlay, LoadingOverlay } from "./VideoOverlay";
1919

2020
export default function WebRTCVideo() {
21+
const [keys, setKeys] = useState(keyboardMappingsStore.keys);
22+
const [modifiers, setModifiers] = useState(keyboardMappingsStore.modifiers);
23+
24+
useEffect(() => {
25+
const unsubscribeKeyboardStore = keyboardMappingsStore.subscribe(() => {
26+
setKeys(keyboardMappingsStore.keys);
27+
setModifiers(keyboardMappingsStore.modifiers);
28+
});
29+
return unsubscribeKeyboardStore; // Cleanup on unmount
30+
}, []);
31+
2132
// Video and stream related refs and states
2233
const videoElm = useRef<HTMLVideoElement>(null);
2334
const mediaStream = useRTCStore(state => state.mediaStream);

ui/src/components/popovers/PasteModal.tsx

+21-3
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,26 @@ import { useCallback, useEffect, useRef, useState } from "react";
99
import { LuCornerDownLeft } from "react-icons/lu";
1010
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
1111
import { useClose } from "@headlessui/react";
12-
import { chars, keys, modifiers } from "@/keyboardMappings";
12+
import { keyboardMappingsStore } from "@/keyboardMappings/KeyboardMappingStore";
1313

1414
const hidKeyboardPayload = (keys: number[], modifier: number) => {
1515
return { keys, modifier };
1616
};
1717

1818
export default function PasteModal() {
19+
const [keys, setKeys] = useState(keyboardMappingsStore.keys);
20+
const [chars, setChars] = useState(keyboardMappingsStore.chars);
21+
const [modifiers, setModifiers] = useState(keyboardMappingsStore.modifiers);
22+
23+
useEffect(() => {
24+
const unsubscribeKeyboardStore = keyboardMappingsStore.subscribe(() => {
25+
setKeys(keyboardMappingsStore.keys);
26+
setChars(keyboardMappingsStore.chars);
27+
setModifiers(keyboardMappingsStore.modifiers);
28+
});
29+
return unsubscribeKeyboardStore; // Cleanup on unmount
30+
}, []);
31+
1932
const TextAreaRef = useRef<HTMLTextAreaElement>(null);
2033
const setPasteMode = useHidStore(state => state.setPasteModeEnabled);
2134
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
@@ -41,13 +54,18 @@ export default function PasteModal() {
4154

4255
try {
4356
for (const char of text) {
44-
const { key, shift } = chars[char] ?? {};
57+
const { key, shift, alt } = chars[char] ?? {};
4558
if (!key) continue;
4659

60+
// Build the modifier bitmask
61+
const modifier =
62+
(shift ? modifiers["ShiftLeft"] : 0) |
63+
(alt ? modifiers["AltLeft"] : 0);
64+
4765
await new Promise<void>((resolve, reject) => {
4866
send(
4967
"keyboardReport",
50-
hidKeyboardPayload([keys[key]], shift ? modifiers["ShiftLeft"] : 0),
68+
hidKeyboardPayload([keys[key]], modifier),
5169
params => {
5270
if ("error" in params) return reject(params.error);
5371
send("keyboardReport", hidKeyboardPayload([], 0), params => {

ui/src/components/sidebar/settings.tsx

+49
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { useRevalidator } from "react-router-dom";
2828
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
2929
import PluginList from "@components/PluginList";
3030
import USBConfigDialog from "@components/USBConfigDialog";
31+
import { keyboardMappingsStore } from "@/keyboardMappings/KeyboardMappingStore";
32+
import { KeyboardLayout } from "@/keyboardMappings/KeyboardLayouts";
3133

3234
export function SettingsItem({
3335
title,
@@ -80,6 +82,7 @@ export default function SettingsSidebar() {
8082
const setSidebarView = useUiStore(state => state.setSidebarView);
8183
const settings = useSettingsStore();
8284
const [send] = useJsonRpc();
85+
const [keyboardLayout, setKeyboardLayout] = useState("us");
8386
const [streamQuality, setStreamQuality] = useState("1");
8487
const [autoUpdate, setAutoUpdate] = useState(true);
8588
const [devChannel, setDevChannel] = useState(false);
@@ -150,6 +153,20 @@ export default function SettingsSidebar() {
150153
});
151154
};
152155

156+
const handleKeyboardLayoutChange = (keyboardLayout: string) => {
157+
send("setKeyboardLayout", { kbLayout: keyboardLayout }, resp => {
158+
if ("error" in resp) {
159+
notifications.error(
160+
`Failed to set keyboard layout: ${resp.error.data || "Unknown error"}`,
161+
);
162+
return;
163+
}
164+
// TODO set this to update to the actual layout chosen
165+
keyboardMappingsStore.setLayout(KeyboardLayout.UKApple)
166+
setKeyboardLayout(keyboardLayout);
167+
});
168+
};
169+
153170
const handleStreamQualityChange = (factor: string) => {
154171
send("setStreamQualityFactor", { factor: Number(factor) }, resp => {
155172
if ("error" in resp) {
@@ -300,6 +317,11 @@ export default function SettingsSidebar() {
300317
setDevChannel(resp.result as boolean);
301318
});
302319

320+
send("getKeyboardLayout", {}, resp => {
321+
if ("error" in resp) return;
322+
setKeyboardLayout(String(resp.result));
323+
});
324+
303325
send("getStreamQualityFactor", {}, resp => {
304326
if ("error" in resp) return;
305327
setStreamQuality(String(resp.result));
@@ -556,6 +578,33 @@ export default function SettingsSidebar() {
556578
</div>
557579
</div>
558580
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
581+
<div className="pb-2 space-y-4">
582+
<SectionHeader
583+
title="Keyboard"
584+
description="Customize keyboard behaviour"
585+
/>
586+
<div className="space-y-4">
587+
<SettingsItem
588+
title="Keyboard Layout"
589+
description="Set keyboard layout (this should match the target machine)"
590+
>
591+
<SelectMenuBasic
592+
size="SM"
593+
label=""
594+
// TODO figure out how to make this selector wider like the EDID one?
595+
//fullWidth
596+
value={keyboardLayout}
597+
options={[
598+
{ value: "uk", label: "GB" },
599+
{ value: "uk_apple", label: "GB Apple" },
600+
{ value: "us", label: "US" },
601+
]}
602+
onChange={e => handleKeyboardLayoutChange(e.target.value)}
603+
/>
604+
</SettingsItem>
605+
</div>
606+
</div>
607+
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
559608
<div className="pb-2 space-y-4">
560609
<SectionHeader
561610
title="Video"

0 commit comments

Comments
 (0)