diff --git a/config.go b/config.go index cf096a7..f5111a3 100644 --- a/config.go +++ b/config.go @@ -73,41 +73,45 @@ func (m *KeyboardMacro) Validate() error { } type Config struct { - CloudURL string `json:"cloud_url"` - CloudAppURL string `json:"cloud_app_url"` - CloudToken string `json:"cloud_token"` - GoogleIdentity string `json:"google_identity"` - JigglerEnabled bool `json:"jiggler_enabled"` - AutoUpdateEnabled bool `json:"auto_update_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"` - KeyboardMacros []KeyboardMacro `json:"keyboard_macros"` - EdidString string `json:"hdmi_edid_string"` - ActiveExtension string `json:"active_extension"` - DisplayMaxBrightness int `json:"display_max_brightness"` - DisplayDimAfterSec int `json:"display_dim_after_sec"` - DisplayOffAfterSec int `json:"display_off_after_sec"` - TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", "" - UsbConfig *usbgadget.Config `json:"usb_config"` - UsbDevices *usbgadget.Devices `json:"usb_devices"` - DefaultLogLevel string `json:"default_log_level"` + CloudURL string `json:"cloud_url"` + CloudAppURL string `json:"cloud_app_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"` + KeyboardMacros []KeyboardMacro `json:"keyboard_macros"` + EdidString string `json:"hdmi_edid_string"` + ActiveExtension string `json:"active_extension"` + DisplayMaxBrightness int `json:"display_max_brightness"` + DisplayDimAfterSec int `json:"display_dim_after_sec"` + DisplayOffAfterSec int `json:"display_off_after_sec"` + TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", "" + UsbConfig *usbgadget.Config `json:"usb_config"` + UsbDevices *usbgadget.Devices `json:"usb_devices"` + DefaultLogLevel string `json:"default_log_level"` } const configPath = "/userdata/kvm_config.json" var defaultConfig = &Config{ - CloudURL: "https://api.jetkvm.com", - CloudAppURL: "https://app.jetkvm.com", - AutoUpdateEnabled: true, // Set a default value - ActiveExtension: "", - KeyboardMacros: []KeyboardMacro{}, - DisplayMaxBrightness: 64, - DisplayDimAfterSec: 120, // 2 minutes - DisplayOffAfterSec: 1800, // 30 minutes - TLSMode: "", + CloudURL: "https://api.jetkvm.com", + CloudAppURL: "https://app.jetkvm.com", + AutoUpdateEnabled: true, // Set a default value + KeyboardLayout: "en-US", + KeyboardMappingEnabled: false, + ActiveExtension: "", + KeyboardMacros: []KeyboardMacro{}, + DisplayMaxBrightness: 64, + DisplayDimAfterSec: 120, // 2 minutes + DisplayOffAfterSec: 1800, // 30 minutes + TLSMode: "", UsbConfig: &usbgadget.Config{ VendorId: "0x1d6b", //The Linux Foundation ProductId: "0x0104", //Multifunction Composite Gadget diff --git a/dev_deploy.sh b/dev_deploy.sh index 02bbb24..dc18c16 100755 --- a/dev_deploy.sh +++ b/dev_deploy.sh @@ -83,6 +83,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/jsonrpc.go b/jsonrpc.go index 248390e..26636a7 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -164,6 +164,30 @@ 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 +} + +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 +} + func rpcReboot(force bool) error { logger.Info().Msg("Got reboot request from JSONRPC, rebooting...") @@ -957,73 +981,77 @@ func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) { } var rpcHandlers = map[string]RPCHandler{ - "ping": {Func: rpcPing}, - "reboot": {Func: rpcReboot, Params: []string{"force"}}, - "getDeviceID": {Func: rpcGetDeviceID}, - "deregisterDevice": {Func: rpcDeregisterDevice}, - "getCloudState": {Func: rpcGetCloudState}, - "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, - "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, - "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "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"}}, - "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"}}, - "getTLSState": {Func: rpcGetTLSState}, - "setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}}, - "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, - "getMassStorageMode": {Func: rpcGetMassStorageMode}, - "isUpdatePending": {Func: rpcIsUpdatePending}, - "getUsbEmulationState": {Func: rpcGetUsbEmulationState}, - "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, - "getUsbConfig": {Func: rpcGetUsbConfig}, - "setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}}, - "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}, - "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}}, - "getBacklightSettings": {Func: rpcGetBacklightSettings}, - "getDCPowerState": {Func: rpcGetDCPowerState}, - "setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}}, - "getActiveExtension": {Func: rpcGetActiveExtension}, - "setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}}, - "getATXState": {Func: rpcGetATXState}, - "setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}}, - "getSerialSettings": {Func: rpcGetSerialSettings}, - "setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}}, - "getUsbDevices": {Func: rpcGetUsbDevices}, - "setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}}, - "setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}}, - "setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}}, - "getScrollSensitivity": {Func: rpcGetScrollSensitivity}, - "setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}}, - "getKeyboardMacros": {Func: getKeyboardMacros}, - "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, + "ping": {Func: rpcPing}, + "reboot": {Func: rpcReboot, Params: []string{"force"}}, + "getDeviceID": {Func: rpcGetDeviceID}, + "deregisterDevice": {Func: rpcDeregisterDevice}, + "getCloudState": {Func: rpcGetCloudState}, + "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, + "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, + "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "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"}}, + "getTLSState": {Func: rpcGetTLSState}, + "setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}}, + "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, + "getMassStorageMode": {Func: rpcGetMassStorageMode}, + "isUpdatePending": {Func: rpcIsUpdatePending}, + "getUsbEmulationState": {Func: rpcGetUsbEmulationState}, + "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, + "getUsbConfig": {Func: rpcGetUsbConfig}, + "setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}}, + "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}, + "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}}, + "getBacklightSettings": {Func: rpcGetBacklightSettings}, + "getDCPowerState": {Func: rpcGetDCPowerState}, + "setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}}, + "getActiveExtension": {Func: rpcGetActiveExtension}, + "setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}}, + "getATXState": {Func: rpcGetATXState}, + "setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}}, + "getSerialSettings": {Func: rpcGetSerialSettings}, + "setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}}, + "getUsbDevices": {Func: rpcGetUsbDevices}, + "setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}}, + "setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}}, + "setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}}, + "getScrollSensitivity": {Func: rpcGetScrollSensitivity}, + "setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}}, + "getKeyboardMacros": {Func: getKeyboardMacros}, + "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, } diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx index aa00da7..d70d590 100644 --- a/ui/src/components/InfoBar.tsx +++ b/ui/src/components/InfoBar.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { cx } from "@/cva.config"; import { @@ -7,10 +7,21 @@ import { useRTCStore, useSettingsStore, useVideoStore, + useKeyboardMappingsStore, } from "@/hooks/stores"; -import { keys, modifiers } from "@/keyboardMappings"; export default function InfoBar() { + 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 activeKeys = useHidStore(state => state.activeKeys); const activeModifiers = useHidStore(state => state.activeModifiers); const mouseX = useMouseStore(state => state.mouseX); diff --git a/ui/src/components/MacroStepCard.tsx b/ui/src/components/MacroStepCard.tsx index 8642c28..61ba2bc 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/SelectMenuBasic.tsx b/ui/src/components/SelectMenuBasic.tsx index c518bfe..066603a 100644 --- a/ui/src/components/SelectMenuBasic.tsx +++ b/ui/src/components/SelectMenuBasic.tsx @@ -27,6 +27,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/VirtualKeyboard.tsx b/ui/src/components/VirtualKeyboard.tsx index 09a94a6..da302db 100644 --- a/ui/src/components/VirtualKeyboard.tsx +++ b/ui/src/components/VirtualKeyboard.tsx @@ -9,9 +9,9 @@ import { Button } from "@components/Button"; import "react-simple-keyboard/build/css/index.css"; -import { useHidStore, useUiStore } from "@/hooks/stores"; +import { useHidStore, useUiStore, useKeyboardMappingsStore } from "@/hooks/stores"; import { cx } from "@/cva.config"; -import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings"; +import { keyDisplayMap } from "@/keyboardMappings/KeyboardLayouts"; import useKeyboard from "@/hooks/useKeyboard"; import DetachIconRaw from "@/assets/detach-icon.svg"; import AttachIconRaw from "@/assets/attach-icon.svg"; @@ -25,7 +25,40 @@ const AttachIcon = ({ className }: { className?: string }) => { }; function KeyboardWrapper() { - const [layoutName, setLayoutName] = useState("default"); + 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); + 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( @@ -112,16 +145,25 @@ 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; // 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") { @@ -143,10 +185,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; @@ -155,25 +204,30 @@ function KeyboardWrapper() { // Handle caps lock state change if (isKeyCaps) { + toggleLayout(); setIsCapsLockActive(!isCapsLockActive); } // 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); // 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); @@ -280,6 +334,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} @@ -332,4 +405,4 @@ function KeyboardWrapper() { ); } -export default KeyboardWrapper; +export default KeyboardWrapper; \ No newline at end of file diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index be69899..f38d74e 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -7,8 +7,8 @@ import { useRTCStore, useSettingsStore, useVideoStore, + useKeyboardMappingsStore, } from "@/hooks/stores"; -import { keys, modifiers } from "@/keyboardMappings"; import { useResizeObserver } from "@/hooks/useResizeObserver"; import { cx } from "@/cva.config"; import VirtualKeyboard from "@components/VirtualKeyboard"; @@ -24,7 +24,30 @@ import { NoAutoplayPermissionsOverlay, } 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. +// 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); + const [modifiers, setModifiers] = useState(useKeyboardMappingsStore.modifiers); + + // This map is used to maintain consistency between localised key mappings + const activeKeyState = useRef>(new Map()); + + useEffect(() => { + const unsubscribeKeyboardStore = useKeyboardMappingsStore.subscribe(() => { + setKeys(useKeyboardMappingsStore.keys); + setChars(useKeyboardMappingsStore.chars); + setModifiers(useKeyboardMappingsStore.modifiers); + }); + return unsubscribeKeyboardStore; // Cleanup on unmount + }, []); + // Video and stream related refs and states const videoElm = useRef(null); const mediaStream = useRTCStore(state => state.mediaStream); @@ -233,13 +256,13 @@ export default function WebRTCVideo() { sendAbsMouseMovement(0, 0, 0); }, [sendAbsMouseMovement]); + // 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); - // Example: activeModifiers = [0x01, 0x02, 0x04, 0x08] // Assuming 0x01 = ControlLeft, 0x02 = ShiftLeft, 0x04 = AltLeft, 0x08 = MetaLeft return ( @@ -250,6 +273,7 @@ export default function WebRTCVideo() { .filter( modifier => shiftKey || + mappedKeyModifers.shift || (modifier !== modifiers["ShiftLeft"] && modifier !== modifiers["ShiftRight"]), ) @@ -268,7 +292,14 @@ export default function WebRTCVideo() { .filter( modifier => altKey || - (modifier !== modifiers["AltLeft"] && modifier !== modifiers["AltRight"]), + mappedKeyModifers.altLeft || + (modifier !== modifiers["AltLeft"]), + ) + .filter( + modifier => + altKey || + mappedKeyModifers.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 @@ -287,33 +318,45 @@ export default function WebRTCVideo() { async (e: KeyboardEvent) => { e.preventDefault(); const prev = useHidStore.getState(); - let code = e.code; - const key = e.key; + const code = e.code; + var localisedKey = settings.keyboardMappingEnabled ? e.key : code; - // 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")); 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: code }; + //if (!key) continue; + + // Add the mapped key to keyState + activeKeyState.current.set(e.code, { mappedKey, modifiers: {shift, altLeft, altRight}}); // Add the key to the active keys - const newKeys = [...prev.activeKeys, keys[code]].filter(Boolean); + 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], - ]); + (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 @@ -323,6 +366,8 @@ export default function WebRTCVideo() { setTimeout(() => { const prev = useHidStore.getState(); sendKeyboardEvent([], newModifiers || prev.activeModifiers); + activeKeyState.current.delete("MetaLeft"); + activeKeyState.current.delete("MetaRight"); }, 10); } @@ -334,6 +379,10 @@ export default function WebRTCVideo() { setIsScrollLockActive, handleModifierKeys, sendKeyboardEvent, + chars, + keys, + modifiers, + settings, ], ); @@ -346,14 +395,125 @@ 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" || + e.code === "ControlRight"; + + var newKeys = prev.activeKeys; + + // 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; + }; + + if (removeCurrentKey) { + 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 = handleModifierKeys( + e, + prev.activeModifiers.filter(k => k !== modifiers[e.code]), + {shift: false, altLeft: false, altRight: false} + ); + + // 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); + + // 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 + } + + // 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 + newKeys = newKeys.filter(k => k !== keys[mappedKey]).filter(Boolean); + + // Filter out the associated modifier + //const newModifiers = prev.activeModifiers.filter(k => k !== modifier).filter(Boolean); + const newModifiers = handleModifierKeys( + e, + 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 }; + //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]), ); + */ sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); }, @@ -363,6 +523,9 @@ export default function WebRTCVideo() { setIsScrollLockActive, handleModifierKeys, sendKeyboardEvent, + chars, + keys, + modifiers, ], ); @@ -444,6 +607,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 643f55b..e17dd45 100644 --- a/ui/src/components/popovers/PasteModal.tsx +++ b/ui/src/components/popovers/PasteModal.tsx @@ -8,8 +8,7 @@ import { GridCard } from "@components/Card"; import { TextAreaWithLabel } from "@components/TextArea"; import { SettingsPageHeader } from "@components/SettingsPageheader"; import { useJsonRpc } from "@/hooks/useJsonRpc"; -import { useHidStore, useRTCStore, useUiStore } from "@/hooks/stores"; -import { chars, keys, modifiers } from "@/keyboardMappings"; +import { useHidStore, useRTCStore, useUiStore, useKeyboardMappingsStore } from "@/hooks/stores"; import notifications from "@/notifications"; const hidKeyboardPayload = (keys: number[], modifier: number) => { @@ -17,6 +16,19 @@ const hidKeyboardPayload = (keys: number[], modifier: number) => { }; export default function PasteModal() { + 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); + setChars(useKeyboardMappingsStore.chars); + setModifiers(useKeyboardMappingsStore.modifiers); + }); + return unsubscribeKeyboardStore; // Cleanup on unmount + }, []); + const TextAreaRef = useRef(null); const setPasteMode = useHidStore(state => state.setPasteModeEnabled); const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); @@ -42,13 +54,19 @@ export default function PasteModal() { try { for (const char of text) { - const { key, shift } = chars[char] ?? {}; + const { key, shift, altLeft, altRight } = chars[char] ?? {}; if (!key) continue; + // 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 + 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 => { @@ -63,7 +81,7 @@ export default function PasteModal() { console.error(error); notifications.error("Failed to paste text"); } - }, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode]); + }, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, chars, keys, modifiers]); useEffect(() => { if (TextAreaRef.current) { @@ -125,9 +143,12 @@ 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(", ")} + + Tip: You can set your desired keyboard layout in settings, and remember to enable keyboard mapping. +
)} diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 0fa4121..5294a5d 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"; import { MAX_STEPS_PER_MACRO, MAX_TOTAL_MACROS, MAX_KEYS_PER_STEP } from "@/constants/macros"; // Define the JsonRpc types for better type checking @@ -285,6 +286,9 @@ interface SettingsState { mouseMode: string; setMouseMode: (mode: string) => void; + keyboardMappingEnabled: boolean; + setkeyboardMappingEnabled: (enabled: boolean) => void; + debugMode: boolean; setDebugMode: (enabled: boolean) => void; @@ -299,6 +303,9 @@ interface SettingsState { export const useSettingsStore = create( persist( set => ({ + keyboardMappingEnabled: false, + setkeyboardMappingEnabled: enabled => set({keyboardMappingEnabled: enabled}), + isCursorHidden: false, setCursorVisibility: enabled => set({ isCursorHidden: enabled }), @@ -631,6 +638,69 @@ export const useUsbConfigModalStore = create(set => ({ setErrorMessage: message => set({ errorMessage: message }), })); +class KeyboardMappingsStore { + private _layout: string = 'en-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.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(); + } + + getMappingState() { + return this._mappingsEnabled; + } + + 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(); interface LocalAuthModalState { modalView: | "createPassword" @@ -804,4 +874,4 @@ export const useMacrosStore = create((set, get) => ({ set({ loading: false }); } } -})); \ No newline at end of file +})); diff --git a/ui/src/hooks/useKeyboard.ts b/ui/src/hooks/useKeyboard.ts index 0ce1eef..acc25c8 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.ts b/ui/src/keyboardMappings.ts deleted file mode 100644 index 347939a..0000000 --- a/ui/src/keyboardMappings.ts +++ /dev/null @@ -1,291 +0,0 @@ -export const keys = { - 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 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; - -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" -}; diff --git a/ui/src/keyboardMappings/KeyboardLayouts.ts b/ui/src/keyboardMappings/KeyboardLayouts.ts new file mode 100644 index 0000000..e5e0ad7 --- /dev/null +++ b/ui/src/keyboardMappings/KeyboardLayouts.ts @@ -0,0 +1,218 @@ +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) { + case "en-GB_apple": + return { + keys: keysUKApple, + chars: charsUKApple, + modifiers: modifiersUKApple, + }; + case "en-GB": + return { + keys: keysUK, + chars: charsUK, + modifiers: modifiersUK, + }; + case "de-DE": + return { + keys: keysDE_T1, + chars: charsDE_T1, + modifiers: modifiersDE_T1, + }; + case "en-US": + default: + return { + keys: keysUS, + chars: charsUS, + modifiers: modifiersUS, + }; + } +} + +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/de_t1.ts b/ui/src/keyboardMappings/layouts/de_t1.ts new file mode 100644 index 0000000..8fd7bfa --- /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 new file mode 100644 index 0000000..f64979f --- /dev/null +++ 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 new file mode 100644 index 0000000..fef400f --- /dev/null +++ b/ui/src/keyboardMappings/layouts/uk_apple.ts @@ -0,0 +1,25 @@ +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, altLeft: true }, + "£": { 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 +export const modifiersUKApple = { + ...modifiersUS, +} as Record; \ 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 0000000..a5395c2 --- /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 diff --git a/ui/src/routes/devices.$id.settings.macros.tsx b/ui/src/routes/devices.$id.settings.macros.tsx index f809f57..a48db1f 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 d6223d0..9ff2e54 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 c0b4181..07eb25e 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