Enable multiple keyboard layouts for paste text from host

This commit is contained in:
Daniel Lorch 2025-05-02 00:52:03 +02:00
parent b4dd4961fc
commit 22849fceab
12 changed files with 406 additions and 121 deletions

View File

@ -87,6 +87,7 @@ type Config struct {
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
KeyboardMacros []KeyboardMacro `json:"keyboard_macros"`
KeyboardLayout string `json:"keyboard_layout"`
EdidString string `json:"hdmi_edid_string"`
ActiveExtension string `json:"active_extension"`
DisplayRotation string `json:"display_rotation"`
@ -109,6 +110,7 @@ var defaultConfig = &Config{
ActiveExtension: "",
KeyboardMacros: []KeyboardMacro{},
DisplayRotation: "270",
KeyboardLayout: "en-US",
DisplayMaxBrightness: 64,
DisplayDimAfterSec: 120, // 2 minutes
DisplayOffAfterSec: 1800, // 30 minutes

View File

@ -1042,6 +1042,8 @@ var rpcHandlers = map[string]RPCHandler{
"setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}},
"setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
"getKeyboardLayout": {Func: rpcGetKeyboardLayout},
"setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}},
"getKeyboardMacros": {Func: getKeyboardMacros},
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
}

View File

@ -8,8 +8,9 @@ import { GridCard } from "@components/Card";
import { TextAreaWithLabel } from "@components/TextArea";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { useHidStore, useRTCStore, useUiStore } from "@/hooks/stores";
import { chars, keys, modifiers } from "@/keyboardMappings";
import { useHidStore, useRTCStore, useUiStore, useDeviceSettingsStore } from "@/hooks/stores";
import { keys, modifiers } from "@/keyboardMappings";
import { layouts, chars } from "@/keyboardLayouts";
import notifications from "@/notifications";
const hidKeyboardPayload = (keys: number[], modifier: number) => {
@ -27,6 +28,11 @@ export default function PasteModal() {
const [invalidChars, setInvalidChars] = useState<string[]>([]);
const close = useClose();
const keyboardLayout = useDeviceSettingsStore(state => state.keyboardLayout);
const setKeyboardLayout = useDeviceSettingsStore(
state => state.setKeyboardLayout,
);
const onCancelPasteMode = useCallback(() => {
setPasteMode(false);
setDisableVideoFocusTrap(false);
@ -42,13 +48,25 @@ export default function PasteModal() {
try {
for (const char of text) {
const { key, shift } = chars[char] ?? {};
const { key, shift, altRight, space, capsLock } = chars[keyboardLayout][char] ?? {};
if (!key) continue;
const keyz = [keys[key]];
if (space) {
keyz.push(keys["Space"]);
}
if (capsLock) {
keyz.unshift(keys["CapsLock"]);
keyz.push(keys["CapsLock"]);
}
const modz = shift ? modifiers["ShiftLeft"] : 0
| (altRight ? modifiers["AltRight"] : 0);
await new Promise<void>((resolve, reject) => {
send(
"keyboardReport",
hidKeyboardPayload([keys[key]], shift ? modifiers["ShiftLeft"] : 0),
hidKeyboardPayload(keyz, modz),
params => {
if ("error" in params) return reject(params.error);
send("keyboardReport", hidKeyboardPayload([], 0), params => {
@ -69,6 +87,11 @@ export default function PasteModal() {
if (TextAreaRef.current) {
TextAreaRef.current.focus();
}
send("getKeyboardLayout", {}, resp => {
if ("error" in resp) return;
setKeyboardLayout(resp.result as string);
});
}, []);
return (
@ -113,7 +136,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[char]),
.filter(char => !chars[keyboardLayout][char]),
),
];
@ -132,6 +155,11 @@ export default function PasteModal() {
)}
</div>
</div>
<div className="space-y-4">
<p className="text-xs text-slate-600 dark:text-slate-400">
Sending key codes for keyboard layout {layouts[keyboardLayout]}
</p>
</div>
</div>
</div>
</div>

View File

@ -347,6 +347,8 @@ export interface DeviceSettingsState {
trackpadThreshold: number;
scrollSensitivity: "low" | "default" | "high";
setScrollSensitivity: (sensitivity: DeviceSettingsState["scrollSensitivity"]) => void;
keyboardLayout: string;
setKeyboardLayout: (layout: string) => void;
}
export interface RemoteVirtualMediaState {
@ -879,4 +881,5 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
set({ loading: false });
}
},
}
}));

12
ui/src/keyboardLayouts.ts Normal file
View File

@ -0,0 +1,12 @@
import { chars as chars_en_US } from "@/keyboardLayouts/en_US"
import { chars as chars_de_CH } from "@/keyboardLayouts/de_CH"
export const layouts = {
"en_US": "English (US)",
"de_CH": "Swiss German"
} as Record<string, string>;
export const chars = {
"en_US": chars_en_US,
"de_CH": chars_de_CH,
} as Record<string, Record<string, { key: string | number; shift?: boolean, altRight?: boolean, space?: boolean, capsLock?: boolean }>>;

View File

@ -0,0 +1,140 @@
export const chars = {
A: { key: "KeyA", shift: true },
B: { key: "KeyB", shift: true },
C: { key: "KeyC", shift: true },
D: { key: "KeyD", shift: true },
E: { key: "KeyE", shift: true },
F: { key: "KeyF", shift: true },
G: { key: "KeyG", shift: true },
H: { key: "KeyH", shift: true },
I: { key: "KeyI", shift: true },
J: { key: "KeyJ", shift: true },
K: { key: "KeyK", shift: true },
L: { key: "KeyL", shift: true },
M: { key: "KeyM", shift: true },
N: { key: "KeyN", shift: true },
O: { key: "KeyO", shift: true },
P: { key: "KeyP", shift: true },
Q: { key: "KeyQ", shift: true },
R: { key: "KeyR", shift: true },
S: { key: "KeyS", shift: true },
T: { key: "KeyT", shift: true },
U: { key: "KeyU", shift: true },
V: { key: "KeyV", shift: true },
W: { key: "KeyW", shift: true },
X: { key: "KeyX", shift: true },
Y: { key: "KeyZ", shift: true },
Z: { key: "KeyY", shift: true },
a: { key: "KeyA" },
"æ": { key: "KeyA", altRight: true },
b: { key: "KeyB" },
c: { key: "KeyC" },
d: { key: "KeyD" },
"ð": { key: "KeyD", altRight: true },
e: { key: "KeyE" },
f: { key: "KeyF" },
"đ": { key: "KeyF", altRight: true },
g: { key: "KeyG" },
"ŋ": { key: "KeyG", altRight: true },
h: { key: "KeyH" },
"ħ": { key: "KeyH", altRight: true },
i: { key: "KeyI" },
"→": { key: "KeyI", altRight: true },
j: { key: "KeyJ" },
k: { key: "KeyK" },
"ĸ": { key: "KeyK", altRight: true },
l: { key: "KeyL" },
"ł": { key: "KeyL", altRight: true },
m: { key: "KeyM" },
"µ": { key: "KeyM", altRight: true },
n: { key: "KeyN" },
o: { key: "KeyO" },
"œ": { key: "KeyO", altRight: true },
p: { key: "KeyP" },
"þ": { key: "KeyP", altRight: true },
q: { key: "KeyQ" },
r: { key: "KeyR" },
"¶": { key: "KeyR", altRight: true },
s: { key: "KeyS" },
"ß": { key: "KeyS", altRight: true },
t: { key: "KeyT" },
"ŧ": { key: "KeyT", altRight: true },
u: { key: "KeyU" },
"↓": { key: "KeyU", altRight: true },
v: { key: "KeyV" },
"„": { key: "KeyV", altRight: true },
w: { key: "KeyW" },
"ſ": { key: "KeyW", altRight: true },
x: { key: "KeyX" },
"»": { key: "KeyX", altRight: true },
y: { key: "KeyZ" },
"←": { key: "KeyZ", altRight: true },
z: { key: "KeyY" },
"«": { key: "KeyY", altRight: true },
"§": { key: "Backquote" },
"°": { key: "Backquote", shift: true },
1: { key: "Digit1" },
"+": { key: "Digit1", shift: true },
"|": { key: "Digit1", altRight: true },
2: { key: "Digit2" },
"\"": { key: "Digit2", shift: true },
"@": { key: "Digit2", altRight: true },
3: { key: "Digit3" },
"*": { key: "Digit3", shift: true },
"#": { key: "Digit3", altRight: true },
4: { key: "Digit4" },
"ç": { key: "Digit4", shift: true },
"¼": { key: "Digit4", altRight: true },
5: { key: "Digit5" },
"%": { key: "Digit5", shift: true },
"½": { key: "Digit5", altRight: true },
6: { key: "Digit6" },
"&": { key: "Digit6", shift: true },
"¬": { key: "Digit6", altRight: true },
7: { key: "Digit7" },
"/": { key: "Digit7", shift: true },
8: { key: "Digit8" },
"(": { key: "Digit8", shift: true },
"¢": { key: "Digit8", altRight: true },
9: { key: "Digit9" },
")": { key: "Digit9", shift: true },
0: { key: "Digit0" },
"=": { key: "Digit0", shift: true },
"'": { key: "Minus" },
"?": { key: "Minus", shift: true },
"^": { key: "Equal", space: true }, // dead key
"`": { key: "Equal", shift: true },
"~": { key: "Equal", altRight: true, space: true }, // dead key
"ü": { key: "BracketLeft" },
"è": { key: "BracketLeft", shift: true },
"[": { key: "BracketLeft", altRight: true },
"Ü": { key: "BracketLeft", capsLock: true },
"!": { key: "BracketRight", shift: true },
"]": { key: "BracketRight", altRight: true },
"ö": { key: "Semicolon" },
"é": { key: "Semicolon", shift: true },
"Ö": { key: "Semicolon", capsLock: true },
"ä": { key: "Quote" },
"à": { key: "Quote", shift: true },
"{": { key: "Quote", altRight: true },
"Ä": { key: "Quote", capsLock: true },
"$": { key: "Backslash" },
"£": { key: "Backslash", shift: true },
"}": { key: "Backslash", altRight: true },
",": { key: "Comma" },
";": { key: "Comma", shift: true },
"•": { key: "Comma", altRight: true },
".": { key: "Period" },
":": { key: "Period", shift: true },
"·": { key: "Period", altRight: true },
"-": { key: "Slash" },
"_": { key: "Slash", shift: true },
"<": { key: "IntlBackslash" },
">": { key: "IntlBackslash", shift: true },
"\\": { key: "IntlBackslash", altRight: true },
"€": { key: "KeyE", altRight: true },
" ": { key: "Space" },
"\n": { key: "Enter" },
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, { key: string | number; shift?: boolean, altRight?: boolean, space?: boolean, capsLock?: boolean }>

View File

@ -0,0 +1,109 @@
export const chars = {
A: { key: "KeyA", shift: true },
B: { key: "KeyB", shift: true },
C: { key: "KeyC", shift: true },
D: { key: "KeyD", shift: true },
E: { key: "KeyE", shift: true },
F: { key: "KeyF", shift: true },
G: { key: "KeyG", shift: true },
H: { key: "KeyH", shift: true },
I: { key: "KeyI", shift: true },
J: { key: "KeyJ", shift: true },
K: { key: "KeyK", shift: true },
L: { key: "KeyL", shift: true },
M: { key: "KeyM", shift: true },
N: { key: "KeyN", shift: true },
O: { key: "KeyO", shift: true },
P: { key: "KeyP", shift: true },
Q: { key: "KeyQ", shift: true },
R: { key: "KeyR", shift: true },
S: { key: "KeyS", shift: true },
T: { key: "KeyT", shift: true },
U: { key: "KeyU", shift: true },
V: { key: "KeyV", shift: true },
W: { key: "KeyW", shift: true },
X: { key: "KeyX", shift: true },
Y: { key: "KeyY", shift: true },
Z: { key: "KeyZ", shift: true },
a: { key: "KeyA", shift: false },
b: { key: "KeyB", shift: false },
c: { key: "KeyC", shift: false },
d: { key: "KeyD", shift: false },
e: { key: "KeyE", shift: false },
f: { key: "KeyF", shift: false },
g: { key: "KeyG", shift: false },
h: { key: "KeyH", shift: false },
i: { key: "KeyI", shift: false },
j: { key: "KeyJ", shift: false },
k: { key: "KeyK", shift: false },
l: { key: "KeyL", shift: false },
m: { key: "KeyM", shift: false },
n: { key: "KeyN", shift: false },
o: { key: "KeyO", shift: false },
p: { key: "KeyP", shift: false },
q: { key: "KeyQ", shift: false },
r: { key: "KeyR", shift: false },
s: { key: "KeyS", shift: false },
t: { key: "KeyT", shift: false },
u: { key: "KeyU", shift: false },
v: { key: "KeyV", shift: false },
w: { key: "KeyW", shift: false },
x: { key: "KeyX", shift: false },
y: { key: "KeyY", shift: false },
z: { key: "KeyZ", shift: false },
1: { key: "Digit1", shift: false },
"!": { key: "Digit1", shift: true },
2: { key: "Digit2", shift: false },
"@": { key: "Digit2", shift: true },
3: { key: "Digit3", shift: false },
"#": { key: "Digit3", shift: true },
4: { key: "Digit4", shift: false },
$: { key: "Digit4", shift: true },
"%": { key: "Digit5", shift: true },
5: { key: "Digit5", shift: false },
"^": { key: "Digit6", shift: true },
6: { key: "Digit6", shift: false },
"&": { key: "Digit7", shift: true },
7: { key: "Digit7", shift: false },
"*": { key: "Digit8", shift: true },
8: { key: "Digit8", shift: false },
"(": { key: "Digit9", shift: true },
9: { key: "Digit9", shift: false },
")": { key: "Digit0", shift: true },
0: { key: "Digit0", shift: false },
"-": { key: "Minus", shift: false },
_: { key: "Minus", shift: true },
"=": { key: "Equal", shift: false },
"+": { key: "Equal", shift: true },
"'": { key: "Quote", shift: false },
'"': { key: "Quote", shift: true },
",": { key: "Comma", shift: false },
"<": { key: "Comma", shift: true },
"/": { key: "Slash", shift: false },
"?": { key: "Slash", shift: true },
".": { key: "Period", shift: false },
">": { key: "Period", shift: true },
";": { key: "Semicolon", shift: false },
":": { key: "Semicolon", shift: true },
"[": { key: "BracketLeft", shift: false },
"{": { key: "BracketLeft", shift: true },
"]": { key: "BracketRight", shift: false },
"}": { key: "BracketRight", shift: true },
"\\": { key: "Backslash", shift: false },
"|": { key: "Backslash", shift: true },
"`": { key: "Backquote", shift: false },
"~": { key: "Backquote", shift: true },
"§": { key: "IntlBackslash", shift: false },
"±": { key: "IntlBackslash", shift: true },
" ": { key: "Space", shift: false },
"\n": { key: "Enter", shift: false },
Enter: { key: "Enter", shift: false },
Tab: { key: "Tab", shift: false },
PrintScreen: { key: "Prt Sc", shift: false },
SystemRequest: { key: "Prt Sc", shift: true },
ScrollLock: { key: "ScrollLock", shift: false},
Pause: { key: "Pause", shift: false },
Break: { key: "Pause", shift: true },
Insert: { key: "Insert", shift: false },
Delete: { key: "Delete", shift: false },
} as Record<string, { key: string | number; shift: boolean }>

View File

@ -43,7 +43,7 @@ export const keys = {
F13: 0x68,
Home: 0x4a,
Insert: 0x49,
IntlBackslash: 0x31,
IntlBackslash: 0x64,
KeyA: 0x04,
KeyB: 0x05,
KeyC: 0x06,
@ -104,116 +104,6 @@ export const keys = {
Tab: 0x2b,
} as Record<string, number>;
export const chars = {
A: { key: "KeyA", shift: true },
B: { key: "KeyB", shift: true },
C: { key: "KeyC", shift: true },
D: { key: "KeyD", shift: true },
E: { key: "KeyE", shift: true },
F: { key: "KeyF", shift: true },
G: { key: "KeyG", shift: true },
H: { key: "KeyH", shift: true },
I: { key: "KeyI", shift: true },
J: { key: "KeyJ", shift: true },
K: { key: "KeyK", shift: true },
L: { key: "KeyL", shift: true },
M: { key: "KeyM", shift: true },
N: { key: "KeyN", shift: true },
O: { key: "KeyO", shift: true },
P: { key: "KeyP", shift: true },
Q: { key: "KeyQ", shift: true },
R: { key: "KeyR", shift: true },
S: { key: "KeyS", shift: true },
T: { key: "KeyT", shift: true },
U: { key: "KeyU", shift: true },
V: { key: "KeyV", shift: true },
W: { key: "KeyW", shift: true },
X: { key: "KeyX", shift: true },
Y: { key: "KeyY", shift: true },
Z: { key: "KeyZ", shift: true },
a: { key: "KeyA", shift: false },
b: { key: "KeyB", shift: false },
c: { key: "KeyC", shift: false },
d: { key: "KeyD", shift: false },
e: { key: "KeyE", shift: false },
f: { key: "KeyF", shift: false },
g: { key: "KeyG", shift: false },
h: { key: "KeyH", shift: false },
i: { key: "KeyI", shift: false },
j: { key: "KeyJ", shift: false },
k: { key: "KeyK", shift: false },
l: { key: "KeyL", shift: false },
m: { key: "KeyM", shift: false },
n: { key: "KeyN", shift: false },
o: { key: "KeyO", shift: false },
p: { key: "KeyP", shift: false },
q: { key: "KeyQ", shift: false },
r: { key: "KeyR", shift: false },
s: { key: "KeyS", shift: false },
t: { key: "KeyT", shift: false },
u: { key: "KeyU", shift: false },
v: { key: "KeyV", shift: false },
w: { key: "KeyW", shift: false },
x: { key: "KeyX", shift: false },
y: { key: "KeyY", shift: false },
z: { key: "KeyZ", shift: false },
1: { key: "Digit1", shift: false },
"!": { key: "Digit1", shift: true },
2: { key: "Digit2", shift: false },
"@": { key: "Digit2", shift: true },
3: { key: "Digit3", shift: false },
"#": { key: "Digit3", shift: true },
4: { key: "Digit4", shift: false },
$: { key: "Digit4", shift: true },
"%": { key: "Digit5", shift: true },
5: { key: "Digit5", shift: false },
"^": { key: "Digit6", shift: true },
6: { key: "Digit6", shift: false },
"&": { key: "Digit7", shift: true },
7: { key: "Digit7", shift: false },
"*": { key: "Digit8", shift: true },
8: { key: "Digit8", shift: false },
"(": { key: "Digit9", shift: true },
9: { key: "Digit9", shift: false },
")": { key: "Digit0", shift: true },
0: { key: "Digit0", shift: false },
"-": { key: "Minus", shift: false },
_: { key: "Minus", shift: true },
"=": { key: "Equal", shift: false },
"+": { key: "Equal", shift: true },
"'": { key: "Quote", shift: false },
'"': { key: "Quote", shift: true },
",": { key: "Comma", shift: false },
"<": { key: "Comma", shift: true },
"/": { key: "Slash", shift: false },
"?": { key: "Slash", shift: true },
".": { key: "Period", shift: false },
">": { key: "Period", shift: true },
";": { key: "Semicolon", shift: false },
":": { key: "Semicolon", shift: true },
"[": { key: "BracketLeft", shift: false },
"{": { key: "BracketLeft", shift: true },
"]": { key: "BracketRight", shift: false },
"}": { key: "BracketRight", shift: true },
"\\": { key: "Backslash", shift: false },
"|": { key: "Backslash", shift: true },
"`": { key: "Backquote", shift: false },
"~": { key: "Backquote", shift: true },
"§": { key: "IntlBackslash", shift: false },
"±": { key: "IntlBackslash", shift: true },
" ": { key: "Space", shift: false },
"\n": { key: "Enter", shift: false },
Enter: { key: "Enter", shift: false },
Tab: { key: "Tab", shift: false },
PrintScreen: { key: "Prt Sc", shift: false },
SystemRequest: { key: "Prt Sc", shift: true },
ScrollLock: { key: "ScrollLock", shift: false},
Pause: { key: "Pause", shift: false },
Break: { key: "Pause", shift: true },
Insert: { key: "Insert", shift: false },
Delete: { key: "Delete", shift: false },
} as Record<string, { key: string | number; shift: boolean }>;
export const modifiers = {
ControlLeft: 0x01,
ControlRight: 0x10,

View File

@ -32,7 +32,8 @@ import { CLOUD_API, DEVICE_API } from "./ui.config";
import OtherSessionRoute from "./routes/devices.$id.other-session";
import MountRoute from "./routes/devices.$id.mount";
import * as SettingsRoute from "./routes/devices.$id.settings";
import SettingsKeyboardMouseRoute from "./routes/devices.$id.settings.mouse";
import SettingsMouseRoute from "./routes/devices.$id.settings.mouse";
import SettingsKeyboardRoute from "./routes/devices.$id.settings.keyboard";
import api from "./api";
import * as SettingsIndexRoute from "./routes/devices.$id.settings._index";
import SettingsAdvancedRoute from "./routes/devices.$id.settings.advanced";
@ -147,7 +148,11 @@ if (isOnDevice) {
},
{
path: "mouse",
element: <SettingsKeyboardMouseRoute />,
element: <SettingsMouseRoute />,
},
{
path: "keyboard",
element: <SettingsKeyboardRoute />,
},
{
path: "advanced",
@ -276,7 +281,11 @@ if (isOnDevice) {
},
{
path: "mouse",
element: <SettingsKeyboardMouseRoute />,
element: <SettingsMouseRoute />,
},
{
path: "keyboard",
element: <SettingsKeyboardRoute />,
},
{
path: "advanced",

View File

@ -0,0 +1,77 @@
import { useCallback, useEffect } from "react";
import { useDeviceSettingsStore } from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { layouts } from "@/keyboardLayouts";
import { FeatureFlag } from "../components/FeatureFlag";
import { SelectMenuBasic } from "../components/SelectMenuBasic";
import { SettingsItem } from "./devices.$id.settings";
export default function SettingsKeyboardRoute() {
const keyboardLayout = useDeviceSettingsStore(state => state.keyboardLayout);
const setKeyboardLayout = useDeviceSettingsStore(
state => state.setKeyboardLayout,
);
const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } })
const [send] = useJsonRpc();
useEffect(() => {
send("getKeyboardLayout", {}, resp => {
if ("error" in resp) return;
setKeyboardLayout(resp.result as string);
});
}, []);
const onKeyboardLayoutChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const layout = e.target.value;
send("setKeyboardLayout", { layout }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to set keyboard layout: ${resp.error.data || "Unknown error"}`,
);
}
notifications.success("Keyboard layout set successfully");
setKeyboardLayout(layout);
});
},
[send, setKeyboardLayout],
);
return (
<div className="space-y-4">
<SettingsPageHeader
title="Keyboard"
description="Configure keyboard layout settings for your device"
/>
<div className="space-y-4">
<FeatureFlag minAppVersion="0.4.0" name="Paste text">
{ /* 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="Paste text"
description="Keyboard layout of target operating system"
>
<SelectMenuBasic
size="SM"
label=""
fullWidth
value={keyboardLayout}
onChange={onKeyboardLayoutChange}
options={layoutOptions}
/>
</SettingsItem>
<p className="text-xs text-slate-600 dark:text-slate-400">
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>
</FeatureFlag>
</div>
</div>
);
}

View File

@ -15,7 +15,7 @@ import { cx } from "../cva.config";
import { SettingsItem } from "./devices.$id.settings";
export default function SettingsKeyboardMouseRoute() {
export default function SettingsMouseRoute() {
const hideCursor = useSettingsStore(state => state.isCursorHidden);
const setHideCursor = useSettingsStore(state => state.setCursorVisibility);

View File

@ -1,6 +1,7 @@
import { NavLink, Outlet, useLocation } from "react-router-dom";
import {
LuSettings,
LuMouse,
LuKeyboard,
LuVideo,
LuCpu,
@ -149,11 +150,23 @@ export default function SettingsRoute() {
className={({ isActive }) => (isActive ? "active" : "")}
>
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
<LuKeyboard className="h-4 w-4 shrink-0" />
<LuMouse className="h-4 w-4 shrink-0" />
<h1>Mouse</h1>
</div>
</NavLink>
</div>
<div className="shrink-0">
<NavLink
to="keyboard"
className={({ isActive }) => (isActive ? "active" : "")}
>
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent">
<LuKeyboard className="h-4 w-4 shrink-0" />
<h1>Keyboard</h1>
</div>
</NavLink>
</div>
<div className="shrink-0">
<NavLink
to="video"