From c3087abe02c3dca5af6a87eabf7096442e0dcb90 Mon Sep 17 00:00:00 2001 From: Daniel Lorch Date: Fri, 2 May 2025 00:52:03 +0200 Subject: [PATCH] Enable multiple keyboard layouts for paste text from host --- config.go | 2 + jsonrpc.go | 14 ++ ui/src/components/popovers/PasteModal.tsx | 38 ++++- ui/src/hooks/stores.ts | 7 +- ui/src/keyboardLayouts.ts | 12 ++ ui/src/keyboardLayouts/de_CH.ts | 140 ++++++++++++++++++ ui/src/keyboardLayouts/en_US.ts | 102 +++++++++++++ ui/src/keyboardMappings.ts | 105 +------------ ui/src/main.tsx | 15 +- .../routes/devices.$id.settings.keyboard.tsx | 77 ++++++++++ ui/src/routes/devices.$id.settings.mouse.tsx | 2 +- ui/src/routes/devices.$id.settings.tsx | 14 +- 12 files changed, 413 insertions(+), 115 deletions(-) create mode 100644 ui/src/keyboardLayouts.ts create mode 100644 ui/src/keyboardLayouts/de_CH.ts create mode 100644 ui/src/keyboardLayouts/en_US.ts create mode 100644 ui/src/routes/devices.$id.settings.keyboard.tsx diff --git a/config.go b/config.go index 23d4c84..f8daef5 100644 --- a/config.go +++ b/config.go @@ -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"` DisplayMaxBrightness int `json:"display_max_brightness"` @@ -107,6 +108,7 @@ var defaultConfig = &Config{ AutoUpdateEnabled: true, // Set a default value ActiveExtension: "", KeyboardMacros: []KeyboardMacro{}, + KeyboardLayout: "en-US", DisplayMaxBrightness: 64, DisplayDimAfterSec: 120, // 2 minutes DisplayOffAfterSec: 1800, // 30 minutes diff --git a/jsonrpc.go b/jsonrpc.go index d35f635..13280d9 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -863,6 +863,18 @@ func rpcSetScrollSensitivity(sensitivity string) error { return nil } +func rpcGetKeyboardLayout() (string, error) { + return config.KeyboardLayout, nil +} + +func rpcSetKeyboardLayout(layout string) error { + config.KeyboardLayout = layout + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + return nil +} + func getKeyboardMacros() (interface{}, error) { macros := make([]KeyboardMacro, len(config.KeyboardMacros)) copy(macros, config.KeyboardMacros) @@ -1028,6 +1040,8 @@ var rpcHandlers = map[string]RPCHandler{ "setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}}, "getScrollSensitivity": {Func: rpcGetScrollSensitivity}, "setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}}, + "getKeyboardLayout": {Func: rpcGetKeyboardLayout}, + "setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}}, "getKeyboardMacros": {Func: getKeyboardMacros}, "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, } diff --git a/ui/src/components/popovers/PasteModal.tsx b/ui/src/components/popovers/PasteModal.tsx index 643f55b..9e3d9ee 100644 --- a/ui/src/components/popovers/PasteModal.tsx +++ b/ui/src/components/popovers/PasteModal.tsx @@ -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([]); 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((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() { )} +
+

+ Sending key codes for keyboard layout {layouts[keyboardLayout]} +

+
diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index db1fd04..dc97ccf 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -336,6 +336,8 @@ export interface DeviceSettingsState { trackpadThreshold: number; scrollSensitivity: "low" | "default" | "high"; setScrollSensitivity: (sensitivity: DeviceSettingsState["scrollSensitivity"]) => void; + keyboardLayout: string; + setKeyboardLayout: (layout: string) => void; } export const useDeviceSettingsStore = create(set => ({ @@ -397,6 +399,9 @@ export const useDeviceSettingsStore = create(set => ({ scrollSensitivity: sensitivity, }); }, + + keyboardLayout: "en-US", + setKeyboardLayout: layout => set({ keyboardLayout: layout }), })); export interface RemoteVirtualMediaState { @@ -893,4 +898,4 @@ export const useMacrosStore = create((set, get) => ({ set({ loading: false }); } } -})); \ No newline at end of file +})); diff --git a/ui/src/keyboardLayouts.ts b/ui/src/keyboardLayouts.ts new file mode 100644 index 0000000..15c3be4 --- /dev/null +++ b/ui/src/keyboardLayouts.ts @@ -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; + +export const chars = { + "en_US": chars_en_US, + "de_CH": chars_de_CH, +} as Record>; diff --git a/ui/src/keyboardLayouts/de_CH.ts b/ui/src/keyboardLayouts/de_CH.ts new file mode 100644 index 0000000..405feb8 --- /dev/null +++ b/ui/src/keyboardLayouts/de_CH.ts @@ -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 diff --git a/ui/src/keyboardLayouts/en_US.ts b/ui/src/keyboardLayouts/en_US.ts new file mode 100644 index 0000000..57c9eef --- /dev/null +++ b/ui/src/keyboardLayouts/en_US.ts @@ -0,0 +1,102 @@ +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 }, +} as Record diff --git a/ui/src/keyboardMappings.ts b/ui/src/keyboardMappings.ts index 347939a..c98efeb 100644 --- a/ui/src/keyboardMappings.ts +++ b/ui/src/keyboardMappings.ts @@ -43,7 +43,7 @@ export const keys = { F13: 0x68, Home: 0x4a, Insert: 0x49, - IntlBackslash: 0x31, + IntlBackslash: 0x64, KeyA: 0x04, KeyB: 0x05, KeyC: 0x06, @@ -99,109 +99,6 @@ export const keys = { Tab: 0x2b, } as Record; -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 }, -} as Record; - export const modifiers = { ControlLeft: 0x01, ControlRight: 0x10, diff --git a/ui/src/main.tsx b/ui/src/main.tsx index f4bdd34..18ae323 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -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: , + element: , + }, + { + path: "keyboard", + element: , }, { path: "advanced", @@ -276,7 +281,11 @@ if (isOnDevice) { }, { path: "mouse", - element: , + element: , + }, + { + path: "keyboard", + element: , }, { path: "advanced", diff --git a/ui/src/routes/devices.$id.settings.keyboard.tsx b/ui/src/routes/devices.$id.settings.keyboard.tsx new file mode 100644 index 0000000..b4981af --- /dev/null +++ b/ui/src/routes/devices.$id.settings.keyboard.tsx @@ -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) => { + 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 ( +
+ + +
+ + { /* this menu item could be renamed to plain "Keyboard layout" in the future, when also the virtual keyboard layout mappings are being implemented */ } + + + +

+ 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. +

+
+
+
+ ); +} diff --git a/ui/src/routes/devices.$id.settings.mouse.tsx b/ui/src/routes/devices.$id.settings.mouse.tsx index d6223d0..14cc890 100644 --- a/ui/src/routes/devices.$id.settings.mouse.tsx +++ b/ui/src/routes/devices.$id.settings.mouse.tsx @@ -18,7 +18,7 @@ import { SettingsItem } from "./devices.$id.settings"; type ScrollSensitivity = "low" | "default" | "high"; -export default function SettingsKeyboardMouseRoute() { +export default function SettingsMouseRoute() { const hideCursor = useSettingsStore(state => state.isCursorHidden); const setHideCursor = useSettingsStore(state => state.setCursorVisibility); diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx index f8e5262..182f873 100644 --- a/ui/src/routes/devices.$id.settings.tsx +++ b/ui/src/routes/devices.$id.settings.tsx @@ -1,6 +1,7 @@ import { NavLink, Outlet, useLocation } from "react-router-dom"; import { LuSettings, + LuMouse, LuKeyboard, LuVideo, LuCpu, @@ -148,11 +149,22 @@ export default function SettingsRoute() { className={({ isActive }) => (isActive ? "active" : "")} >
- +

Mouse

+
+ (isActive ? "active" : "")} + > +
+ +

Keyboard

+
+
+