From 0e855adc351d9df0f7f488c46ff9e40d5334757d Mon Sep 17 00:00:00 2001 From: William Johnstone Date: Wed, 29 Jan 2025 01:52:13 +0000 Subject: [PATCH 1/9] 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. --- config.go | 2 + jsonrpc.go | 14 ++ ui/src/components/InfoBar.tsx | 15 +- ui/src/components/VirtualKeyboard.tsx | 14 +- ui/src/components/WebRTCVideo.tsx | 13 +- ui/src/components/popovers/PasteModal.tsx | 24 +- ui/src/components/sidebar/settings.tsx | 49 ++++ ui/src/keyboardMappings.ts | 214 ----------------- ui/src/keyboardMappings/KeyboardLayouts.ts | 25 ++ .../keyboardMappings/KeyboardMappingStore.ts | 39 ++++ ui/src/keyboardMappings/layouts/uk_apple.ts | 24 ++ ui/src/keyboardMappings/layouts/us.ts | 215 ++++++++++++++++++ 12 files changed, 427 insertions(+), 221 deletions(-) delete mode 100644 ui/src/keyboardMappings.ts create mode 100644 ui/src/keyboardMappings/KeyboardLayouts.ts create mode 100644 ui/src/keyboardMappings/KeyboardMappingStore.ts create mode 100644 ui/src/keyboardMappings/layouts/uk_apple.ts create mode 100644 ui/src/keyboardMappings/layouts/us.ts diff --git a/config.go b/config.go index 1636434a..3ae40668 100644 --- a/config.go +++ b/config.go @@ -17,6 +17,7 @@ type Config struct { GoogleIdentity string `json:"google_identity"` JigglerEnabled bool `json:"jiggler_enabled"` AutoUpdateEnabled bool `json:"auto_update_enabled"` + KeyboardLayout string `json:"keyboard_layout"` IncludePreRelease bool `json:"include_pre_release"` HashedPassword string `json:"hashed_password"` LocalAuthToken string `json:"local_auth_token"` @@ -29,6 +30,7 @@ const configPath = "/userdata/kvm_config.json" var defaultConfig = &Config{ CloudURL: "https://api.jetkvm.com", AutoUpdateEnabled: true, // Set a default value + KeyboardLayout: "us", } var config *Config diff --git a/jsonrpc.go b/jsonrpc.go index 2ce5f189..f7543bb5 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -131,6 +131,18 @@ func rpcGetDeviceID() (string, error) { return GetDeviceID(), nil } +func rpcGetKeyboardLayout() (string, error) { + return config.KeyboardLayout, nil +} + +func rpcSetKeyboardLayout(KeyboardLayout string) (string, error) { + config.KeyboardLayout = KeyboardLayout + if err := SaveConfig(); err != nil { + return config.KeyboardLayout, fmt.Errorf("failed to save config: %w", err) + } + return KeyboardLayout, nil +} + var streamFactor = 1.0 func rpcGetStreamQualityFactor() (float64, error) { @@ -523,6 +535,8 @@ var rpcHandlers = map[string]RPCHandler{ "setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}}, "getJigglerState": {Func: rpcGetJigglerState}, "sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}}, + "getKeyboardLayout": {Func: rpcGetKeyboardLayout}, + "setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"kbLayout"}}, "getStreamQualityFactor": {Func: rpcGetStreamQualityFactor}, "setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}}, "getAutoUpdateState": {Func: rpcGetAutoUpdateState}, diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx index be940434..4490afe9 100644 --- a/ui/src/components/InfoBar.tsx +++ b/ui/src/components/InfoBar.tsx @@ -6,10 +6,21 @@ import { useSettingsStore, useVideoStore, } from "@/hooks/stores"; -import { useEffect } from "react"; -import { keys, modifiers } from "@/keyboardMappings"; +import { useEffect, useState } from "react"; +import { keyboardMappingsStore } from "@/keyboardMappings/KeyboardMappingStore"; export default function InfoBar() { + const [keys, setKeys] = useState(keyboardMappingsStore.keys); + const [modifiers, setModifiers] = useState(keyboardMappingsStore.modifiers); + + useEffect(() => { + const unsubscribeKeyboardStore = keyboardMappingsStore.subscribe(() => { + setKeys(keyboardMappingsStore.keys); + setModifiers(keyboardMappingsStore.modifiers); + }); + return unsubscribeKeyboardStore; // Cleanup on unmount + }, []); + const activeKeys = useHidStore(state => state.activeKeys); const activeModifiers = useHidStore(state => state.activeModifiers); const mouseX = useMouseStore(state => state.mouseX); diff --git a/ui/src/components/VirtualKeyboard.tsx b/ui/src/components/VirtualKeyboard.tsx index e3858c00..f056c11c 100644 --- a/ui/src/components/VirtualKeyboard.tsx +++ b/ui/src/components/VirtualKeyboard.tsx @@ -7,7 +7,8 @@ import "react-simple-keyboard/build/css/index.css"; import { useHidStore, useUiStore } from "@/hooks/stores"; import { Transition } from "@headlessui/react"; import { cx } from "@/cva.config"; -import { keys, modifiers } from "@/keyboardMappings"; +//import { keys, modifiers } from "@/keyboardMappings/KeyboardMappingStore"; +import { keyboardMappingsStore } from "@/keyboardMappings/KeyboardMappingStore"; import useKeyboard from "@/hooks/useKeyboard"; import DetachIconRaw from "@/assets/detach-icon.svg"; import AttachIconRaw from "@/assets/attach-icon.svg"; @@ -21,6 +22,17 @@ const AttachIcon = ({ className }: { className?: string }) => { }; function KeyboardWrapper() { + const [keys, setKeys] = useState(keyboardMappingsStore.keys); + const [modifiers, setModifiers] = useState(keyboardMappingsStore.modifiers); + + useEffect(() => { + const unsubscribeKeyboardStore = keyboardMappingsStore.subscribe(() => { + setKeys(keyboardMappingsStore.keys); + setModifiers(keyboardMappingsStore.modifiers); + }); + return unsubscribeKeyboardStore; // Cleanup on unmount + }, []); + const [layoutName, setLayoutName] = useState("default"); const keyboardRef = useRef(null); diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index f5f083bb..8e6b867c 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -7,7 +7,7 @@ import { useUiStore, useVideoStore, } from "@/hooks/stores"; -import { keys, modifiers } from "@/keyboardMappings"; +import { keyboardMappingsStore } from "@/keyboardMappings/KeyboardMappingStore"; import { useResizeObserver } from "@/hooks/useResizeObserver"; import { cx } from "@/cva.config"; import VirtualKeyboard from "@components/VirtualKeyboard"; @@ -18,6 +18,17 @@ import { useJsonRpc } from "@/hooks/useJsonRpc"; import { ConnectionErrorOverlay, HDMIErrorOverlay, LoadingOverlay } from "./VideoOverlay"; export default function WebRTCVideo() { + const [keys, setKeys] = useState(keyboardMappingsStore.keys); + const [modifiers, setModifiers] = useState(keyboardMappingsStore.modifiers); + + useEffect(() => { + const unsubscribeKeyboardStore = keyboardMappingsStore.subscribe(() => { + setKeys(keyboardMappingsStore.keys); + setModifiers(keyboardMappingsStore.modifiers); + }); + return unsubscribeKeyboardStore; // Cleanup on unmount + }, []); + // Video and stream related refs and states const videoElm = useRef(null); const mediaStream = useRTCStore(state => state.mediaStream); diff --git a/ui/src/components/popovers/PasteModal.tsx b/ui/src/components/popovers/PasteModal.tsx index 661c48dd..6b3878f5 100644 --- a/ui/src/components/popovers/PasteModal.tsx +++ b/ui/src/components/popovers/PasteModal.tsx @@ -9,13 +9,26 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { LuCornerDownLeft } from "react-icons/lu"; import { ExclamationCircleIcon } from "@heroicons/react/16/solid"; import { useClose } from "@headlessui/react"; -import { chars, keys, modifiers } from "@/keyboardMappings"; +import { keyboardMappingsStore } from "@/keyboardMappings/KeyboardMappingStore"; const hidKeyboardPayload = (keys: number[], modifier: number) => { return { keys, modifier }; }; export default function PasteModal() { + const [keys, setKeys] = useState(keyboardMappingsStore.keys); + const [chars, setChars] = useState(keyboardMappingsStore.chars); + const [modifiers, setModifiers] = useState(keyboardMappingsStore.modifiers); + + useEffect(() => { + const unsubscribeKeyboardStore = keyboardMappingsStore.subscribe(() => { + setKeys(keyboardMappingsStore.keys); + setChars(keyboardMappingsStore.chars); + setModifiers(keyboardMappingsStore.modifiers); + }); + return unsubscribeKeyboardStore; // Cleanup on unmount + }, []); + const TextAreaRef = useRef(null); const setPasteMode = useHidStore(state => state.setPasteModeEnabled); const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); @@ -41,13 +54,18 @@ export default function PasteModal() { try { for (const char of text) { - const { key, shift } = chars[char] ?? {}; + const { key, shift, alt } = chars[char] ?? {}; if (!key) continue; + // Build the modifier bitmask + const modifier = + (shift ? modifiers["ShiftLeft"] : 0) | + (alt ? modifiers["AltLeft"] : 0); + await new Promise((resolve, reject) => { send( "keyboardReport", - hidKeyboardPayload([keys[key]], shift ? modifiers["ShiftLeft"] : 0), + hidKeyboardPayload([keys[key]], modifier), params => { if ("error" in params) return reject(params.error); send("keyboardReport", hidKeyboardPayload([], 0), params => { diff --git a/ui/src/components/sidebar/settings.tsx b/ui/src/components/sidebar/settings.tsx index ec606a67..f7bd99a1 100644 --- a/ui/src/components/sidebar/settings.tsx +++ b/ui/src/components/sidebar/settings.tsx @@ -25,6 +25,8 @@ import LocalAuthPasswordDialog from "@/components/LocalAuthPasswordDialog"; import { LocalDevice } from "@routes/devices.$id"; import { useRevalidator } from "react-router-dom"; import { ShieldCheckIcon } from "@heroicons/react/20/solid"; +import { keyboardMappingsStore } from "@/keyboardMappings/KeyboardMappingStore"; +import { KeyboardLayout } from "@/keyboardMappings/KeyboardLayouts"; export function SettingsItem({ title, @@ -77,6 +79,7 @@ export default function SettingsSidebar() { const setSidebarView = useUiStore(state => state.setSidebarView); const settings = useSettingsStore(); const [send] = useJsonRpc(); + const [keyboardLayout, setKeyboardLayout] = useState("us"); const [streamQuality, setStreamQuality] = useState("1"); const [autoUpdate, setAutoUpdate] = useState(true); const [devChannel, setDevChannel] = useState(false); @@ -146,6 +149,20 @@ export default function SettingsSidebar() { }); }; + const handleKeyboardLayoutChange = (keyboardLayout: string) => { + send("setKeyboardLayout", { kbLayout: keyboardLayout }, resp => { + if ("error" in resp) { + notifications.error( + `Failed to set keyboard layout: ${resp.error.data || "Unknown error"}`, + ); + return; + } + // TODO set this to update to the actual layout chosen + keyboardMappingsStore.setLayout(KeyboardLayout.UKApple) + setKeyboardLayout(keyboardLayout); + }); + }; + const handleStreamQualityChange = (factor: string) => { send("setStreamQualityFactor", { factor: Number(factor) }, resp => { if ("error" in resp) { @@ -274,6 +291,11 @@ export default function SettingsSidebar() { setDevChannel(resp.result as boolean); }); + send("getKeyboardLayout", {}, resp => { + if ("error" in resp) return; + setKeyboardLayout(String(resp.result)); + }); + send("getStreamQualityFactor", {}, resp => { if ("error" in resp) return; setStreamQuality(String(resp.result)); @@ -509,6 +531,33 @@ export default function SettingsSidebar() {
+
+ +
+ + handleKeyboardLayoutChange(e.target.value)} + /> + +
+
+
; - -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, - ShiftLeft: 0x02, - ShiftRight: 0x20, - AltLeft: 0x04, - AltRight: 0x40, - MetaLeft: 0x08, - MetaRight: 0x80, -} as Record; diff --git a/ui/src/keyboardMappings/KeyboardLayouts.ts b/ui/src/keyboardMappings/KeyboardLayouts.ts new file mode 100644 index 00000000..baadeab4 --- /dev/null +++ b/ui/src/keyboardMappings/KeyboardLayouts.ts @@ -0,0 +1,25 @@ +import {keysUKApple, charsUKApple, modifiersUKApple } from './layouts/uk_apple'; +import {keysUS, charsUS, modifiersUS } from './layouts/us'; + +export enum KeyboardLayout { + US = "us", + UKApple = "uk_apple", + } + +export function getKeyboardMappings(layout: KeyboardLayout) { + switch (layout) { + case KeyboardLayout.UKApple: + return { + keys: keysUKApple, + chars: charsUKApple, + modifiers: modifiersUKApple, + }; + case KeyboardLayout.US: + default: + return { + keys: keysUS, + chars: charsUS, + modifiers: modifiersUS, + }; + } + } \ No newline at end of file diff --git a/ui/src/keyboardMappings/KeyboardMappingStore.ts b/ui/src/keyboardMappings/KeyboardMappingStore.ts new file mode 100644 index 00000000..2d41bc1f --- /dev/null +++ b/ui/src/keyboardMappings/KeyboardMappingStore.ts @@ -0,0 +1,39 @@ +import { getKeyboardMappings, KeyboardLayout } from "@/keyboardMappings/KeyboardLayouts"; + +// TODO Move this in with all the other stores? + +class KeyboardMappingsStore { + private _layout: KeyboardLayout = KeyboardLayout.US; + private _subscribers: (() => void)[] = []; + + public keys = getKeyboardMappings(this._layout).keys; + public chars = getKeyboardMappings(this._layout).chars; + public modifiers = getKeyboardMappings(this._layout).modifiers; + + setLayout(newLayout: KeyboardLayout) { + if (this._layout === newLayout) return; + this._layout = newLayout; + const updatedMappings = getKeyboardMappings(newLayout); + this.keys = updatedMappings.keys; + this.chars = updatedMappings.chars; + this.modifiers = updatedMappings.modifiers; + this._notifySubscribers(); + } + + getLayout() { + return this._layout; + } + + subscribe(callback: () => void) { + this._subscribers.push(callback); + return () => { + this._subscribers = this._subscribers.filter(sub => sub !== callback); // Cleanup + }; + } + + private _notifySubscribers() { + this._subscribers.forEach(callback => callback()); + } +} + +export const keyboardMappingsStore = new KeyboardMappingsStore(); \ No newline at end of file diff --git a/ui/src/keyboardMappings/layouts/uk_apple.ts b/ui/src/keyboardMappings/layouts/uk_apple.ts new file mode 100644 index 00000000..b9107ea6 --- /dev/null +++ b/ui/src/keyboardMappings/layouts/uk_apple.ts @@ -0,0 +1,24 @@ +import { charsUS, keysUS, modifiersUS } from "./us"; + +// Extend US Keys with UK Apple-specific changes +export const keysUKApple = { + ...keysUS, +} as Record; + +// Extend US Chars with UK Apple-specific changes +export const charsUKApple = { + ...charsUS, + "`": { key: "Backquote", shift: false }, + "~": { key: "Backquote", shift: true }, + "\\" : { key: "Backslash", shift: false }, + "|": { key: "Backslash", shift: true }, + "#": { key: "Digit3", shift: false, alt: true }, + "£": { key: "Digit3", shift: true }, + "@": { key: "Digit2", shift: true }, + "\"": { key: "Quote", shift: true }, +} as Record; + +// Modifiers are typically the same between UK and US layouts +export const modifiersUKApple = { + ...modifiersUS, +}; \ No newline at end of file diff --git a/ui/src/keyboardMappings/layouts/us.ts b/ui/src/keyboardMappings/layouts/us.ts new file mode 100644 index 00000000..4b75b778 --- /dev/null +++ b/ui/src/keyboardMappings/layouts/us.ts @@ -0,0 +1,215 @@ +export const keysUS = { + AltLeft: 0xe2, + AltRight: 0xe6, + ArrowDown: 0x51, + ArrowLeft: 0x50, + ArrowRight: 0x4f, + ArrowUp: 0x52, + Backquote: 0x35, + Backslash: 0x31, + Backspace: 0x2a, + BracketLeft: 0x2f, + BracketRight: 0x30, + CapsLock: 0x39, + Comma: 0x36, + ContextMenu: 0, + Delete: 0x4c, + Digit0: 0x27, + Digit1: 0x1e, + Digit2: 0x1f, + Digit3: 0x20, + Digit4: 0x21, + Digit5: 0x22, + Digit6: 0x23, + Digit7: 0x24, + Digit8: 0x25, + Digit9: 0x26, + End: 0x4d, + Enter: 0x28, + Equal: 0x2e, + Escape: 0x29, + F1: 0x3a, + F2: 0x3b, + F3: 0x3c, + F4: 0x3d, + F5: 0x3e, + F6: 0x3f, + F7: 0x40, + F8: 0x41, + F9: 0x42, + F10: 0x43, + F11: 0x44, + F12: 0x45, + F13: 0x68, + Home: 0x4a, + Insert: 0x49, + IntlBackslash: 0x31, + KeyA: 0x04, + KeyB: 0x05, + KeyC: 0x06, + KeyD: 0x07, + KeyE: 0x08, + KeyF: 0x09, + KeyG: 0x0a, + KeyH: 0x0b, + KeyI: 0x0c, + KeyJ: 0x0d, + KeyK: 0x0e, + KeyL: 0x0f, + KeyM: 0x10, + KeyN: 0x11, + KeyO: 0x12, + KeyP: 0x13, + KeyQ: 0x14, + KeyR: 0x15, + KeyS: 0x16, + KeyT: 0x17, + KeyU: 0x18, + KeyV: 0x19, + KeyW: 0x1a, + KeyX: 0x1b, + KeyY: 0x1c, + KeyZ: 0x1d, + KeypadExclamation: 0xcf, + Minus: 0x2d, + NumLock: 0x53, + Numpad0: 0x62, + Numpad1: 0x59, + Numpad2: 0x5a, + Numpad3: 0x5b, + Numpad4: 0x5c, + Numpad5: 0x5d, + Numpad6: 0x5e, + Numpad7: 0x5f, + Numpad8: 0x60, + Numpad9: 0x61, + NumpadAdd: 0x57, + NumpadDivide: 0x54, + NumpadEnter: 0x58, + NumpadMultiply: 0x55, + NumpadSubtract: 0x56, + NumpadDecimal: 0x63, + PageDown: 0x4e, + PageUp: 0x4b, + Period: 0x37, + Quote: 0x34, + Semicolon: 0x33, + Slash: 0x38, + Space: 0x2c, + Tab: 0x2b, +} as Record; + +export const charsUS = { + 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 modifiersUS = { + ControlLeft: 0x01, + ControlRight: 0x10, + ShiftLeft: 0x02, + ShiftRight: 0x20, + AltLeft: 0x04, + AltRight: 0x40, + MetaLeft: 0x08, + MetaRight: 0x80, +} as Record; + \ No newline at end of file From 7c40e2e01109e11df912900450ee04d1832793ec Mon Sep 17 00:00:00 2001 From: William Johnstone Date: Sat, 1 Feb 2025 18:38:40 +0000 Subject: [PATCH 2/9] Move keyboardmapping store to stores.ts, simplified some things, updated settings.tsx to set the keyboard layout properly. --- ui/src/components/InfoBar.tsx | 12 +++--- ui/src/components/VirtualKeyboard.tsx | 14 +++---- ui/src/components/WebRTCVideo.tsx | 20 ++++++---- ui/src/components/popovers/PasteModal.tsx | 22 +++++------ ui/src/components/sidebar/settings.tsx | 9 ++--- ui/src/hooks/stores.ts | 37 ++++++++++++++++++ ui/src/keyboardMappings/KeyboardLayouts.ts | 25 +++++------- .../keyboardMappings/KeyboardMappingStore.ts | 39 ------------------- ui/src/keyboardMappings/layouts/uk.ts | 0 ui/src/keyboardMappings/layouts/uk_apple.ts | 4 +- ui/src/keyboardMappings/layouts/us.ts | 4 +- 11 files changed, 90 insertions(+), 96 deletions(-) delete mode 100644 ui/src/keyboardMappings/KeyboardMappingStore.ts create mode 100644 ui/src/keyboardMappings/layouts/uk.ts diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx index 4490afe9..dbbaa407 100644 --- a/ui/src/components/InfoBar.tsx +++ b/ui/src/components/InfoBar.tsx @@ -5,18 +5,18 @@ import { useRTCStore, useSettingsStore, useVideoStore, + useKeyboardMappingsStore, } from "@/hooks/stores"; import { useEffect, useState } from "react"; -import { keyboardMappingsStore } from "@/keyboardMappings/KeyboardMappingStore"; export default function InfoBar() { - const [keys, setKeys] = useState(keyboardMappingsStore.keys); - const [modifiers, setModifiers] = useState(keyboardMappingsStore.modifiers); + const [keys, setKeys] = useState(useKeyboardMappingsStore.keys); + const [modifiers, setModifiers] = useState(useKeyboardMappingsStore.modifiers); useEffect(() => { - const unsubscribeKeyboardStore = keyboardMappingsStore.subscribe(() => { - setKeys(keyboardMappingsStore.keys); - setModifiers(keyboardMappingsStore.modifiers); + const unsubscribeKeyboardStore = useKeyboardMappingsStore.subscribe(() => { + setKeys(useKeyboardMappingsStore.keys); + setModifiers(useKeyboardMappingsStore.modifiers); }); return unsubscribeKeyboardStore; // Cleanup on unmount }, []); diff --git a/ui/src/components/VirtualKeyboard.tsx b/ui/src/components/VirtualKeyboard.tsx index f056c11c..fab78f9a 100644 --- a/ui/src/components/VirtualKeyboard.tsx +++ b/ui/src/components/VirtualKeyboard.tsx @@ -4,11 +4,9 @@ import { Button } from "@components/Button"; import Card from "@components/Card"; import { ChevronDownIcon } from "@heroicons/react/16/solid"; import "react-simple-keyboard/build/css/index.css"; -import { useHidStore, useUiStore } from "@/hooks/stores"; +import { useHidStore, useUiStore, useKeyboardMappingsStore } from "@/hooks/stores"; import { Transition } from "@headlessui/react"; import { cx } from "@/cva.config"; -//import { keys, modifiers } from "@/keyboardMappings/KeyboardMappingStore"; -import { keyboardMappingsStore } from "@/keyboardMappings/KeyboardMappingStore"; import useKeyboard from "@/hooks/useKeyboard"; import DetachIconRaw from "@/assets/detach-icon.svg"; import AttachIconRaw from "@/assets/attach-icon.svg"; @@ -22,13 +20,13 @@ const AttachIcon = ({ className }: { className?: string }) => { }; function KeyboardWrapper() { - const [keys, setKeys] = useState(keyboardMappingsStore.keys); - const [modifiers, setModifiers] = useState(keyboardMappingsStore.modifiers); + const [keys, setKeys] = useState(useKeyboardMappingsStore.keys); + const [modifiers, setModifiers] = useState(useKeyboardMappingsStore.modifiers); useEffect(() => { - const unsubscribeKeyboardStore = keyboardMappingsStore.subscribe(() => { - setKeys(keyboardMappingsStore.keys); - setModifiers(keyboardMappingsStore.modifiers); + const unsubscribeKeyboardStore = useKeyboardMappingsStore.subscribe(() => { + setKeys(useKeyboardMappingsStore.keys); + setModifiers(useKeyboardMappingsStore.modifiers); }); return unsubscribeKeyboardStore; // Cleanup on unmount }, []); diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 8e6b867c..61b1f9a2 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -6,8 +6,8 @@ import { useSettingsStore, useUiStore, useVideoStore, + useKeyboardMappingsStore, } from "@/hooks/stores"; -import { keyboardMappingsStore } from "@/keyboardMappings/KeyboardMappingStore"; import { useResizeObserver } from "@/hooks/useResizeObserver"; import { cx } from "@/cva.config"; import VirtualKeyboard from "@components/VirtualKeyboard"; @@ -18,13 +18,13 @@ import { useJsonRpc } from "@/hooks/useJsonRpc"; import { ConnectionErrorOverlay, HDMIErrorOverlay, LoadingOverlay } from "./VideoOverlay"; export default function WebRTCVideo() { - const [keys, setKeys] = useState(keyboardMappingsStore.keys); - const [modifiers, setModifiers] = useState(keyboardMappingsStore.modifiers); + const [keys, setKeys] = useState(useKeyboardMappingsStore.keys); + const [modifiers, setModifiers] = useState(useKeyboardMappingsStore.modifiers); useEffect(() => { - const unsubscribeKeyboardStore = keyboardMappingsStore.subscribe(() => { - setKeys(keyboardMappingsStore.keys); - setModifiers(keyboardMappingsStore.modifiers); + const unsubscribeKeyboardStore = useKeyboardMappingsStore.subscribe(() => { + setKeys(useKeyboardMappingsStore.keys); + setModifiers(useKeyboardMappingsStore.modifiers); }); return unsubscribeKeyboardStore; // Cleanup on unmount }, []); @@ -218,12 +218,15 @@ export default function WebRTCVideo() { const prev = useHidStore.getState(); let code = e.code; const key = e.key; + console.log(e); + console.log(key); - // if (document.activeElement?.id !== "videoFocusTrap") { + // if (document.activeElement?.id !== "videoFocusTrap") {hH // console.log("KEYUP: Not focusing on the video", document.activeElement); // return; // } - console.log(document.activeElement); + // + // console.log(document.activeElement); setIsNumLockActive(e.getModifierState("NumLock")); setIsCapsLockActive(e.getModifierState("CapsLock")); @@ -289,6 +292,7 @@ export default function WebRTCVideo() { prev.activeModifiers.filter(k => k !== modifiers[e.code]), ); + console.log(e.key); sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); }, [ diff --git a/ui/src/components/popovers/PasteModal.tsx b/ui/src/components/popovers/PasteModal.tsx index 6b3878f5..2387888b 100644 --- a/ui/src/components/popovers/PasteModal.tsx +++ b/ui/src/components/popovers/PasteModal.tsx @@ -3,28 +3,27 @@ import { GridCard } from "@components/Card"; import { TextAreaWithLabel } from "@components/TextArea"; import { SectionHeader } from "@components/SectionHeader"; import { useJsonRpc } from "@/hooks/useJsonRpc"; -import { useHidStore, useRTCStore, useUiStore } from "@/hooks/stores"; +import { useHidStore, useRTCStore, useUiStore, useKeyboardMappingsStore } from "@/hooks/stores"; import notifications from "../../notifications"; import { useCallback, useEffect, useRef, useState } from "react"; import { LuCornerDownLeft } from "react-icons/lu"; import { ExclamationCircleIcon } from "@heroicons/react/16/solid"; import { useClose } from "@headlessui/react"; -import { keyboardMappingsStore } from "@/keyboardMappings/KeyboardMappingStore"; const hidKeyboardPayload = (keys: number[], modifier: number) => { return { keys, modifier }; }; export default function PasteModal() { - const [keys, setKeys] = useState(keyboardMappingsStore.keys); - const [chars, setChars] = useState(keyboardMappingsStore.chars); - const [modifiers, setModifiers] = useState(keyboardMappingsStore.modifiers); + const [keys, setKeys] = useState(useKeyboardMappingsStore.keys); + const [chars, setChars] = useState(useKeyboardMappingsStore.chars); + const [modifiers, setModifiers] = useState(useKeyboardMappingsStore.modifiers); useEffect(() => { - const unsubscribeKeyboardStore = keyboardMappingsStore.subscribe(() => { - setKeys(keyboardMappingsStore.keys); - setChars(keyboardMappingsStore.chars); - setModifiers(keyboardMappingsStore.modifiers); + const unsubscribeKeyboardStore = useKeyboardMappingsStore.subscribe(() => { + setKeys(useKeyboardMappingsStore.keys); + setChars(useKeyboardMappingsStore.chars); + setModifiers(useKeyboardMappingsStore.modifiers); }); return unsubscribeKeyboardStore; // Cleanup on unmount }, []); @@ -54,13 +53,14 @@ export default function PasteModal() { try { for (const char of text) { - const { key, shift, alt } = chars[char] ?? {}; + const { key, shift, altLeft, altRight } = chars[char] ?? {}; if (!key) continue; // Build the modifier bitmask const modifier = (shift ? modifiers["ShiftLeft"] : 0) | - (alt ? modifiers["AltLeft"] : 0); + (altLeft ? modifiers["AltLeft"] : 0) | + (altRight ? modifiers["AltRight"] : 0); // This is important for a lot of keyboard layouts, right and left alt have different functions await new Promise((resolve, reject) => { send( diff --git a/ui/src/components/sidebar/settings.tsx b/ui/src/components/sidebar/settings.tsx index f7bd99a1..a6c3fe11 100644 --- a/ui/src/components/sidebar/settings.tsx +++ b/ui/src/components/sidebar/settings.tsx @@ -4,6 +4,7 @@ import { useSettingsStore, useUiStore, useUpdateStore, + useKeyboardMappingsStore, } from "@/hooks/stores"; import { Checkbox } from "@components/Checkbox"; import { Button, LinkButton } from "@components/Button"; @@ -25,8 +26,6 @@ import LocalAuthPasswordDialog from "@/components/LocalAuthPasswordDialog"; import { LocalDevice } from "@routes/devices.$id"; import { useRevalidator } from "react-router-dom"; import { ShieldCheckIcon } from "@heroicons/react/20/solid"; -import { keyboardMappingsStore } from "@/keyboardMappings/KeyboardMappingStore"; -import { KeyboardLayout } from "@/keyboardMappings/KeyboardLayouts"; export function SettingsItem({ title, @@ -157,8 +156,7 @@ export default function SettingsSidebar() { ); return; } - // TODO set this to update to the actual layout chosen - keyboardMappingsStore.setLayout(KeyboardLayout.UKApple) + useKeyboardMappingsStore.setLayout(keyboardLayout) setKeyboardLayout(keyboardLayout); }); }; @@ -294,6 +292,7 @@ export default function SettingsSidebar() { send("getKeyboardLayout", {}, resp => { if ("error" in resp) return; setKeyboardLayout(String(resp.result)); + useKeyboardMappingsStore.setLayout(String(resp.result)) }); send("getStreamQualityFactor", {}, resp => { @@ -545,7 +544,7 @@ export default function SettingsSidebar() { size="SM" label="" // TODO figure out how to make this selector wider like the EDID one? - //fullWidth + //fullWidthƒ value={keyboardLayout} options={[ { value: "uk", label: "GB" }, diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index b4cfbec8..29daa82d 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -1,5 +1,6 @@ import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; +import { getKeyboardMappings } from "@/keyboardMappings/KeyboardLayouts"; // Utility function to append stats to a Map const appendStatToMap = ( @@ -528,3 +529,39 @@ export const useLocalAuthModalStore = create(set => ({ setModalView: view => set({ modalView: view }), setErrorMessage: message => set({ errorMessage: message }), })); + +class KeyboardMappingsStore { + private _layout: string = 'us'; + private _subscribers: (() => void)[] = []; + + public keys = getKeyboardMappings(this._layout).keys; + public chars = getKeyboardMappings(this._layout).chars; + public modifiers = getKeyboardMappings(this._layout).modifiers; + + setLayout(newLayout: string) { + if (this._layout === newLayout) return; + this._layout = newLayout; + const updatedMappings = getKeyboardMappings(newLayout); + this.keys = updatedMappings.keys; + this.chars = updatedMappings.chars; + this.modifiers = updatedMappings.modifiers; + this._notifySubscribers(); + } + + getLayout() { + return this._layout; + } + + subscribe(callback: () => void) { + this._subscribers.push(callback); + return () => { + this._subscribers = this._subscribers.filter(sub => sub !== callback); // Cleanup + }; + } + + private _notifySubscribers() { + this._subscribers.forEach(callback => callback()); + } +} + +export const useKeyboardMappingsStore = new KeyboardMappingsStore(); \ No newline at end of file diff --git a/ui/src/keyboardMappings/KeyboardLayouts.ts b/ui/src/keyboardMappings/KeyboardLayouts.ts index baadeab4..b631d76d 100644 --- a/ui/src/keyboardMappings/KeyboardLayouts.ts +++ b/ui/src/keyboardMappings/KeyboardLayouts.ts @@ -1,20 +1,15 @@ import {keysUKApple, charsUKApple, modifiersUKApple } from './layouts/uk_apple'; import {keysUS, charsUS, modifiersUS } from './layouts/us'; -export enum KeyboardLayout { - US = "us", - UKApple = "uk_apple", - } - -export function getKeyboardMappings(layout: KeyboardLayout) { - switch (layout) { - case KeyboardLayout.UKApple: - return { - keys: keysUKApple, - chars: charsUKApple, - modifiers: modifiersUKApple, - }; - case KeyboardLayout.US: +export function getKeyboardMappings(layout: string) { + switch (layout) { + case "uk_apple": + return { + keys: keysUKApple, + chars: charsUKApple, + modifiers: modifiersUKApple, + }; + case "us": default: return { keys: keysUS, @@ -22,4 +17,4 @@ export function getKeyboardMappings(layout: KeyboardLayout) { modifiers: modifiersUS, }; } - } \ No newline at end of file +} \ No newline at end of file diff --git a/ui/src/keyboardMappings/KeyboardMappingStore.ts b/ui/src/keyboardMappings/KeyboardMappingStore.ts deleted file mode 100644 index 2d41bc1f..00000000 --- a/ui/src/keyboardMappings/KeyboardMappingStore.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { getKeyboardMappings, KeyboardLayout } from "@/keyboardMappings/KeyboardLayouts"; - -// TODO Move this in with all the other stores? - -class KeyboardMappingsStore { - private _layout: KeyboardLayout = KeyboardLayout.US; - private _subscribers: (() => void)[] = []; - - public keys = getKeyboardMappings(this._layout).keys; - public chars = getKeyboardMappings(this._layout).chars; - public modifiers = getKeyboardMappings(this._layout).modifiers; - - setLayout(newLayout: KeyboardLayout) { - if (this._layout === newLayout) return; - this._layout = newLayout; - const updatedMappings = getKeyboardMappings(newLayout); - this.keys = updatedMappings.keys; - this.chars = updatedMappings.chars; - this.modifiers = updatedMappings.modifiers; - this._notifySubscribers(); - } - - getLayout() { - return this._layout; - } - - subscribe(callback: () => void) { - this._subscribers.push(callback); - return () => { - this._subscribers = this._subscribers.filter(sub => sub !== callback); // Cleanup - }; - } - - private _notifySubscribers() { - this._subscribers.forEach(callback => callback()); - } -} - -export const keyboardMappingsStore = new KeyboardMappingsStore(); \ No newline at end of file diff --git a/ui/src/keyboardMappings/layouts/uk.ts b/ui/src/keyboardMappings/layouts/uk.ts new file mode 100644 index 00000000..e69de29b diff --git a/ui/src/keyboardMappings/layouts/uk_apple.ts b/ui/src/keyboardMappings/layouts/uk_apple.ts index b9107ea6..d257f432 100644 --- a/ui/src/keyboardMappings/layouts/uk_apple.ts +++ b/ui/src/keyboardMappings/layouts/uk_apple.ts @@ -12,11 +12,11 @@ export const charsUKApple = { "~": { key: "Backquote", shift: true }, "\\" : { key: "Backslash", shift: false }, "|": { key: "Backslash", shift: true }, - "#": { key: "Digit3", shift: false, alt: true }, + "#": { key: "Digit3", shift: false, altLeft: true }, "£": { key: "Digit3", shift: true }, "@": { key: "Digit2", shift: true }, "\"": { key: "Quote", shift: true }, -} as Record; +} as Record; // Modifiers are typically the same between UK and US layouts export const modifiersUKApple = { diff --git a/ui/src/keyboardMappings/layouts/us.ts b/ui/src/keyboardMappings/layouts/us.ts index 4b75b778..15a00711 100644 --- a/ui/src/keyboardMappings/layouts/us.ts +++ b/ui/src/keyboardMappings/layouts/us.ts @@ -200,8 +200,8 @@ export const charsUS = { "\n": { key: "Enter", shift: false }, Enter: { key: "Enter", shift: false }, Tab: { key: "Tab", shift: false }, -} as Record; - +} as Record; + export const modifiersUS = { ControlLeft: 0x01, ControlRight: 0x10, From 5f1e53f24a8d00e5bfab79f074b0bd678b0b89c6 Mon Sep 17 00:00:00 2001 From: William Johnstone Date: Mon, 3 Feb 2025 00:39:53 +0000 Subject: [PATCH 3/9] Fully implemented layout setting and switching in the UI. Updated PasteModal to add more clarity to error message. Begin working on key remapping in WebRTC (working to a reasonable degree). --- dev_deploy.sh | 1 + ui/src/components/WebRTCVideo.tsx | 68 ++++++++++++++++++--- ui/src/components/popovers/PasteModal.tsx | 2 +- ui/src/keyboardMappings/layouts/uk_apple.ts | 2 +- ui/src/keyboardMappings/layouts/us.ts | 2 +- 5 files changed, 65 insertions(+), 10 deletions(-) diff --git a/dev_deploy.sh b/dev_deploy.sh index a106395b..72bb6dd8 100755 --- a/dev_deploy.sh +++ b/dev_deploy.sh @@ -71,6 +71,7 @@ export LD_LIBRARY_PATH=/oem/usr/lib:\$LD_LIBRARY_PATH # Kill any existing instances of the application killall jetkvm_app || true killall jetkvm_app_debug || true +killall jetkvm_native || true # Navigate to the directory where the binary will be stored cd "$REMOTE_PATH" diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 61b1f9a2..9b160454 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -19,11 +19,17 @@ import { ConnectionErrorOverlay, HDMIErrorOverlay, LoadingOverlay } from "./Vide export default function WebRTCVideo() { const [keys, setKeys] = useState(useKeyboardMappingsStore.keys); + const [chars, setChars] = useState(useKeyboardMappingsStore.chars); const [modifiers, setModifiers] = useState(useKeyboardMappingsStore.modifiers); + // TODO move this into stores as well as I think this will need to be used in InfoBar + // This map is used to maintain consistency between localised key mappings + const activeKeyState = useRef>(new Map()); + useEffect(() => { const unsubscribeKeyboardStore = useKeyboardMappingsStore.subscribe(() => { - setKeys(useKeyboardMappingsStore.keys); + setKeys(useKeyboardMappingsStore.keys); + setChars(useKeyboardMappingsStore.chars); setModifiers(useKeyboardMappingsStore.modifiers); }); return unsubscribeKeyboardStore; // Cleanup on unmount @@ -217,9 +223,9 @@ export default function WebRTCVideo() { e.preventDefault(); const prev = useHidStore.getState(); let code = e.code; - const key = e.key; + const localisedKey = e.key; console.log(e); - console.log(key); + console.log("Localised Key: " + localisedKey); // if (document.activeElement?.id !== "videoFocusTrap") {hH // console.log("KEYUP: Not focusing on the video", document.activeElement); @@ -232,25 +238,42 @@ export default function WebRTCVideo() { setIsCapsLockActive(e.getModifierState("CapsLock")); setIsScrollLockActive(e.getModifierState("ScrollLock")); - if (code == "IntlBackslash" && ["`", "~"].includes(key)) { + /*if (code == "IntlBackslash" && ["`", "~"].includes(key)) { code = "Backquote"; } else if (code == "Backquote" && ["§", "±"].includes(key)) { code = "IntlBackslash"; - } + }*/ + + const { key: mappedKey, shift, altLeft, altRight } = chars[localisedKey] ?? { key: e.code }; + //if (!key) continue; + console.log("Mapped Key: " + mappedKey) + console.log("Current KB Layout:" + useKeyboardMappingsStore.getLayout()); + + // Build the modifier bitmask + const modifier = + (shift ? modifiers["ShiftLeft"] : 0) | + (altLeft ? modifiers["AltLeft"] : 0) | + (altRight ? modifiers["AltRight"] : 0); // This is important for a lot of keyboard layouts, right and left alt have different functions + + // Add the mapped key to keyState + activeKeyState.current.set(e.code, { mappedKey, modifiers: modifier }); + console.log(activeKeyState) // Add the key to the active keys - const newKeys = [...prev.activeKeys, keys[code]].filter(Boolean); + const newKeys = [...prev.activeKeys, keys[mappedKey]].filter(Boolean); // Add the modifier to the active modifiers const newModifiers = handleModifierKeys(e, [ ...prev.activeModifiers, modifiers[code], + modifier, //Is this bad, will we have duplicate modifiers? ]); // When pressing the meta key + another key, the key will never trigger a keyup // event, so we need to clear the keys after a short delay // https://bugs.chromium.org/p/chromium/issues/detail?id=28089 // https://bugzilla.mozilla.org/show_bug.cgi?id=1299553 + // TODO add this to the activekey state if (e.metaKey) { setTimeout(() => { const prev = useHidStore.getState(); @@ -283,14 +306,44 @@ export default function WebRTCVideo() { setIsCapsLockActive(e.getModifierState("CapsLock")); setIsScrollLockActive(e.getModifierState("ScrollLock")); + // Retrieve the mapped key and modifiers from keyState + const keyInfo = activeKeyState.current.get(e.code); + if (!keyInfo) return; // Ignore if no record exists + + const { mappedKey, modifiers: modifier } = keyInfo; + + // Remove the key from keyState + activeKeyState.current.delete(e.code); + + // Filter out the key that was just released + const newKeys = prev.activeKeys.filter(k => k !== keys[mappedKey]).filter(Boolean); + console.log(activeKeyState) + + // Filter out the associated modifier + //const newModifiers = prev.activeModifiers.filter(k => k !== modifier).filter(Boolean); + const newModifiers = handleModifierKeys( + e, + prev.activeModifiers.filter(k => k !== modifier), + ); + /* + const { key: mappedKey/*, shift, altLeft, altRight*//* } = chars[e.key] ?? { key: e.code }; + //if (!key) continue; + console.log("Mapped Key: " + mappedKey) + // Build the modifier bitmask + /*const modifier = + (shift ? modifiers["ShiftLeft"] : 0) | + (altLeft ? modifiers["AltLeft"] : 0) | + (altRight ? modifiers["AltRight"] : 0); // This is important for a lot of keyboard layouts, right and left alt have different functions*//* + // Filtering out the key that was just released (keys[e.code]) - const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean); + const newKeys = prev.activeKeys.filter(k => k !== keys[mappedKey]).filter(Boolean); // Filter out the modifier that was just released const newModifiers = handleModifierKeys( e, prev.activeModifiers.filter(k => k !== modifiers[e.code]), ); + */ console.log(e.key); sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); @@ -321,6 +374,7 @@ export default function WebRTCVideo() { return () => { abortController.abort(); + activeKeyState.current.clear(); }; }, [keyDownHandler, keyUpHandler, resetKeyboardState, sendKeyboardEvent], diff --git a/ui/src/components/popovers/PasteModal.tsx b/ui/src/components/popovers/PasteModal.tsx index 2387888b..9728cb8d 100644 --- a/ui/src/components/popovers/PasteModal.tsx +++ b/ui/src/components/popovers/PasteModal.tsx @@ -141,7 +141,7 @@ export default function PasteModal() {
- The following characters won't be pasted:{" "} + The following characters won't be pasted as the current keyboard layout does not contain a valid mapping:{" "} {invalidChars.join(", ")}
diff --git a/ui/src/keyboardMappings/layouts/uk_apple.ts b/ui/src/keyboardMappings/layouts/uk_apple.ts index d257f432..24d974e2 100644 --- a/ui/src/keyboardMappings/layouts/uk_apple.ts +++ b/ui/src/keyboardMappings/layouts/uk_apple.ts @@ -16,7 +16,7 @@ export const charsUKApple = { "£": { key: "Digit3", shift: true }, "@": { key: "Digit2", shift: true }, "\"": { key: "Quote", shift: true }, -} as Record; +} as Record; // Modifiers are typically the same between UK and US layouts export const modifiersUKApple = { diff --git a/ui/src/keyboardMappings/layouts/us.ts b/ui/src/keyboardMappings/layouts/us.ts index 15a00711..a5395c2b 100644 --- a/ui/src/keyboardMappings/layouts/us.ts +++ b/ui/src/keyboardMappings/layouts/us.ts @@ -200,7 +200,7 @@ export const charsUS = { "\n": { key: "Enter", shift: false }, Enter: { key: "Enter", shift: false }, Tab: { key: "Tab", shift: false }, -} as Record; +} as Record; export const modifiersUS = { ControlLeft: 0x01, From f1de6639ef7526a6d0694a31949f5419878bdd9e Mon Sep 17 00:00:00 2001 From: William Johnstone Date: Tue, 4 Feb 2025 01:45:34 +0000 Subject: [PATCH 4/9] Continued work on keyboard overhaul, found many more issues caused by my changes (YAY!). Also spent 4 hours troubleshooting to find out I didn't realise how useCallback works... :/ Anway, not much longer before work on just the mappings can begin. --- ui/src/components/VirtualKeyboard.tsx | 5 +- ui/src/components/WebRTCVideo.tsx | 96 +++++++++++++++++++-- ui/src/keyboardMappings/layouts/uk_apple.ts | 2 +- 3 files changed, 93 insertions(+), 10 deletions(-) diff --git a/ui/src/components/VirtualKeyboard.tsx b/ui/src/components/VirtualKeyboard.tsx index fab78f9a..9f09240a 100644 --- a/ui/src/components/VirtualKeyboard.tsx +++ b/ui/src/components/VirtualKeyboard.tsx @@ -20,12 +20,15 @@ const AttachIcon = ({ className }: { className?: string }) => { }; function KeyboardWrapper() { + // TODO implement virtual keyboard mapping const [keys, setKeys] = useState(useKeyboardMappingsStore.keys); + //const [chars, setChars] = useState(useKeyboardMappingsStore.chars); const [modifiers, setModifiers] = useState(useKeyboardMappingsStore.modifiers); useEffect(() => { const unsubscribeKeyboardStore = useKeyboardMappingsStore.subscribe(() => { - setKeys(useKeyboardMappingsStore.keys); + setKeys(useKeyboardMappingsStore.keys); + //setChars(useKeyboardMappingsStore.chars); setModifiers(useKeyboardMappingsStore.modifiers); }); return unsubscribeKeyboardStore; // Cleanup on unmount diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 9b160454..cf3990d6 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -22,9 +22,8 @@ export default function WebRTCVideo() { const [chars, setChars] = useState(useKeyboardMappingsStore.chars); const [modifiers, setModifiers] = useState(useKeyboardMappingsStore.modifiers); - // TODO move this into stores as well as I think this will need to be used in InfoBar // This map is used to maintain consistency between localised key mappings - const activeKeyState = useRef>(new Map()); + const activeKeyState = useRef>(new Map()); useEffect(() => { const unsubscribeKeyboardStore = useKeyboardMappingsStore.subscribe(() => { @@ -140,6 +139,7 @@ export default function WebRTCVideo() { if (blockWheelEvent) return; e.preventDefault(); + // TODO this should be user controllable // Define a scaling factor to adjust scrolling sensitivity const scrollSensitivity = 0.8; // Adjust this value to change scroll speed @@ -155,9 +155,11 @@ export default function WebRTCVideo() { // Invert the scroll value to match expected behavior const invertedScroll = -roundedScroll; + // TODO remove debug logs console.log("wheelReport", { wheelY: invertedScroll }); send("wheelReport", { wheelY: invertedScroll }); + // TODO this is making scrolling feel slow and sluggish, also throwing a violation in chrome setBlockWheelEvent(true); setTimeout(() => setBlockWheelEvent(false), 50); }, @@ -168,6 +170,7 @@ export default function WebRTCVideo() { sendMouseMovement(0, 0, 0); }, [sendMouseMovement]); + // TODO this needs reworked ot work with mappings. // Keyboard-related const handleModifierKeys = useCallback( (e: KeyboardEvent, activeModifiers: number[]) => { @@ -244,19 +247,23 @@ export default function WebRTCVideo() { code = "IntlBackslash"; }*/ - const { key: mappedKey, shift, altLeft, altRight } = chars[localisedKey] ?? { key: e.code }; + const { key: mappedKey, shift, altLeft, altRight } = useKeyboardMappingsStore.chars[localisedKey] ?? { key: e.code }; //if (!key) continue; console.log("Mapped Key: " + mappedKey) console.log("Current KB Layout:" + useKeyboardMappingsStore.getLayout()); + console.log(chars[localisedKey]); // Build the modifier bitmask - const modifier = + const modifierBitmask = (shift ? modifiers["ShiftLeft"] : 0) | (altLeft ? modifiers["AltLeft"] : 0) | (altRight ? modifiers["AltRight"] : 0); // This is important for a lot of keyboard layouts, right and left alt have different functions - + // On second thought this may not be relevant here, may be best to just send altRight through, needs testing + console.log("Modifier Bitmask: " + modifierBitmask) + console.log("Shift: " + shift + ", altLeft: " + altLeft + ", altRight: " + altRight) + // Add the mapped key to keyState - activeKeyState.current.set(e.code, { mappedKey, modifiers: modifier }); + activeKeyState.current.set(e.code, { mappedKey, modifiers: {shift, altLeft, altRight, bitmask: modifierBitmask}}); console.log(activeKeyState) // Add the key to the active keys @@ -266,7 +273,7 @@ export default function WebRTCVideo() { const newModifiers = handleModifierKeys(e, [ ...prev.activeModifiers, modifiers[code], - modifier, //Is this bad, will we have duplicate modifiers? + modifierBitmask, //Is this bad, will we have duplicate modifiers? ]); // When pressing the meta key + another key, the key will never trigger a keyup @@ -289,6 +296,9 @@ export default function WebRTCVideo() { setIsScrollLockActive, handleModifierKeys, sendKeyboardEvent, + chars, + keys, + modifiers, ], ); @@ -306,6 +316,73 @@ export default function WebRTCVideo() { setIsCapsLockActive(e.getModifierState("CapsLock")); setIsScrollLockActive(e.getModifierState("ScrollLock")); + // Check if the released key is a modifier (e.g., Shift, Alt, Control) + const isModifierKey = + e.code === "ShiftLeft" || + e.code === "ShiftRight" || + e.code === "AltLeft" || + e.code === "AltRight"; + //e.code === "ControlLeft" || These shouldn't make a difference for mappings + //e.code === "ControlRight"; + + // Handle modifier release + if (isModifierKey) { + // Update all affected keys when this modifier is released + activeKeyState.current.forEach((value, code) => { + const { mappedKey, modifiers: mappedModifiers} = value; + + // Remove the released modifier from the modifier bitmask + //const updatedModifiers = modifiers & ~modifiers[e.code]; + + // Recalculate the remapped key based on the updated modifiers + //const updatedMappedKey = chars[originalKey]?.key || originalKey; + + var removeCurrentKey = false; + + // Shift Handling + if (mappedModifiers.shift && (e.code === "ShiftLeft" || e.code === "ShiftRight")) { + activeKeyState.current.delete(code); + removeCurrentKey = true; + }; + // Left Alt handling + if (mappedModifiers.altLeft && e.code === "AltLeft") { + activeKeyState.current.delete(code); + removeCurrentKey = true; + }; + // Right Alt handling + if (mappedModifiers.altRight && e.code === "AltRight") { + activeKeyState.current.delete(code); + removeCurrentKey = true; + }; + + var newKeys = prev.activeKeys; + + if (removeCurrentKey) { + newKeys = prev.activeKeys + .filter(k => k !== keys[mappedKey]) // Remove the previously mapped key + //.concat(keys[updatedMappedKey]) // Add the new remapped key, don't need to do this. + .filter(Boolean); + }; + + const newModifiers = prev.activeModifiers.filter(k => k !== modifiers[e.code]); + + // Update the keyState + /*activeKeyState.current.delete(code);/*.set(code, { + mappedKey: updatedMappedKey, + modifiers: updatedModifiers, + originalKey, + });*/ + + // Remove the modifer key from keyState + activeKeyState.current.delete(e.code); + + // Send the updated HID payload + sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); + }); + + return; // Exit as we've already handled the modifier release + } + // Retrieve the mapped key and modifiers from keyState const keyInfo = activeKeyState.current.get(e.code); if (!keyInfo) return; // Ignore if no record exists @@ -323,7 +400,7 @@ export default function WebRTCVideo() { //const newModifiers = prev.activeModifiers.filter(k => k !== modifier).filter(Boolean); const newModifiers = handleModifierKeys( e, - prev.activeModifiers.filter(k => k !== modifier), + prev.activeModifiers.filter(k => k !== modifier.bitmask), ); /* const { key: mappedKey/*, shift, altLeft, altRight*//* } = chars[e.key] ?? { key: e.code }; @@ -354,6 +431,9 @@ export default function WebRTCVideo() { setIsScrollLockActive, handleModifierKeys, sendKeyboardEvent, + chars, + keys, + modifiers, ], ); diff --git a/ui/src/keyboardMappings/layouts/uk_apple.ts b/ui/src/keyboardMappings/layouts/uk_apple.ts index 24d974e2..c5ec7f92 100644 --- a/ui/src/keyboardMappings/layouts/uk_apple.ts +++ b/ui/src/keyboardMappings/layouts/uk_apple.ts @@ -21,4 +21,4 @@ export const charsUKApple = { // Modifiers are typically the same between UK and US layouts export const modifiersUKApple = { ...modifiersUS, -}; \ No newline at end of file +} as Record; \ No newline at end of file From 8732a6aff878358a22569c34094e987868fc391e Mon Sep 17 00:00:00 2001 From: William Johnstone Date: Wed, 5 Feb 2025 01:26:24 +0000 Subject: [PATCH 5/9] Fix ghost keys issue, properly implemented modifer mapping --- ui/src/components/WebRTCVideo.tsx | 75 ++++++++++++++++++++----------- ui/src/hooks/stores.ts | 2 + 2 files changed, 51 insertions(+), 26 deletions(-) diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index cf3990d6..c636bdc6 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -23,7 +23,7 @@ export default function WebRTCVideo() { const [modifiers, setModifiers] = useState(useKeyboardMappingsStore.modifiers); // This map is used to maintain consistency between localised key mappings - const activeKeyState = useRef>(new Map()); + const activeKeyState = useRef>(new Map()); useEffect(() => { const unsubscribeKeyboardStore = useKeyboardMappingsStore.subscribe(() => { @@ -170,14 +170,16 @@ export default function WebRTCVideo() { sendMouseMovement(0, 0, 0); }, [sendMouseMovement]); - // TODO this needs reworked ot work with mappings. + // TODO this needs reworked ot work with mappings // Keyboard-related const handleModifierKeys = useCallback( - (e: KeyboardEvent, activeModifiers: number[]) => { + (e: KeyboardEvent, activeModifiers: number[], mappedKeyModifers: { shift: boolean; altLeft: boolean; altRight: boolean; }) => { const { shiftKey, ctrlKey, altKey, metaKey } = e; - const filteredModifiers = activeModifiers.filter(Boolean); + // TODO remove debug logging + console.log(shiftKey + " " +ctrlKey + " " +altKey + " " +metaKey + " " +mappedKeyModifers.shift + " "+mappedKeyModifers.altLeft + " "+mappedKeyModifers.altRight + " ") + const filteredModifiers = activeModifiers.filter(Boolean);3 // Example: activeModifiers = [0x01, 0x02, 0x04, 0x08] // Assuming 0x01 = ControlLeft, 0x02 = ShiftLeft, 0x04 = AltLeft, 0x08 = MetaLeft return ( @@ -188,6 +190,7 @@ export default function WebRTCVideo() { .filter( modifier => shiftKey || + mappedKeyModifers.shift || (modifier !== modifiers["ShiftLeft"] && modifier !== modifiers["ShiftRight"]), ) @@ -206,6 +209,8 @@ export default function WebRTCVideo() { .filter( modifier => altKey || + mappedKeyModifers.altLeft || + mappedKeyModifers.altRight || (modifier !== modifiers["AltLeft"] && modifier !== modifiers["AltRight"]), ) // Meta: Keep if Meta is pressed or if the key isn't a Meta key @@ -247,40 +252,38 @@ export default function WebRTCVideo() { code = "IntlBackslash"; }*/ - const { key: mappedKey, shift, altLeft, altRight } = useKeyboardMappingsStore.chars[localisedKey] ?? { key: e.code }; + const { key: mappedKey, shift, altLeft, altRight } = chars[localisedKey] ?? { key: code }; //if (!key) continue; console.log("Mapped Key: " + mappedKey) console.log("Current KB Layout:" + useKeyboardMappingsStore.getLayout()); console.log(chars[localisedKey]); - // Build the modifier bitmask - const modifierBitmask = - (shift ? modifiers["ShiftLeft"] : 0) | - (altLeft ? modifiers["AltLeft"] : 0) | - (altRight ? modifiers["AltRight"] : 0); // This is important for a lot of keyboard layouts, right and left alt have different functions - // On second thought this may not be relevant here, may be best to just send altRight through, needs testing - console.log("Modifier Bitmask: " + modifierBitmask) console.log("Shift: " + shift + ", altLeft: " + altLeft + ", altRight: " + altRight) // Add the mapped key to keyState - activeKeyState.current.set(e.code, { mappedKey, modifiers: {shift, altLeft, altRight, bitmask: modifierBitmask}}); + activeKeyState.current.set(e.code, { mappedKey, modifiers: {shift, altLeft, altRight}}); console.log(activeKeyState) // Add the key to the active keys const newKeys = [...prev.activeKeys, keys[mappedKey]].filter(Boolean); + // TODO I feel this may not be applying the modifiers correctly, specifically altRight // Add the modifier to the active modifiers const newModifiers = handleModifierKeys(e, [ ...prev.activeModifiers, modifiers[code], - modifierBitmask, //Is this bad, will we have duplicate modifiers? - ]); + (shift? modifiers['ShiftLeft'] : 0), + (altLeft? modifiers['AltLeft'] : 0), + (altRight? modifiers['AltRight'] : 0),], + {shift: shift, altLeft: altLeft? true : false, altRight: altRight ? true : false} + ); // When pressing the meta key + another key, the key will never trigger a keyup // event, so we need to clear the keys after a short delay // https://bugs.chromium.org/p/chromium/issues/detail?id=28089 // https://bugzilla.mozilla.org/show_bug.cgi?id=1299553 // TODO add this to the activekey state + // TODO set this to remove from activekeystate as well if (e.metaKey) { setTimeout(() => { const prev = useHidStore.getState(); @@ -305,6 +308,7 @@ export default function WebRTCVideo() { const keyUpHandler = useCallback( (e: KeyboardEvent) => { e.preventDefault(); + console.log(e) const prev = useHidStore.getState(); // if (document.activeElement?.id !== "videoFocusTrap") { @@ -321,12 +325,15 @@ export default function WebRTCVideo() { e.code === "ShiftLeft" || e.code === "ShiftRight" || e.code === "AltLeft" || - e.code === "AltRight"; - //e.code === "ControlLeft" || These shouldn't make a difference for mappings - //e.code === "ControlRight"; + e.code === "AltRight" || + e.code === "ControlLeft" || + e.code === "ControlRight"; + + var newKeys = prev.activeKeys; // Handle modifier release if (isModifierKey) { + console.log("ITS A MODIFER") // Update all affected keys when this modifier is released activeKeyState.current.forEach((value, code) => { const { mappedKey, modifiers: mappedModifiers} = value; @@ -355,16 +362,21 @@ export default function WebRTCVideo() { removeCurrentKey = true; }; - var newKeys = prev.activeKeys; - if (removeCurrentKey) { - newKeys = prev.activeKeys + newKeys = newKeys .filter(k => k !== keys[mappedKey]) // Remove the previously mapped key //.concat(keys[updatedMappedKey]) // Add the new remapped key, don't need to do this. .filter(Boolean); }; - - const newModifiers = prev.activeModifiers.filter(k => k !== modifiers[e.code]); + }); + console.log("prev.activemodifers: " + prev.activeModifiers) + console.log("prev.activemodifers.filtered: " + prev.activeModifiers.filter(k => k !== modifiers[e.code])) + const newModifiers = handleModifierKeys( + e, + prev.activeModifiers.filter(k => k !== modifiers[e.code]), + {shift: false, altLeft: false, altRight: false} + ); + console.log("New modifiers in keyup: " + newModifiers) // Update the keyState /*activeKeyState.current.delete(code);/*.set(code, { @@ -376,9 +388,14 @@ export default function WebRTCVideo() { // Remove the modifer key from keyState activeKeyState.current.delete(e.code); + // This is required to filter out the alt keys as well as the modifier. + newKeys = newKeys + .filter(k => k !== keys[e.code]) // Remove the previously mapped key + //.concat(keys[updatedMappedKey]) // Add the new remapped key, don't need to do this. + .filter(Boolean); + // Send the updated HID payload sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); - }); return; // Exit as we've already handled the modifier release } @@ -393,14 +410,20 @@ export default function WebRTCVideo() { activeKeyState.current.delete(e.code); // Filter out the key that was just released - const newKeys = prev.activeKeys.filter(k => k !== keys[mappedKey]).filter(Boolean); + newKeys = newKeys.filter(k => k !== keys[mappedKey]).filter(Boolean); console.log(activeKeyState) // Filter out the associated modifier //const newModifiers = prev.activeModifiers.filter(k => k !== modifier).filter(Boolean); const newModifiers = handleModifierKeys( e, - prev.activeModifiers.filter(k => k !== modifier.bitmask), + prev.activeModifiers.filter(k => { + if (modifier.shift && k == modifiers["ShiftLeft"]) return false; + if (modifier.altLeft && k == modifiers["AltLeft"]) return false; + if (modifier.altRight && k == modifiers["AltRight"]) return false; + return true; + }), + {shift: modifier.shift, altLeft: modifier.altLeft? true : false, altRight: modifier.altRight ? true : false} ); /* const { key: mappedKey/*, shift, altLeft, altRight*//* } = chars[e.key] ?? { key: e.code }; diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 29daa82d..6118b0b8 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -387,6 +387,8 @@ export const useHidStore = create(set => ({ activeKeys: [], activeModifiers: [], updateActiveKeysAndModifiers: ({ keys, modifiers }) => { + // TODO remove debug logs + console.log("keys: " + keys + "modifiers: " + modifiers) return set({ activeKeys: keys, activeModifiers: modifiers }); }, From 40b1c70be030c1a155eea3945b12c49a26782842 Mon Sep 17 00:00:00 2001 From: William Johnstone Date: Tue, 25 Feb 2025 00:44:17 +0000 Subject: [PATCH 6/9] Added German (T1) mappings, UK mappings, updated UK apple mappings. Added functionality to disable keyboard mapping. --- config.go | 30 +++--- jsonrpc.go | 108 +++++++++++--------- ui/src/components/SelectMenuBasic.tsx | 1 + ui/src/components/WebRTCVideo.tsx | 25 +++-- ui/src/components/popovers/PasteModal.tsx | 5 +- ui/src/components/sidebar/settings.tsx | 44 +++++++- ui/src/hooks/stores.ts | 36 ++++++- ui/src/keyboardMappings/KeyboardLayouts.ts | 14 +++ ui/src/keyboardMappings/layouts/de_t1.ts | 69 +++++++++++++ ui/src/keyboardMappings/layouts/uk.ts | 24 +++++ ui/src/keyboardMappings/layouts/uk_apple.ts | 1 + 11 files changed, 279 insertions(+), 78 deletions(-) create mode 100644 ui/src/keyboardMappings/layouts/de_t1.ts diff --git a/config.go b/config.go index 3ae40668..eb707c53 100644 --- a/config.go +++ b/config.go @@ -12,25 +12,27 @@ type WakeOnLanDevice struct { } type Config struct { - CloudURL string `json:"cloud_url"` - CloudToken string `json:"cloud_token"` - GoogleIdentity string `json:"google_identity"` - JigglerEnabled bool `json:"jiggler_enabled"` - AutoUpdateEnabled bool `json:"auto_update_enabled"` - KeyboardLayout string `json:"keyboard_layout"` - IncludePreRelease bool `json:"include_pre_release"` - HashedPassword string `json:"hashed_password"` - LocalAuthToken string `json:"local_auth_token"` - LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration - WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` + CloudURL string `json:"cloud_url"` + CloudToken string `json:"cloud_token"` + GoogleIdentity string `json:"google_identity"` + JigglerEnabled bool `json:"jiggler_enabled"` + AutoUpdateEnabled bool `json:"auto_update_enabled"` + KeyboardLayout string `json:"keyboard_layout"` + KeyboardMappingEnabled bool `json:"keyboard_mapping_enabled"` + IncludePreRelease bool `json:"include_pre_release"` + HashedPassword string `json:"hashed_password"` + LocalAuthToken string `json:"local_auth_token"` + LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration + WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` } const configPath = "/userdata/kvm_config.json" var defaultConfig = &Config{ - CloudURL: "https://api.jetkvm.com", - AutoUpdateEnabled: true, // Set a default value - KeyboardLayout: "us", + CloudURL: "https://api.jetkvm.com", + AutoUpdateEnabled: true, // Set a default value + KeyboardLayout: "us", + KeyboardMappingEnabled: false, } var config *Config diff --git a/jsonrpc.go b/jsonrpc.go index f7543bb5..c859fc5e 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -143,6 +143,18 @@ func rpcSetKeyboardLayout(KeyboardLayout string) (string, error) { return KeyboardLayout, nil } +func rpcGetKeyboardMappingState() (bool, error) { + return config.KeyboardMappingEnabled, nil +} + +func rpcSetKeyboardMappingState(enabled bool) (bool, error) { + config.KeyboardMappingEnabled = enabled + if err := SaveConfig(); err != nil { + return config.KeyboardMappingEnabled, fmt.Errorf("failed to save config: %w", err) + } + return enabled, nil +} + var streamFactor = 1.0 func rpcGetStreamQualityFactor() (float64, error) { @@ -521,51 +533,53 @@ func rpcResetConfig() error { // TODO: replace this crap with code generator var rpcHandlers = map[string]RPCHandler{ - "ping": {Func: rpcPing}, - "getDeviceID": {Func: rpcGetDeviceID}, - "deregisterDevice": {Func: rpcDeregisterDevice}, - "getCloudState": {Func: rpcGetCloudState}, - "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, - "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, - "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, - "getVideoState": {Func: rpcGetVideoState}, - "getUSBState": {Func: rpcGetUSBState}, - "unmountImage": {Func: rpcUnmountImage}, - "rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}}, - "setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}}, - "getJigglerState": {Func: rpcGetJigglerState}, - "sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}}, - "getKeyboardLayout": {Func: rpcGetKeyboardLayout}, - "setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"kbLayout"}}, - "getStreamQualityFactor": {Func: rpcGetStreamQualityFactor}, - "setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}}, - "getAutoUpdateState": {Func: rpcGetAutoUpdateState}, - "setAutoUpdateState": {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}}, - "getEDID": {Func: rpcGetEDID}, - "setEDID": {Func: rpcSetEDID, Params: []string{"edid"}}, - "getDevChannelState": {Func: rpcGetDevChannelState}, - "setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}}, - "getUpdateStatus": {Func: rpcGetUpdateStatus}, - "tryUpdate": {Func: rpcTryUpdate}, - "getDevModeState": {Func: rpcGetDevModeState}, - "setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}}, - "getSSHKeyState": {Func: rpcGetSSHKeyState}, - "setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}}, - "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, - "getMassStorageMode": {Func: rpcGetMassStorageMode}, - "isUpdatePending": {Func: rpcIsUpdatePending}, - "getUsbEmulationState": {Func: rpcGetUsbEmulationState}, - "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, - "checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}}, - "getVirtualMediaState": {Func: rpcGetVirtualMediaState}, - "getStorageSpace": {Func: rpcGetStorageSpace}, - "mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}}, - "mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}}, - "mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}}, - "listStorageFiles": {Func: rpcListStorageFiles}, - "deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}}, - "startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}}, - "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, - "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, - "resetConfig": {Func: rpcResetConfig}, + "ping": {Func: rpcPing}, + "getDeviceID": {Func: rpcGetDeviceID}, + "deregisterDevice": {Func: rpcDeregisterDevice}, + "getCloudState": {Func: rpcGetCloudState}, + "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, + "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, + "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, + "getVideoState": {Func: rpcGetVideoState}, + "getUSBState": {Func: rpcGetUSBState}, + "unmountImage": {Func: rpcUnmountImage}, + "rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}}, + "setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}}, + "getJigglerState": {Func: rpcGetJigglerState}, + "sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}}, + "getKeyboardLayout": {Func: rpcGetKeyboardLayout}, + "setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"kbLayout"}}, + "setKeyboardMappingState": {Func: rpcSetKeyboardMappingState, Params: []string{"enabled"}}, + "getKeyboardMappingState": {Func: rpcGetKeyboardMappingState}, + "getStreamQualityFactor": {Func: rpcGetStreamQualityFactor}, + "setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}}, + "getAutoUpdateState": {Func: rpcGetAutoUpdateState}, + "setAutoUpdateState": {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}}, + "getEDID": {Func: rpcGetEDID}, + "setEDID": {Func: rpcSetEDID, Params: []string{"edid"}}, + "getDevChannelState": {Func: rpcGetDevChannelState}, + "setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}}, + "getUpdateStatus": {Func: rpcGetUpdateStatus}, + "tryUpdate": {Func: rpcTryUpdate}, + "getDevModeState": {Func: rpcGetDevModeState}, + "setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}}, + "getSSHKeyState": {Func: rpcGetSSHKeyState}, + "setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}}, + "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, + "getMassStorageMode": {Func: rpcGetMassStorageMode}, + "isUpdatePending": {Func: rpcIsUpdatePending}, + "getUsbEmulationState": {Func: rpcGetUsbEmulationState}, + "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, + "checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}}, + "getVirtualMediaState": {Func: rpcGetVirtualMediaState}, + "getStorageSpace": {Func: rpcGetStorageSpace}, + "mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}}, + "mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}}, + "mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}}, + "listStorageFiles": {Func: rpcListStorageFiles}, + "deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}}, + "startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}}, + "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, + "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, + "resetConfig": {Func: rpcResetConfig}, } diff --git a/ui/src/components/SelectMenuBasic.tsx b/ui/src/components/SelectMenuBasic.tsx index eb4c5403..f515eb3f 100644 --- a/ui/src/components/SelectMenuBasic.tsx +++ b/ui/src/components/SelectMenuBasic.tsx @@ -24,6 +24,7 @@ type SelectMenuProps = Pick< const sizes = { XS: "h-[24.5px] pl-3 pr-8 text-xs", SM: "h-[32px] pl-3 pr-8 text-[13px]", + SM_Wide: "h-[32px] pl-3 pr-8 mr-5 text-[13px]", MD: "h-[40px] pl-4 pr-10 text-sm", LG: "h-[48px] pl-4 pr-10 px-5 text-base", }; diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index c636bdc6..080a21f8 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -17,6 +17,10 @@ import useKeyboard from "@/hooks/useKeyboard"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import { ConnectionErrorOverlay, HDMIErrorOverlay, LoadingOverlay } from "./VideoOverlay"; +// TODO Implement keyboard lock API to resolve #127 +// https://developer.chrome.com/docs/capabilities/web-apis/keyboard-lock +// An appropriate error message will need to be displayed in order to alert users to browser compatibility issues. + export default function WebRTCVideo() { const [keys, setKeys] = useState(useKeyboardMappingsStore.keys); const [chars, setChars] = useState(useKeyboardMappingsStore.chars); @@ -155,8 +159,6 @@ export default function WebRTCVideo() { // Invert the scroll value to match expected behavior const invertedScroll = -roundedScroll; - // TODO remove debug logs - console.log("wheelReport", { wheelY: invertedScroll }); send("wheelReport", { wheelY: invertedScroll }); // TODO this is making scrolling feel slow and sluggish, also throwing a violation in chrome @@ -179,7 +181,7 @@ export default function WebRTCVideo() { // TODO remove debug logging console.log(shiftKey + " " +ctrlKey + " " +altKey + " " +metaKey + " " +mappedKeyModifers.shift + " "+mappedKeyModifers.altLeft + " "+mappedKeyModifers.altRight + " ") - const filteredModifiers = activeModifiers.filter(Boolean);3 + const filteredModifiers = activeModifiers.filter(Boolean); // Example: activeModifiers = [0x01, 0x02, 0x04, 0x08] // Assuming 0x01 = ControlLeft, 0x02 = ShiftLeft, 0x04 = AltLeft, 0x08 = MetaLeft return ( @@ -210,8 +212,13 @@ export default function WebRTCVideo() { modifier => altKey || mappedKeyModifers.altLeft || + (modifier !== modifiers["AltLeft"]), + ) + .filter( + modifier => + altKey || mappedKeyModifers.altRight || - (modifier !== modifiers["AltLeft"] && modifier !== modifiers["AltRight"]), + (modifier !== modifiers["AltRight"]) ) // Meta: Keep if Meta is pressed or if the key isn't a Meta key // Example: If metaKey is true, keep all modifiers @@ -230,8 +237,9 @@ export default function WebRTCVideo() { async (e: KeyboardEvent) => { e.preventDefault(); const prev = useHidStore.getState(); - let code = e.code; - const localisedKey = e.key; + const code = e.code; + console.log("MAPPING ENABLED: " + settings.keyboardMappingEnabled) + var localisedKey = settings.keyboardMappingEnabled ? e.key : code; console.log(e); console.log("Localised Key: " + localisedKey); @@ -282,12 +290,12 @@ export default function WebRTCVideo() { // event, so we need to clear the keys after a short delay // https://bugs.chromium.org/p/chromium/issues/detail?id=28089 // https://bugzilla.mozilla.org/show_bug.cgi?id=1299553 - // TODO add this to the activekey state - // TODO set this to remove from activekeystate as well if (e.metaKey) { setTimeout(() => { const prev = useHidStore.getState(); sendKeyboardEvent([], newModifiers || prev.activeModifiers); + activeKeyState.current.delete("MetaLeft"); + activeKeyState.current.delete("MetaRight"); }, 10); } @@ -302,6 +310,7 @@ export default function WebRTCVideo() { chars, keys, modifiers, + settings, ], ); diff --git a/ui/src/components/popovers/PasteModal.tsx b/ui/src/components/popovers/PasteModal.tsx index 9728cb8d..66c26942 100644 --- a/ui/src/components/popovers/PasteModal.tsx +++ b/ui/src/components/popovers/PasteModal.tsx @@ -79,7 +79,7 @@ export default function PasteModal() { } catch (error) { notifications.error("Failed to paste text"); } - }, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode]); + }, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, chars, keys, modifiers]); useEffect(() => { if (TextAreaRef.current) { @@ -144,6 +144,9 @@ export default function PasteModal() { The following characters won't be pasted as the current keyboard layout does not contain a valid mapping:{" "} {invalidChars.join(", ")} + + Tip: You can set your desired keyboard layout in settings, and remember to enable keyboard mapping. +
)}
diff --git a/ui/src/components/sidebar/settings.tsx b/ui/src/components/sidebar/settings.tsx index a6c3fe11..4e8d37cd 100644 --- a/ui/src/components/sidebar/settings.tsx +++ b/ui/src/components/sidebar/settings.tsx @@ -79,6 +79,7 @@ export default function SettingsSidebar() { const settings = useSettingsStore(); const [send] = useJsonRpc(); const [keyboardLayout, setKeyboardLayout] = useState("us"); + const [kbMappingEnabled, setKeyboardMapping] = useState(false); const [streamQuality, setStreamQuality] = useState("1"); const [autoUpdate, setAutoUpdate] = useState(true); const [devChannel, setDevChannel] = useState(false); @@ -161,6 +162,20 @@ export default function SettingsSidebar() { }); }; + const handleKeyboardMappingChange = (enabled: boolean) => { + send("setKeyboardMappingState", { enabled }, resp => { + if ("error" in resp) { + notifications.error( + `Failed to set keyboard maping state state: ${resp.error.data || "Unknown error"}`, + ); + return; + } + settings.setkeyboardMappingEnabled(enabled); + useKeyboardMappingsStore.setMappingsState(enabled); + setKeyboardMapping(enabled); + }); + }; + const handleStreamQualityChange = (factor: string) => { send("setStreamQualityFactor", { factor: Number(factor) }, resp => { if ("error" in resp) { @@ -295,6 +310,13 @@ export default function SettingsSidebar() { useKeyboardMappingsStore.setLayout(String(resp.result)) }); + send("getKeyboardMappingState", {}, resp => { + if ("error" in resp) return; + setKeyboardMapping(resp.result as boolean); + settings.setkeyboardMappingEnabled(resp.result as boolean); + useKeyboardMappingsStore.setMappingsState(resp.result as boolean); + }); + send("getStreamQualityFactor", {}, resp => { if ("error" in resp) return; setStreamQuality(String(resp.result)); @@ -536,20 +558,32 @@ export default function SettingsSidebar() { description="Customize keyboard behaviour" />
+ + { + handleKeyboardMappingChange(e.target.checked); + }} + /> + handleKeyboardLayoutChange(e.target.value)} /> diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 6118b0b8..f1c4923a 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -265,6 +265,9 @@ interface SettingsState { mouseMode: string; setMouseMode: (mode: string) => void; + keyboardMappingEnabled: boolean; + setkeyboardMappingEnabled: (enabled: boolean) => void; + debugMode: boolean; setDebugMode: (enabled: boolean) => void; @@ -276,6 +279,9 @@ interface SettingsState { export const useSettingsStore = create( persist( set => ({ + keyboardMappingEnabled: false, + setkeyboardMappingEnabled: enabled => set({keyboardMappingEnabled: enabled}), + isCursorHidden: false, setCursorVisibility: enabled => set({ isCursorHidden: enabled }), @@ -535,18 +541,42 @@ export const useLocalAuthModalStore = create(set => ({ class KeyboardMappingsStore { private _layout: string = 'us'; private _subscribers: (() => void)[] = []; + private _mappingsEnabled: boolean = false; public keys = getKeyboardMappings(this._layout).keys; public chars = getKeyboardMappings(this._layout).chars; public modifiers = getKeyboardMappings(this._layout).modifiers; + private mappedKeys = getKeyboardMappings(this._layout).keys; + private mappedChars = getKeyboardMappings(this._layout).chars; + private mappedModifiers = getKeyboardMappings(this._layout).modifiers; + setLayout(newLayout: string) { if (this._layout === newLayout) return; this._layout = newLayout; const updatedMappings = getKeyboardMappings(newLayout); - this.keys = updatedMappings.keys; - this.chars = updatedMappings.chars; - this.modifiers = updatedMappings.modifiers; + this.mappedKeys = updatedMappings.keys; + this.mappedChars = updatedMappings.chars; + this.mappedModifiers = updatedMappings.modifiers; + if (this._mappingsEnabled) { + this.keys = this.mappedKeys; + this.chars = this.mappedChars; + this.modifiers = this.mappedModifiers; + this._notifySubscribers(); + } + } + + setMappingsState(enabled: boolean) { + this._mappingsEnabled = enabled; + if (this._mappingsEnabled) { + this.keys = this.mappedKeys; + this.chars = this.mappedChars; + this.modifiers = this.mappedModifiers; + } else { + this.keys = getKeyboardMappings('us').keys; + this.chars = getKeyboardMappings('us').chars; + this.modifiers = getKeyboardMappings('us').modifiers; + } this._notifySubscribers(); } diff --git a/ui/src/keyboardMappings/KeyboardLayouts.ts b/ui/src/keyboardMappings/KeyboardLayouts.ts index b631d76d..11fd798d 100644 --- a/ui/src/keyboardMappings/KeyboardLayouts.ts +++ b/ui/src/keyboardMappings/KeyboardLayouts.ts @@ -1,5 +1,7 @@ import {keysUKApple, charsUKApple, modifiersUKApple } from './layouts/uk_apple'; +import {keysUK, charsUK, modifiersUK } from './layouts/uk'; import {keysUS, charsUS, modifiersUS } from './layouts/us'; +import { keysDE_T1, charsDE_T1, modifiersDE_T1 } from './layouts/de_t1'; export function getKeyboardMappings(layout: string) { switch (layout) { @@ -9,6 +11,18 @@ export function getKeyboardMappings(layout: string) { chars: charsUKApple, modifiers: modifiersUKApple, }; + case "uk": + return { + keys: keysUK, + chars: charsUK, + modifiers: modifiersUK, + }; + case "de_t1": + return { + keys: keysDE_T1, + chars: charsDE_T1, + modifiers: modifiersDE_T1, + }; case "us": default: return { diff --git a/ui/src/keyboardMappings/layouts/de_t1.ts b/ui/src/keyboardMappings/layouts/de_t1.ts new file mode 100644 index 00000000..8fd7bfac --- /dev/null +++ b/ui/src/keyboardMappings/layouts/de_t1.ts @@ -0,0 +1,69 @@ +import { charsUS, keysUS, modifiersUS } from "./us"; + +export const keysDE_T1 = { + ...keysUS, +} as Record; + +export const charsDE_T1 = { + ...charsUS, + + "y": { key: "KeyZ", shift: false }, + "Y": { key: "KeyZ", shift: true }, + "z": { key: "KeyY", shift: false }, + "Z": { key: "KeyY", shift: true }, + + "ä": { key: "Quote", shift: false }, + "Ä": { key: "Quote", shift: true }, + "ö": { key: "Semicolon", shift: false }, + "Ö": { key: "Semicolon", shift: true }, + "ü": { key: "BracketLeft", shift: false }, + "Ü": { key: "BracketLeft", shift: true }, + "ß": { key: "Minus", shift: false }, + "?": { key: "Minus", shift: true }, + + "§": { key: "Digit3", shift: true }, + "°": { key: "Backquote", shift: true }, + + "@": { key: "KeyQ", shift: false, altRight: true }, + "\"": { key: "Digit2", shift: true }, + + "#": { key: "Backslash", shift: false }, + "'": { key: "Backslash", shift: true }, + + ".": { key: "Period", shift: false }, + ":": { key: "Period", shift: true }, + ",": { key: "Comma", shift: false }, + ";": { key: "Comma", shift: true }, + + "-": { key: "Slash", shift: false }, + "_": { key: "Slash", shift: true }, + + "*": { key: "BracketRight", shift: true }, + "+": { key: "BracketRight", shift: false }, + "=": { key: "Digit0", shift: true }, + "~": { key: "BracketRight", shift: false, altRight: true }, + "{": { key: "Digit7", shift: false, altRight: true }, + "}": { key: "Digit0", shift: false, altRight: true }, + "[": { key: "Digit8", shift: false, altRight: true }, + "]": { key: "Digit9", shift: false, altRight: true }, + + "\\": { key: "Minus", shift: false, altRight: true }, + "|": { key: "IntlBackslash", shift: true, altRight: true }, + + "<": { key: "IntlBackslash", shift: false }, + ">": { key: "IntlBackslash", shift: true }, + + "^": {key: "Backquote", shift: false}, + + "€": { key: "KeyE", shift: false, altRight: true }, + + "²": {key: "Digit2", shift: false, altRight: true }, + "³": {key: "Digit3", shift: false, altRight: true }, + + "μ": {key: "KeyM", shift: false, altRight: true }, + +} as Record; + +export const modifiersDE_T1 = { + ...modifiersUS, +} as Record; \ No newline at end of file diff --git a/ui/src/keyboardMappings/layouts/uk.ts b/ui/src/keyboardMappings/layouts/uk.ts index e69de29b..f64979f1 100644 --- a/ui/src/keyboardMappings/layouts/uk.ts +++ b/ui/src/keyboardMappings/layouts/uk.ts @@ -0,0 +1,24 @@ +import { charsUS, keysUS, modifiersUS } from "./us"; + +export const keysUK = { + ...keysUS, +} as Record; + +export const charsUK = { + ...charsUS, + "`": { key: "Backquote", shift: false }, + "~": { key: "Backslash", shift: true }, + "\\": { key: "IntlBacklash", shift: false }, + "|": { key: "IntlBacklash", shift: true }, + "#": { key: "Backslash", shift: false }, + "£": { key: "Digit3", shift: true }, + "@": { key: "Quote", shift: true }, + "\"": { key: "Digit2", shift: true }, + "¬": { key: "Backquote", shift: true }, + "¦": { key: "Backquote", shift: false, altRight: true }, + "€": { key: "Digit4", shift: false, altRight: true }, +} as Record; + +export const modifiersUK = { + ...modifiersUS, +} as Record; \ No newline at end of file diff --git a/ui/src/keyboardMappings/layouts/uk_apple.ts b/ui/src/keyboardMappings/layouts/uk_apple.ts index c5ec7f92..fef400fa 100644 --- a/ui/src/keyboardMappings/layouts/uk_apple.ts +++ b/ui/src/keyboardMappings/layouts/uk_apple.ts @@ -16,6 +16,7 @@ export const charsUKApple = { "£": { key: "Digit3", shift: true }, "@": { key: "Digit2", shift: true }, "\"": { key: "Quote", shift: true }, + "¬": { key: "KeyL", shift: false, altLeft: true}, } as Record; // Modifiers are typically the same between UK and US layouts From fb3f5f44fccbbaf8e30a2db6754da1795c22454f Mon Sep 17 00:00:00 2001 From: William Johnstone Date: Thu, 27 Feb 2025 01:33:22 +0000 Subject: [PATCH 7/9] Almost complete implementation of mapped virtual keyboard. Still to implement proper modifer key holding. --- ui/src/components/VirtualKeyboard.tsx | 205 ++++++++++++++++++++++++-- ui/src/components/WebRTCVideo.tsx | 1 + ui/src/hooks/stores.ts | 4 + 3 files changed, 197 insertions(+), 13 deletions(-) diff --git a/ui/src/components/VirtualKeyboard.tsx b/ui/src/components/VirtualKeyboard.tsx index 9f09240a..9ceb8d95 100644 --- a/ui/src/components/VirtualKeyboard.tsx +++ b/ui/src/components/VirtualKeyboard.tsx @@ -20,21 +20,40 @@ const AttachIcon = ({ className }: { className?: string }) => { }; function KeyboardWrapper() { - // TODO implement virtual keyboard mapping const [keys, setKeys] = useState(useKeyboardMappingsStore.keys); - //const [chars, setChars] = useState(useKeyboardMappingsStore.chars); + const [chars, setChars] = useState(useKeyboardMappingsStore.chars); const [modifiers, setModifiers] = useState(useKeyboardMappingsStore.modifiers); useEffect(() => { const unsubscribeKeyboardStore = useKeyboardMappingsStore.subscribe(() => { setKeys(useKeyboardMappingsStore.keys); - //setChars(useKeyboardMappingsStore.chars); + setChars(useKeyboardMappingsStore.chars); setModifiers(useKeyboardMappingsStore.modifiers); + setMappingsEnabled(useKeyboardMappingsStore.getMappingState()); }); return unsubscribeKeyboardStore; // Cleanup on unmount }, []); const [layoutName, setLayoutName] = useState("default"); + const [mappingsEnabled, setMappingsEnabled] = useState(useKeyboardMappingsStore.getMappingState()); + + useEffect(() => { + if (mappingsEnabled) { + if (layoutName == "default" ) { + setLayoutName("mappedLower") + } + if (layoutName == "shift") { + setLayoutName("mappedUpper") + } + } else { + if (layoutName == "mappedLower") { + setLayoutName("default") + } + if (layoutName == "mappedUpper") { + setLayoutName("shift") + } + } + }, [mappingsEnabled, layoutName]); const keyboardRef = useRef(null); const showAttachedVirtualKeyboard = useUiStore( @@ -121,16 +140,28 @@ function KeyboardWrapper() { }; }, [endDrag, onDrag, startDrag]); + // TODO implement meta key and meta key modifer + // TODO implement hold functionality for key combos. (add a hold button, add all keys to an array, when released send as one) const onKeyDown = useCallback( (key: string) => { + const cleanKey = key.replace(/[()]/g, ""); + // Mappings + const { key: mappedKey, shift, altLeft, altRight } = chars[cleanKey] ?? {}; + const isKeyShift = key === "{shift}" || key === "ShiftLeft" || key === "ShiftRight"; const isKeyCaps = key === "CapsLock"; - const cleanKey = key.replace(/[()]/g, ""); - const keyHasShiftModifier = key.includes("("); + const keyHasShiftModifier = (key.includes("(") && key !== "(") || shift; + + //TODO remove debug logs + console.log(layoutName) // Handle toggle of layout for shift or caps lock const toggleLayout = () => { - setLayoutName(prevLayout => (prevLayout === "default" ? "shift" : "default")); + if (mappingsEnabled) { + setLayoutName(prevLayout => (prevLayout === "mappedLower" ? "mappedUpper" : "mappedLower")); + } else { + setLayoutName(prevLayout => (prevLayout === "default" ? "shift" : "default")); + } }; if (key === "CtrlAltDelete") { @@ -152,10 +183,17 @@ function KeyboardWrapper() { return; } - if (isKeyShift || isKeyCaps) { + if (isKeyShift || (!(layoutName == "shift" || layoutName == "mappedUpper") && isCapsLockActive)) { toggleLayout(); + } - if (isCapsLockActive) { + if (layoutName == "shift" || layoutName == "mappedUpper") { + if (!isCapsLockActive) { + toggleLayout(); + } + + if (isKeyCaps && isCapsLockActive) { + toggleLayout(); setIsCapsLockActive(false); sendKeyboardEvent([keys["CapsLock"]], []); return; @@ -164,25 +202,38 @@ function KeyboardWrapper() { // Handle caps lock state change if (isKeyCaps) { + toggleLayout(); setIsCapsLockActive(!isCapsLockActive); } + //TODO remove debug logs + console.log(cleanKey) + console.log(chars[cleanKey]) + + console.log(mappedKey) + // Collect new active keys and modifiers - const newKeys = keys[cleanKey] ? [keys[cleanKey]] : []; + const newKeys = keys[mappedKey ?? cleanKey] ? [keys[mappedKey ?? cleanKey]] : []; const newModifiers = - keyHasShiftModifier && !isCapsLockActive ? [modifiers["ShiftLeft"]] : []; + [ + ((shift || isKeyShift)? modifiers['ShiftLeft'] : 0), + (altLeft? modifiers['AltLeft'] : 0), + (altRight? modifiers['AltRight'] : 0), + ].filter(Boolean); + + console.log(newModifiers); // Update current keys and modifiers - sendKeyboardEvent(newKeys, newModifiers); + sendKeyboardEvent(newKeys, [...new Set(newModifiers)]); // If shift was used as a modifier and caps lock is not active, revert to default layout if (keyHasShiftModifier && !isCapsLockActive) { - setLayoutName("default"); + setLayoutName(mappingsEnabled ? "mappedLower" : "default"); } setTimeout(resetKeyboardState, 100); }, - [isCapsLockActive, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive], + [isCapsLockActive, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive, mappingsEnabled, chars, keys, modifiers, layoutName], ); const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled); @@ -398,6 +449,115 @@ function KeyboardWrapper() { F10: "F10", F11: "F11", F12: "F12", + + "q": "q", + "w": "w", + "e": "e", + "r": "r", + "t": "t", + "y": "y", + "u": "u", + "i": "i", + "o": "o", + "p": "p", + "a": "a", + "s": "s", + "d": "d", + "f": "f", + "g": "g", + "h": "h", + "j": "j", + "k": "k", + "l": "l", + "z": "z", + "x": "x", + "c": "c", + "v": "v", + "b": "b", + "n": "n", + "m": "m", + + "Q": "Q", + "W": "W", + "E": "E", + "R": "R", + "T": "T", + "Y": "Y", + "U": "U", + "I": "I", + "O": "O", + "P": "P", + "A": "A", + "S": "S", + "D": "D", + "F": "F", + "G": "G", + "H": "H", + "J": "J", + "K": "K", + "L": "L", + "Z": "Z", + "X": "X", + "C": "C", + "V": "V", + "B": "B", + "N": "N", + "M": "M", + + "1": "1", + "2": "2", + "3": "3", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", + "9": "9", + "0": "0", + + "!": "!", + "@": "@", + "#": "#", + "$": "$", + "%": "%", + "^": "^", + "&": "&", + "*": "*", + "(": "(", + ")": ")", + + "-": "-", + "_": "_", + + "=": "=", + "+": "+", + + "[": "[", + "]": "]", + "{": "{", + "}": "}", + + "|": "|", + + ";": ";", + ":": ":", + + "'": "'", + "\"": "\"", + + ",": ",", + "<": "<", + + ".": ".", + ">": ">", + + "/": "/", + "?": "?", + + "`": "`", + "~": "~", + + "\\": "\\" }} layout={{ default: [ @@ -418,6 +578,25 @@ function KeyboardWrapper() { "ShiftLeft (KeyZ) (KeyX) (KeyC) (KeyV) (KeyB) (KeyN) (KeyM) (Comma) (Period) (Slash) ShiftRight", "ControlLeft AltLeft MetaLeft Space MetaRight AltRight", ], + mappedLower: [ + "CtrlAltDelete AltMetaEscape", + "Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12", + "` 1 2 3 4 5 6 7 8 9 0 - = Backspace", + "Tab q w e r t y u i o p [ ] \\", + "CapsLock a s d f g h j k l ; ' Enter", + "ShiftLeft z x c v b n m , . / ShiftRight", + "ControlLeft AltLeft MetaLeft Space MetaRight AltRight" + ], + + mappedUpper: [ + "CtrlAltDelete AltMetaEscape", + "Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12", + "~ ! @ # $ % ^ & * ( ) _ + Backspace", + "Tab Q W E R T Y U I O P { } |", + "CapsLock A S D F G H J K L : \" Enter", + "ShiftLeft Z X C V B N M < > ? ShiftRight", + "ControlLeft AltLeft MetaLeft Space MetaRight AltRight" + ], }} disableButtonHold={true} mergeDisplay={true} diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 080a21f8..2ec820dd 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -20,6 +20,7 @@ import { ConnectionErrorOverlay, HDMIErrorOverlay, LoadingOverlay } from "./Vide // TODO Implement keyboard lock API to resolve #127 // https://developer.chrome.com/docs/capabilities/web-apis/keyboard-lock // An appropriate error message will need to be displayed in order to alert users to browser compatibility issues. +// This requires TLS, waiting on TLS support. export default function WebRTCVideo() { const [keys, setKeys] = useState(useKeyboardMappingsStore.keys); diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index f1c4923a..23958c7a 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -580,6 +580,10 @@ class KeyboardMappingsStore { this._notifySubscribers(); } + getMappingState() { + return this._mappingsEnabled; + } + getLayout() { return this._layout; } From 940612ab6a17173bde91f0bccc6ebefc8d9ecc4b Mon Sep 17 00:00:00 2001 From: William Johnstone Date: Fri, 11 Apr 2025 13:26:49 +0100 Subject: [PATCH 8/9] Updated keyboard character codes, added half working spanish mappings --- config.go | 2 +- ui/src/components/WebRTCVideo.tsx | 2 + ui/src/components/sidebar/settings.tsx | 11 ++-- ui/src/hooks/stores.ts | 2 +- ui/src/keyboardMappings/KeyboardLayouts.ts | 21 ++++--- ui/src/keyboardMappings/layouts/es.ts | 68 ++++++++++++++++++++++ 6 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 ui/src/keyboardMappings/layouts/es.ts diff --git a/config.go b/config.go index eb707c53..3657653c 100644 --- a/config.go +++ b/config.go @@ -31,7 +31,7 @@ const configPath = "/userdata/kvm_config.json" var defaultConfig = &Config{ CloudURL: "https://api.jetkvm.com", AutoUpdateEnabled: true, // Set a default value - KeyboardLayout: "us", + KeyboardLayout: "en-US", KeyboardMappingEnabled: false, } diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 2ec820dd..ec690a61 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -22,6 +22,8 @@ import { ConnectionErrorOverlay, HDMIErrorOverlay, LoadingOverlay } from "./Vide // An appropriate error message will need to be displayed in order to alert users to browser compatibility issues. // This requires TLS, waiting on TLS support. + +// TODO Implement keyboard mapping setup in initial JetKVM setup export default function WebRTCVideo() { const [keys, setKeys] = useState(useKeyboardMappingsStore.keys); const [chars, setChars] = useState(useKeyboardMappingsStore.chars); diff --git a/ui/src/components/sidebar/settings.tsx b/ui/src/components/sidebar/settings.tsx index 4e8d37cd..f6f47c11 100644 --- a/ui/src/components/sidebar/settings.tsx +++ b/ui/src/components/sidebar/settings.tsx @@ -78,7 +78,7 @@ export default function SettingsSidebar() { const setSidebarView = useUiStore(state => state.setSidebarView); const settings = useSettingsStore(); const [send] = useJsonRpc(); - const [keyboardLayout, setKeyboardLayout] = useState("us"); + const [keyboardLayout, setKeyboardLayout] = useState("en-US"); const [kbMappingEnabled, setKeyboardMapping] = useState(false); const [streamQuality, setStreamQuality] = useState("1"); const [autoUpdate, setAutoUpdate] = useState(true); @@ -580,10 +580,11 @@ export default function SettingsSidebar() { //fullWidth value={keyboardLayout} options={[ - { value: "us", label: "US" }, - { value: "uk", label: "UK" }, - { value: "uk_apple", label: "UK (Apple)" }, - { value: "de_t1", label: "German (T1)" }, + { value: "en-US", label: "US" }, + { value: "en-GB", label: "UK" }, + { value: "en-GB_apple", label: "UK (Apple)" }, + { value: "de_DE", label: "German (T1)" }, + { value: "es-ES", label: "Spanish (Spain)"}, ]} onChange={e => handleKeyboardLayoutChange(e.target.value)} /> diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 23958c7a..2116b4c7 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -539,7 +539,7 @@ export const useLocalAuthModalStore = create(set => ({ })); class KeyboardMappingsStore { - private _layout: string = 'us'; + private _layout: string = 'en-US'; private _subscribers: (() => void)[] = []; private _mappingsEnabled: boolean = false; diff --git a/ui/src/keyboardMappings/KeyboardLayouts.ts b/ui/src/keyboardMappings/KeyboardLayouts.ts index 11fd798d..920eeb4f 100644 --- a/ui/src/keyboardMappings/KeyboardLayouts.ts +++ b/ui/src/keyboardMappings/KeyboardLayouts.ts @@ -1,29 +1,36 @@ -import {keysUKApple, charsUKApple, modifiersUKApple } from './layouts/uk_apple'; -import {keysUK, charsUK, modifiersUK } from './layouts/uk'; -import {keysUS, charsUS, modifiersUS } from './layouts/us'; +import { keysUKApple, charsUKApple, modifiersUKApple } from './layouts/uk_apple'; +import { keysUK, charsUK, modifiersUK } from './layouts/uk'; +import { keysUS, charsUS, modifiersUS } from './layouts/us'; import { keysDE_T1, charsDE_T1, modifiersDE_T1 } from './layouts/de_t1'; +import { keysES, charsES, modifiersES } from './layouts/es'; export function getKeyboardMappings(layout: string) { switch (layout) { - case "uk_apple": + case "en-GB_apple": return { keys: keysUKApple, chars: charsUKApple, modifiers: modifiersUKApple, }; - case "uk": + case "en-GB": return { keys: keysUK, chars: charsUK, modifiers: modifiersUK, }; - case "de_t1": + case "de-DE": return { keys: keysDE_T1, chars: charsDE_T1, modifiers: modifiersDE_T1, }; - case "us": + case "es-ES": + return { + keys: keysES, + chars: charsES, + modifiers: modifiersES, + }; + case "en-US": default: return { keys: keysUS, diff --git a/ui/src/keyboardMappings/layouts/es.ts b/ui/src/keyboardMappings/layouts/es.ts new file mode 100644 index 00000000..ada24f71 --- /dev/null +++ b/ui/src/keyboardMappings/layouts/es.ts @@ -0,0 +1,68 @@ +import { charsUS, keysUS, modifiersUS } from "./us"; + +export const keysES = { + ...keysUS, +} as Record; + +export const charsES = { + ...charsUS, + + "ñ": { key: "Semicolon", shift: false }, + "Ñ": { key: "Semicolon", shift: true }, + + "º": { key: "Backquote", shift: false }, + "ª": { key: "Backquote", shift: true }, + + "¡": { key: "Equals", shift: false}, + + "¿": { key: "Slash", shift: false, altRight: true }, + "?": { key: "Slash", shift: true }, + + "|": { key: "Digit1", shift: false, altRight: true }, + + "@": { key: "Digit2", shift: false, altRight: true }, + "\"": { key: "Digit2", shift: true }, + + "·": { key: "Digit3", shift: false, altRight: true }, + "#": { key: "Digit3", shift: true }, + + "$": { key: "Digit4", shift: true }, + "€": { key: "Digit5", shift: false, altRight: true }, + + "&": { key: "Digit6", shift: true }, + + "/": { key: "Digit7", shift: true }, + "(": { key: "Digit8", shift: true }, + ")": { key: "Digit9", shift: true }, + "=": { key: "Digit0", shift: true }, + + "'": { key: "Quote", shift: false }, + "?": { key: "Quote", shift: true }, + + "-": { key: "Minus", shift: false }, + "_": { key: "Minus", shift: true }, + + "`": { key: "IntlBackslash", shift: false }, + "^": { key: "IntlBackslash", shift: true }, + "[": { key: "IntlBackslash", shift: false, altRight: true }, + "{": { key: "IntlBackslash", shift: true, altRight: true }, + + "+": { key: "Equal", shift: true }, + "]": { key: "Equal", shift: false, altRight: true }, + "}": { key: "Equal", shift: true, altRight: true }, + + "<": { key: "Backslash", shift: false }, + ">": { key: "Backslash", shift: true }, + + + ",": { key: "Comma", shift: false }, + ";": { key: "Comma", shift: true }, + + ".": { key: "Period", shift: false }, + ":": { key: "Period", shift: true }, + +} as Record; + +export const modifiersES = { + ...modifiersUS, +} as Record; \ No newline at end of file From e53445067e23b9bfe8e23f7be7deac5c61a1d0e0 Mon Sep 17 00:00:00 2001 From: William Johnstone Date: Sat, 12 Apr 2025 17:01:43 +0100 Subject: [PATCH 9/9] Update macros to use new keyboard implementation, add keyboard settings to new settings page, update PasteModal to use new keyboard implemention, dropped spanish mappings. --- ui/src/components/MacroStepCard.tsx | 7 +- ui/src/components/VirtualKeyboard.tsx | 11 - ui/src/components/WebRTCVideo.tsx | 19 -- ui/src/components/popovers/PasteModal.tsx | 1 - ui/src/hooks/stores.ts | 2 - ui/src/hooks/useKeyboard.ts | 15 +- ui/src/keyboardMappings/KeyboardLayouts.ts | 193 +++++++++++++++++- ui/src/keyboardMappings/layouts/es.ts | 68 ------ ui/src/routes/devices.$id.settings.macros.tsx | 3 +- ui/src/routes/devices.$id.settings.mouse.tsx | 90 +++++++- ui/src/routes/devices.$id.settings.tsx | 2 +- 11 files changed, 293 insertions(+), 118 deletions(-) delete mode 100644 ui/src/keyboardMappings/layouts/es.ts diff --git a/ui/src/components/MacroStepCard.tsx b/ui/src/components/MacroStepCard.tsx index 8642c28c..61ba2bcd 100644 --- a/ui/src/components/MacroStepCard.tsx +++ b/ui/src/components/MacroStepCard.tsx @@ -4,21 +4,22 @@ import { Button } from "@/components/Button"; import { Combobox } from "@/components/Combobox"; import { SelectMenuBasic } from "@/components/SelectMenuBasic"; import Card from "@/components/Card"; -import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings"; +import { keyDisplayMap } from "@/keyboardMappings/KeyboardLayouts"; +import { keysUS, modifiersUS } from '../keyboardMappings/layouts/us'; import { MAX_KEYS_PER_STEP, DEFAULT_DELAY } from "@/constants/macros"; import FieldLabel from "@/components/FieldLabel"; // Filter out modifier keys since they're handled in the modifiers section const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta']; -const keyOptions = Object.keys(keys) +const keyOptions = Object.keys(keysUS) .filter(key => !modifierKeyPrefixes.some(prefix => key.startsWith(prefix))) .map(key => ({ value: key, label: keyDisplayMap[key] || key, })); -const modifierOptions = Object.keys(modifiers).map(modifier => ({ +const modifierOptions = Object.keys(modifiersUS).map(modifier => ({ value: modifier, label: modifier.replace(/^(Control|Alt|Shift|Meta)(Left|Right)$/, "$1 $2"), })); diff --git a/ui/src/components/VirtualKeyboard.tsx b/ui/src/components/VirtualKeyboard.tsx index b0e31944..da302db0 100644 --- a/ui/src/components/VirtualKeyboard.tsx +++ b/ui/src/components/VirtualKeyboard.tsx @@ -157,9 +157,6 @@ function KeyboardWrapper() { const isKeyCaps = key === "CapsLock"; const keyHasShiftModifier = (key.includes("(") && key !== "(") || shift; - //TODO remove debug logs - console.log(layoutName) - // Handle toggle of layout for shift or caps lock const toggleLayout = () => { if (mappingsEnabled) { @@ -211,12 +208,6 @@ function KeyboardWrapper() { setIsCapsLockActive(!isCapsLockActive); } - //TODO remove debug logs - console.log(cleanKey) - console.log(chars[cleanKey]) - - console.log(mappedKey) - // Collect new active keys and modifiers const newKeys = keys[mappedKey ?? cleanKey] ? [keys[mappedKey ?? cleanKey]] : []; const newModifiers = @@ -226,8 +217,6 @@ function KeyboardWrapper() { (altRight? modifiers['AltRight'] : 0), ].filter(Boolean); - console.log(newModifiers); - // Update current keys and modifiers sendKeyboardEvent(newKeys, [...new Set(newModifiers)]); diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 95d2cd8b..f38d74e9 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -262,9 +262,6 @@ export default function WebRTCVideo() { (e: KeyboardEvent, activeModifiers: number[], mappedKeyModifers: { shift: boolean; altLeft: boolean; altRight: boolean; }) => { const { shiftKey, ctrlKey, altKey, metaKey } = e; - // TODO remove debug logging - console.log(shiftKey + " " +ctrlKey + " " +altKey + " " +metaKey + " " +mappedKeyModifers.shift + " "+mappedKeyModifers.altLeft + " "+mappedKeyModifers.altRight + " ") - const filteredModifiers = activeModifiers.filter(Boolean); // Example: activeModifiers = [0x01, 0x02, 0x04, 0x08] // Assuming 0x01 = ControlLeft, 0x02 = ShiftLeft, 0x04 = AltLeft, 0x08 = MetaLeft @@ -322,10 +319,7 @@ export default function WebRTCVideo() { e.preventDefault(); const prev = useHidStore.getState(); const code = e.code; - console.log("MAPPING ENABLED: " + settings.keyboardMappingEnabled) var localisedKey = settings.keyboardMappingEnabled ? e.key : code; - console.log(e); - console.log("Localised Key: " + localisedKey); // if (document.activeElement?.id !== "videoFocusTrap") {hH // console.log("KEYUP: Not focusing on the video", document.activeElement); @@ -346,15 +340,9 @@ export default function WebRTCVideo() { const { key: mappedKey, shift, altLeft, altRight } = chars[localisedKey] ?? { key: code }; //if (!key) continue; - console.log("Mapped Key: " + mappedKey) - console.log("Current KB Layout:" + useKeyboardMappingsStore.getLayout()); - console.log(chars[localisedKey]); - - console.log("Shift: " + shift + ", altLeft: " + altLeft + ", altRight: " + altRight) // Add the mapped key to keyState activeKeyState.current.set(e.code, { mappedKey, modifiers: {shift, altLeft, altRight}}); - console.log(activeKeyState) // Add the key to the active keys const newKeys = [...prev.activeKeys, keys[mappedKey]].filter(Boolean); @@ -401,7 +389,6 @@ export default function WebRTCVideo() { const keyUpHandler = useCallback( (e: KeyboardEvent) => { e.preventDefault(); - console.log(e) const prev = useHidStore.getState(); setIsNumLockActive(e.getModifierState("NumLock")); @@ -421,7 +408,6 @@ export default function WebRTCVideo() { // Handle modifier release if (isModifierKey) { - console.log("ITS A MODIFER") // Update all affected keys when this modifier is released activeKeyState.current.forEach((value, code) => { const { mappedKey, modifiers: mappedModifiers} = value; @@ -457,14 +443,11 @@ export default function WebRTCVideo() { .filter(Boolean); }; }); - console.log("prev.activemodifers: " + prev.activeModifiers) - console.log("prev.activemodifers.filtered: " + prev.activeModifiers.filter(k => k !== modifiers[e.code])) const newModifiers = handleModifierKeys( e, prev.activeModifiers.filter(k => k !== modifiers[e.code]), {shift: false, altLeft: false, altRight: false} ); - console.log("New modifiers in keyup: " + newModifiers) // Update the keyState /*activeKeyState.current.delete(code);/*.set(code, { @@ -499,7 +482,6 @@ export default function WebRTCVideo() { // Filter out the key that was just released newKeys = newKeys.filter(k => k !== keys[mappedKey]).filter(Boolean); - console.log(activeKeyState) // Filter out the associated modifier //const newModifiers = prev.activeModifiers.filter(k => k !== modifier).filter(Boolean); @@ -533,7 +515,6 @@ export default function WebRTCVideo() { ); */ - console.log(e.key); sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); }, [ diff --git a/ui/src/components/popovers/PasteModal.tsx b/ui/src/components/popovers/PasteModal.tsx index fbdab045..e17dd45a 100644 --- a/ui/src/components/popovers/PasteModal.tsx +++ b/ui/src/components/popovers/PasteModal.tsx @@ -9,7 +9,6 @@ import { TextAreaWithLabel } from "@components/TextArea"; import { SettingsPageHeader } from "@components/SettingsPageheader"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import { useHidStore, useRTCStore, useUiStore, useKeyboardMappingsStore } from "@/hooks/stores"; -import { chars, keys, modifiers } from "@/keyboardMappings"; import notifications from "@/notifications"; const hidKeyboardPayload = (keys: number[], modifier: number) => { diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 9fcd0749..5294a5d3 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -497,8 +497,6 @@ export const useHidStore = create(set => ({ activeKeys: [], activeModifiers: [], updateActiveKeysAndModifiers: ({ keys, modifiers }) => { - // TODO remove debug logs - console.log("keys: " + keys + "modifiers: " + modifiers) return set({ activeKeys: keys, activeModifiers: modifiers }); }, diff --git a/ui/src/hooks/useKeyboard.ts b/ui/src/hooks/useKeyboard.ts index 0ce1eefc..acc25c8f 100644 --- a/ui/src/hooks/useKeyboard.ts +++ b/ui/src/hooks/useKeyboard.ts @@ -1,8 +1,8 @@ -import { useCallback } from "react"; +import { useCallback, useState, useEffect } from "react"; import { useHidStore, useRTCStore } from "@/hooks/stores"; import { useJsonRpc } from "@/hooks/useJsonRpc"; -import { keys, modifiers } from "@/keyboardMappings"; +import { useKeyboardMappingsStore } from "@/hooks/stores"; export default function useKeyboard() { const [send] = useJsonRpc(); @@ -12,6 +12,17 @@ export default function useKeyboard() { state => state.updateActiveKeysAndModifiers, ); + const [keys, setKeys] = useState(useKeyboardMappingsStore.keys); + const [modifiers, setModifiers] = useState(useKeyboardMappingsStore.modifiers); + + useEffect(() => { + const unsubscribeKeyboardStore = useKeyboardMappingsStore.subscribe(() => { + setKeys(useKeyboardMappingsStore.keys); + setModifiers(useKeyboardMappingsStore.modifiers); + }); + return unsubscribeKeyboardStore; // Cleanup on unmount + }, []); + const sendKeyboardEvent = useCallback( (keys: number[], modifiers: number[]) => { if (rpcDataChannel?.readyState !== "open") return; diff --git a/ui/src/keyboardMappings/KeyboardLayouts.ts b/ui/src/keyboardMappings/KeyboardLayouts.ts index 920eeb4f..e5e0ad72 100644 --- a/ui/src/keyboardMappings/KeyboardLayouts.ts +++ b/ui/src/keyboardMappings/KeyboardLayouts.ts @@ -2,7 +2,6 @@ import { keysUKApple, charsUKApple, modifiersUKApple } from './layouts/uk_apple' import { keysUK, charsUK, modifiersUK } from './layouts/uk'; import { keysUS, charsUS, modifiersUS } from './layouts/us'; import { keysDE_T1, charsDE_T1, modifiersDE_T1 } from './layouts/de_t1'; -import { keysES, charsES, modifiersES } from './layouts/es'; export function getKeyboardMappings(layout: string) { switch (layout) { @@ -24,12 +23,6 @@ export function getKeyboardMappings(layout: string) { chars: charsDE_T1, modifiers: modifiersDE_T1, }; - case "es-ES": - return { - keys: keysES, - chars: charsES, - modifiers: modifiersES, - }; case "en-US": default: return { @@ -38,4 +31,188 @@ export function getKeyboardMappings(layout: string) { modifiers: modifiersUS, }; } -} \ No newline at end of file +} + +export const modifierDisplayMap: Record = { + ControlLeft: "Left Ctrl", + ControlRight: "Right Ctrl", + ShiftLeft: "Left Shift", + ShiftRight: "Right Shift", + AltLeft: "Left Alt", + AltRight: "Right Alt", + MetaLeft: "Left Meta", + MetaRight: "Right Meta", +} as Record; + +export const keyDisplayMap: Record = { + CtrlAltDelete: "Ctrl + Alt + Delete", + AltMetaEscape: "Alt + Meta + Escape", + Escape: "esc", + Tab: "tab", + Backspace: "backspace", + Enter: "enter", + CapsLock: "caps lock", + ShiftLeft: "shift", + ShiftRight: "shift", + ControlLeft: "ctrl", + AltLeft: "alt", + AltRight: "alt", + MetaLeft: "meta", + MetaRight: "meta", + Space: " ", + Home: "home", + PageUp: "pageup", + Delete: "delete", + End: "end", + PageDown: "pagedown", + ArrowLeft: "←", + ArrowRight: "→", + ArrowUp: "↑", + ArrowDown: "↓", + + // Letters + KeyA: "a", KeyB: "b", KeyC: "c", KeyD: "d", KeyE: "e", + KeyF: "f", KeyG: "g", KeyH: "h", KeyI: "i", KeyJ: "j", + KeyK: "k", KeyL: "l", KeyM: "m", KeyN: "n", KeyO: "o", + KeyP: "p", KeyQ: "q", KeyR: "r", KeyS: "s", KeyT: "t", + KeyU: "u", KeyV: "v", KeyW: "w", KeyX: "x", KeyY: "y", + KeyZ: "z", + + // Numbers + Digit1: "1", Digit2: "2", Digit3: "3", Digit4: "4", Digit5: "5", + Digit6: "6", Digit7: "7", Digit8: "8", Digit9: "9", Digit0: "0", + + // Symbols + Minus: "-", + Equal: "=", + BracketLeft: "[", + BracketRight: "]", + Backslash: "\\", + Semicolon: ";", + Quote: "'", + Comma: ",", + Period: ".", + Slash: "/", + Backquote: "`", + IntlBackslash: "\\", + + // Function keys + F1: "F1", F2: "F2", F3: "F3", F4: "F4", + F5: "F5", F6: "F6", F7: "F7", F8: "F8", + F9: "F9", F10: "F10", F11: "F11", F12: "F12", + + // Numpad + Numpad0: "Num 0", Numpad1: "Num 1", Numpad2: "Num 2", + Numpad3: "Num 3", Numpad4: "Num 4", Numpad5: "Num 5", + Numpad6: "Num 6", Numpad7: "Num 7", Numpad8: "Num 8", + Numpad9: "Num 9", NumpadAdd: "Num +", NumpadSubtract: "Num -", + NumpadMultiply: "Num *", NumpadDivide: "Num /", NumpadDecimal: "Num .", + NumpadEnter: "Num Enter", + + // Mappings for Keyboard Layout Mapping + "q": "q", + "w": "w", + "e": "e", + "r": "r", + "t": "t", + "y": "y", + "u": "u", + "i": "i", + "o": "o", + "p": "p", + "a": "a", + "s": "s", + "d": "d", + "f": "f", + "g": "g", + "h": "h", + "j": "j", + "k": "k", + "l": "l", + "z": "z", + "x": "x", + "c": "c", + "v": "v", + "b": "b", + "n": "n", + "m": "m", + + "Q": "Q", + "W": "W", + "E": "E", + "R": "R", + "T": "T", + "Y": "Y", + "U": "U", + "I": "I", + "O": "O", + "P": "P", + "A": "A", + "S": "S", + "D": "D", + "F": "F", + "G": "G", + "H": "H", + "J": "J", + "K": "K", + "L": "L", + "Z": "Z", + "X": "X", + "C": "C", + "V": "V", + "B": "B", + "N": "N", + "M": "M", + + "1": "1", + "2": "2", + "3": "3", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", + "9": "9", + "0": "0", + + "!": "!", + "@": "@", + "#": "#", + "$": "$", + "%": "%", + "^": "^", + "&": "&", + "*": "*", + "(": "(", + ")": ")", + + "-": "-", + "_": "_", + + "[": "[", + "]": "]", + "{": "{", + "}": "}", + + "|": "|", + + ";": ";", + ":": ":", + + "'": "'", + "\"": "\"", + + ",": ",", + "<": "<", + + ".": ".", + ">": ">", + + "/": "/", + "?": "?", + + "`": "`", + "~": "~", + + "\\": "\\" +}; \ No newline at end of file diff --git a/ui/src/keyboardMappings/layouts/es.ts b/ui/src/keyboardMappings/layouts/es.ts deleted file mode 100644 index ada24f71..00000000 --- a/ui/src/keyboardMappings/layouts/es.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { charsUS, keysUS, modifiersUS } from "./us"; - -export const keysES = { - ...keysUS, -} as Record; - -export const charsES = { - ...charsUS, - - "ñ": { key: "Semicolon", shift: false }, - "Ñ": { key: "Semicolon", shift: true }, - - "º": { key: "Backquote", shift: false }, - "ª": { key: "Backquote", shift: true }, - - "¡": { key: "Equals", shift: false}, - - "¿": { key: "Slash", shift: false, altRight: true }, - "?": { key: "Slash", shift: true }, - - "|": { key: "Digit1", shift: false, altRight: true }, - - "@": { key: "Digit2", shift: false, altRight: true }, - "\"": { key: "Digit2", shift: true }, - - "·": { key: "Digit3", shift: false, altRight: true }, - "#": { key: "Digit3", shift: true }, - - "$": { key: "Digit4", shift: true }, - "€": { key: "Digit5", shift: false, altRight: true }, - - "&": { key: "Digit6", shift: true }, - - "/": { key: "Digit7", shift: true }, - "(": { key: "Digit8", shift: true }, - ")": { key: "Digit9", shift: true }, - "=": { key: "Digit0", shift: true }, - - "'": { key: "Quote", shift: false }, - "?": { key: "Quote", shift: true }, - - "-": { key: "Minus", shift: false }, - "_": { key: "Minus", shift: true }, - - "`": { key: "IntlBackslash", shift: false }, - "^": { key: "IntlBackslash", shift: true }, - "[": { key: "IntlBackslash", shift: false, altRight: true }, - "{": { key: "IntlBackslash", shift: true, altRight: true }, - - "+": { key: "Equal", shift: true }, - "]": { key: "Equal", shift: false, altRight: true }, - "}": { key: "Equal", shift: true, altRight: true }, - - "<": { key: "Backslash", shift: false }, - ">": { key: "Backslash", shift: true }, - - - ",": { key: "Comma", shift: false }, - ";": { key: "Comma", shift: true }, - - ".": { key: "Period", shift: false }, - ":": { key: "Period", shift: true }, - -} as Record; - -export const modifiersES = { - ...modifiersUS, -} as Record; \ No newline at end of file diff --git a/ui/src/routes/devices.$id.settings.macros.tsx b/ui/src/routes/devices.$id.settings.macros.tsx index f809f57c..a48db1ff 100644 --- a/ui/src/routes/devices.$id.settings.macros.tsx +++ b/ui/src/routes/devices.$id.settings.macros.tsx @@ -8,7 +8,7 @@ import { Button } from "@/components/Button"; import EmptyCard from "@/components/EmptyCard"; import Card from "@/components/Card"; import { MAX_TOTAL_MACROS, COPY_SUFFIX, DEFAULT_DELAY } from "@/constants/macros"; -import { keyDisplayMap, modifierDisplayMap } from "@/keyboardMappings"; +import { keyDisplayMap, modifierDisplayMap } from "@/keyboardMappings/KeyboardLayouts"; import notifications from "@/notifications"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import LoadingSpinner from "@/components/LoadingSpinner"; @@ -27,6 +27,7 @@ export default function SettingsMacrosRoute() { const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [macroToDelete, setMacroToDelete] = useState(null); + const isMaxMacrosReached = useMemo(() => macros.length >= MAX_TOTAL_MACROS, [macros.length] diff --git a/ui/src/routes/devices.$id.settings.mouse.tsx b/ui/src/routes/devices.$id.settings.mouse.tsx index d6223d01..9ff2e541 100644 --- a/ui/src/routes/devices.$id.settings.mouse.tsx +++ b/ui/src/routes/devices.$id.settings.mouse.tsx @@ -5,7 +5,7 @@ import MouseIcon from "@/assets/mouse-icon.svg"; import PointingFinger from "@/assets/pointing-finger.svg"; import { GridCard } from "@/components/Card"; import { Checkbox } from "@/components/Checkbox"; -import { useDeviceSettingsStore, useSettingsStore } from "@/hooks/stores"; +import { useDeviceSettingsStore, useSettingsStore, useKeyboardMappingsStore } from "@/hooks/stores"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import notifications from "@/notifications"; import { SettingsPageHeader } from "@components/SettingsPageheader"; @@ -36,6 +36,39 @@ export default function SettingsKeyboardMouseRoute() { const [send] = useJsonRpc(); + const [keyboardLayout, setKeyboardLayout] = useState("en-US"); + const [kbMappingEnabled, setKeyboardMapping] = useState(false); + + const keyboardMappingEnabled = useSettingsStore(state => state.keyboardMappingEnabled); + const setkeyboardMappingEnabled = useSettingsStore(state => state.setkeyboardMappingEnabled); + + const handleKeyboardLayoutChange = (keyboardLayout: string) => { + send("setKeyboardLayout", { kbLayout: keyboardLayout }, resp => { + if ("error" in resp) { + notifications.error( + `Failed to set keyboard layout: ${resp.error.data || "Unknown error"}`, + ); + return; + } + useKeyboardMappingsStore.setLayout(keyboardLayout) + setKeyboardLayout(keyboardLayout); + }); + }; + + const handleKeyboardMappingChange = (enabled: boolean) => { + send("setKeyboardMappingState", { enabled }, resp => { + if ("error" in resp) { + notifications.error( + `Failed to set keyboard maping state state: ${resp.error.data || "Unknown error"}`, + ); + return; + } + setkeyboardMappingEnabled(enabled); + useKeyboardMappingsStore.setMappingsState(enabled); + setKeyboardMapping(enabled); + }); + }; + useEffect(() => { send("getJigglerState", {}, resp => { if ("error" in resp) return; @@ -48,7 +81,21 @@ export default function SettingsKeyboardMouseRoute() { setScrollSensitivity(resp.result as ScrollSensitivity); }); } - }, [isScrollSensitivityEnabled, send, setScrollSensitivity]); + + send("getKeyboardLayout", {}, resp => { + if ("error" in resp) return; + setKeyboardLayout(String(resp.result)); + useKeyboardMappingsStore.setLayout(String(resp.result)) + }); + + send("getKeyboardMappingState", {}, resp => { + if ("error" in resp) return; + setKeyboardMapping(resp.result as boolean); + setkeyboardMappingEnabled(resp.result as boolean); + useKeyboardMappingsStore.setMappingsState(resp.result as boolean); + }); + + }, [isScrollSensitivityEnabled, send, setScrollSensitivity, setkeyboardMappingEnabled, keyboardMappingEnabled, keyboardLayout, setKeyboardLayout]); const handleJigglerChange = (enabled: boolean) => { send("setJigglerState", { enabled }, resp => { @@ -78,6 +125,7 @@ export default function SettingsKeyboardMouseRoute() { [send, setScrollSensitivity], ); + return (
+
+ +
+ + { + handleKeyboardMappingChange(e.target.checked); + }} + /> + + + handleKeyboardLayoutChange(e.target.value)} + /> + +
+
); } diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx index c0b41817..07eb25ea 100644 --- a/ui/src/routes/devices.$id.settings.tsx +++ b/ui/src/routes/devices.$id.settings.tsx @@ -148,7 +148,7 @@ export default function SettingsRoute() { >
-

Mouse

+

Mouse & Keyboard