This commit is contained in:
William Johnstone 2025-04-13 07:52:05 +03:00 committed by GitHub
commit fc3a3388bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1167 additions and 435 deletions

View File

@ -73,41 +73,45 @@ func (m *KeyboardMacro) Validate() error {
} }
type Config struct { type Config struct {
CloudURL string `json:"cloud_url"` CloudURL string `json:"cloud_url"`
CloudAppURL string `json:"cloud_app_url"` CloudAppURL string `json:"cloud_app_url"`
CloudToken string `json:"cloud_token"` CloudToken string `json:"cloud_token"`
GoogleIdentity string `json:"google_identity"` GoogleIdentity string `json:"google_identity"`
JigglerEnabled bool `json:"jiggler_enabled"` JigglerEnabled bool `json:"jiggler_enabled"`
AutoUpdateEnabled bool `json:"auto_update_enabled"` AutoUpdateEnabled bool `json:"auto_update_enabled"`
IncludePreRelease bool `json:"include_pre_release"` KeyboardLayout string `json:"keyboard_layout"`
HashedPassword string `json:"hashed_password"` KeyboardMappingEnabled bool `json:"keyboard_mapping_enabled"`
LocalAuthToken string `json:"local_auth_token"` IncludePreRelease bool `json:"include_pre_release"`
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration HashedPassword string `json:"hashed_password"`
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` LocalAuthToken string `json:"local_auth_token"`
KeyboardMacros []KeyboardMacro `json:"keyboard_macros"` LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
EdidString string `json:"hdmi_edid_string"` WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
ActiveExtension string `json:"active_extension"` KeyboardMacros []KeyboardMacro `json:"keyboard_macros"`
DisplayMaxBrightness int `json:"display_max_brightness"` EdidString string `json:"hdmi_edid_string"`
DisplayDimAfterSec int `json:"display_dim_after_sec"` ActiveExtension string `json:"active_extension"`
DisplayOffAfterSec int `json:"display_off_after_sec"` DisplayMaxBrightness int `json:"display_max_brightness"`
TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", "" DisplayDimAfterSec int `json:"display_dim_after_sec"`
UsbConfig *usbgadget.Config `json:"usb_config"` DisplayOffAfterSec int `json:"display_off_after_sec"`
UsbDevices *usbgadget.Devices `json:"usb_devices"` TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", ""
DefaultLogLevel string `json:"default_log_level"` UsbConfig *usbgadget.Config `json:"usb_config"`
UsbDevices *usbgadget.Devices `json:"usb_devices"`
DefaultLogLevel string `json:"default_log_level"`
} }
const configPath = "/userdata/kvm_config.json" const configPath = "/userdata/kvm_config.json"
var defaultConfig = &Config{ var defaultConfig = &Config{
CloudURL: "https://api.jetkvm.com", CloudURL: "https://api.jetkvm.com",
CloudAppURL: "https://app.jetkvm.com", CloudAppURL: "https://app.jetkvm.com",
AutoUpdateEnabled: true, // Set a default value AutoUpdateEnabled: true, // Set a default value
ActiveExtension: "", KeyboardLayout: "en-US",
KeyboardMacros: []KeyboardMacro{}, KeyboardMappingEnabled: false,
DisplayMaxBrightness: 64, ActiveExtension: "",
DisplayDimAfterSec: 120, // 2 minutes KeyboardMacros: []KeyboardMacro{},
DisplayOffAfterSec: 1800, // 30 minutes DisplayMaxBrightness: 64,
TLSMode: "", DisplayDimAfterSec: 120, // 2 minutes
DisplayOffAfterSec: 1800, // 30 minutes
TLSMode: "",
UsbConfig: &usbgadget.Config{ UsbConfig: &usbgadget.Config{
VendorId: "0x1d6b", //The Linux Foundation VendorId: "0x1d6b", //The Linux Foundation
ProductId: "0x0104", //Multifunction Composite Gadget ProductId: "0x0104", //Multifunction Composite Gadget

View File

@ -83,6 +83,7 @@ export LD_LIBRARY_PATH=/oem/usr/lib:\$LD_LIBRARY_PATH
# Kill any existing instances of the application # Kill any existing instances of the application
killall jetkvm_app || true killall jetkvm_app || true
killall jetkvm_app_debug || true killall jetkvm_app_debug || true
killall jetkvm_native || true
# Navigate to the directory where the binary will be stored # Navigate to the directory where the binary will be stored
cd "${REMOTE_PATH}" cd "${REMOTE_PATH}"

View File

@ -164,6 +164,30 @@ func rpcGetDeviceID() (string, error) {
return GetDeviceID(), nil 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 { func rpcReboot(force bool) error {
logger.Info().Msg("Got reboot request from JSONRPC, rebooting...") logger.Info().Msg("Got reboot request from JSONRPC, rebooting...")
@ -957,73 +981,77 @@ func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) {
} }
var rpcHandlers = map[string]RPCHandler{ var rpcHandlers = map[string]RPCHandler{
"ping": {Func: rpcPing}, "ping": {Func: rpcPing},
"reboot": {Func: rpcReboot, Params: []string{"force"}}, "reboot": {Func: rpcReboot, Params: []string{"force"}},
"getDeviceID": {Func: rpcGetDeviceID}, "getDeviceID": {Func: rpcGetDeviceID},
"deregisterDevice": {Func: rpcDeregisterDevice}, "deregisterDevice": {Func: rpcDeregisterDevice},
"getCloudState": {Func: rpcGetCloudState}, "getCloudState": {Func: rpcGetCloudState},
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},
"getVideoState": {Func: rpcGetVideoState}, "getVideoState": {Func: rpcGetVideoState},
"getUSBState": {Func: rpcGetUSBState}, "getUSBState": {Func: rpcGetUSBState},
"unmountImage": {Func: rpcUnmountImage}, "unmountImage": {Func: rpcUnmountImage},
"rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}}, "rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}},
"setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}}, "setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}},
"getJigglerState": {Func: rpcGetJigglerState}, "getJigglerState": {Func: rpcGetJigglerState},
"sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}}, "sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}},
"getStreamQualityFactor": {Func: rpcGetStreamQualityFactor}, "getKeyboardLayout": {Func: rpcGetKeyboardLayout},
"setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}}, "setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"kbLayout"}},
"getAutoUpdateState": {Func: rpcGetAutoUpdateState}, "setKeyboardMappingState": {Func: rpcSetKeyboardMappingState, Params: []string{"enabled"}},
"setAutoUpdateState": {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}}, "getKeyboardMappingState": {Func: rpcGetKeyboardMappingState},
"getEDID": {Func: rpcGetEDID}, "getStreamQualityFactor": {Func: rpcGetStreamQualityFactor},
"setEDID": {Func: rpcSetEDID, Params: []string{"edid"}}, "setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}},
"getDevChannelState": {Func: rpcGetDevChannelState}, "getAutoUpdateState": {Func: rpcGetAutoUpdateState},
"setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}}, "setAutoUpdateState": {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}},
"getUpdateStatus": {Func: rpcGetUpdateStatus}, "getEDID": {Func: rpcGetEDID},
"tryUpdate": {Func: rpcTryUpdate}, "setEDID": {Func: rpcSetEDID, Params: []string{"edid"}},
"getDevModeState": {Func: rpcGetDevModeState}, "getDevChannelState": {Func: rpcGetDevChannelState},
"setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}}, "setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
"getSSHKeyState": {Func: rpcGetSSHKeyState}, "getUpdateStatus": {Func: rpcGetUpdateStatus},
"setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}}, "tryUpdate": {Func: rpcTryUpdate},
"getTLSState": {Func: rpcGetTLSState}, "getDevModeState": {Func: rpcGetDevModeState},
"setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}}, "setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}},
"setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, "getSSHKeyState": {Func: rpcGetSSHKeyState},
"getMassStorageMode": {Func: rpcGetMassStorageMode}, "setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}},
"isUpdatePending": {Func: rpcIsUpdatePending}, "getTLSState": {Func: rpcGetTLSState},
"getUsbEmulationState": {Func: rpcGetUsbEmulationState}, "setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}},
"setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}},
"getUsbConfig": {Func: rpcGetUsbConfig}, "getMassStorageMode": {Func: rpcGetMassStorageMode},
"setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}}, "isUpdatePending": {Func: rpcIsUpdatePending},
"checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}}, "getUsbEmulationState": {Func: rpcGetUsbEmulationState},
"getVirtualMediaState": {Func: rpcGetVirtualMediaState}, "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}},
"getStorageSpace": {Func: rpcGetStorageSpace}, "getUsbConfig": {Func: rpcGetUsbConfig},
"mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}}, "setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}},
"mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}}, "checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}},
"mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}}, "getVirtualMediaState": {Func: rpcGetVirtualMediaState},
"listStorageFiles": {Func: rpcListStorageFiles}, "getStorageSpace": {Func: rpcGetStorageSpace},
"deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}}, "mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}},
"startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}}, "mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}},
"getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, "mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}},
"setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, "listStorageFiles": {Func: rpcListStorageFiles},
"resetConfig": {Func: rpcResetConfig}, "deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}},
"setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}}, "startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}},
"getBacklightSettings": {Func: rpcGetBacklightSettings}, "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices},
"getDCPowerState": {Func: rpcGetDCPowerState}, "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
"setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}}, "resetConfig": {Func: rpcResetConfig},
"getActiveExtension": {Func: rpcGetActiveExtension}, "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}},
"setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}}, "getBacklightSettings": {Func: rpcGetBacklightSettings},
"getATXState": {Func: rpcGetATXState}, "getDCPowerState": {Func: rpcGetDCPowerState},
"setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}}, "setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}},
"getSerialSettings": {Func: rpcGetSerialSettings}, "getActiveExtension": {Func: rpcGetActiveExtension},
"setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}}, "setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}},
"getUsbDevices": {Func: rpcGetUsbDevices}, "getATXState": {Func: rpcGetATXState},
"setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}}, "setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}},
"setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}}, "getSerialSettings": {Func: rpcGetSerialSettings},
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}}, "setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}},
"getScrollSensitivity": {Func: rpcGetScrollSensitivity}, "getUsbDevices": {Func: rpcGetUsbDevices},
"setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}}, "setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}},
"getKeyboardMacros": {Func: getKeyboardMacros}, "setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, "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"}},
} }

View File

@ -1,4 +1,4 @@
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import { import {
@ -7,10 +7,21 @@ import {
useRTCStore, useRTCStore,
useSettingsStore, useSettingsStore,
useVideoStore, useVideoStore,
useKeyboardMappingsStore,
} from "@/hooks/stores"; } from "@/hooks/stores";
import { keys, modifiers } from "@/keyboardMappings";
export default function InfoBar() { 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 activeKeys = useHidStore(state => state.activeKeys);
const activeModifiers = useHidStore(state => state.activeModifiers); const activeModifiers = useHidStore(state => state.activeModifiers);
const mouseX = useMouseStore(state => state.mouseX); const mouseX = useMouseStore(state => state.mouseX);

View File

@ -4,21 +4,22 @@ import { Button } from "@/components/Button";
import { Combobox } from "@/components/Combobox"; import { Combobox } from "@/components/Combobox";
import { SelectMenuBasic } from "@/components/SelectMenuBasic"; import { SelectMenuBasic } from "@/components/SelectMenuBasic";
import Card from "@/components/Card"; 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 { MAX_KEYS_PER_STEP, DEFAULT_DELAY } from "@/constants/macros";
import FieldLabel from "@/components/FieldLabel"; import FieldLabel from "@/components/FieldLabel";
// Filter out modifier keys since they're handled in the modifiers section // Filter out modifier keys since they're handled in the modifiers section
const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta']; const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta'];
const keyOptions = Object.keys(keys) const keyOptions = Object.keys(keysUS)
.filter(key => !modifierKeyPrefixes.some(prefix => key.startsWith(prefix))) .filter(key => !modifierKeyPrefixes.some(prefix => key.startsWith(prefix)))
.map(key => ({ .map(key => ({
value: key, value: key,
label: keyDisplayMap[key] || key, label: keyDisplayMap[key] || key,
})); }));
const modifierOptions = Object.keys(modifiers).map(modifier => ({ const modifierOptions = Object.keys(modifiersUS).map(modifier => ({
value: modifier, value: modifier,
label: modifier.replace(/^(Control|Alt|Shift|Meta)(Left|Right)$/, "$1 $2"), label: modifier.replace(/^(Control|Alt|Shift|Meta)(Left|Right)$/, "$1 $2"),
})); }));

View File

@ -27,6 +27,7 @@ type SelectMenuProps = Pick<
const sizes = { const sizes = {
XS: "h-[24.5px] pl-3 pr-8 text-xs", XS: "h-[24.5px] pl-3 pr-8 text-xs",
SM: "h-[32px] pl-3 pr-8 text-[13px]", 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", MD: "h-[40px] pl-4 pr-10 text-sm",
LG: "h-[48px] pl-4 pr-10 px-5 text-base", LG: "h-[48px] pl-4 pr-10 px-5 text-base",
}; };

View File

@ -9,9 +9,9 @@ import { Button } from "@components/Button";
import "react-simple-keyboard/build/css/index.css"; 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 { cx } from "@/cva.config";
import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings"; import { keyDisplayMap } from "@/keyboardMappings/KeyboardLayouts";
import useKeyboard from "@/hooks/useKeyboard"; import useKeyboard from "@/hooks/useKeyboard";
import DetachIconRaw from "@/assets/detach-icon.svg"; import DetachIconRaw from "@/assets/detach-icon.svg";
import AttachIconRaw from "@/assets/attach-icon.svg"; import AttachIconRaw from "@/assets/attach-icon.svg";
@ -25,7 +25,40 @@ const AttachIcon = ({ className }: { className?: string }) => {
}; };
function KeyboardWrapper() { 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<HTMLDivElement>(null); const keyboardRef = useRef<HTMLDivElement>(null);
const showAttachedVirtualKeyboard = useUiStore( const showAttachedVirtualKeyboard = useUiStore(
@ -112,16 +145,25 @@ function KeyboardWrapper() {
}; };
}, [endDrag, onDrag, startDrag]); }, [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( const onKeyDown = useCallback(
(key: string) => { (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 isKeyShift = key === "{shift}" || key === "ShiftLeft" || key === "ShiftRight";
const isKeyCaps = key === "CapsLock"; const isKeyCaps = key === "CapsLock";
const cleanKey = key.replace(/[()]/g, ""); const keyHasShiftModifier = (key.includes("(") && key !== "(") || shift;
const keyHasShiftModifier = key.includes("(");
// Handle toggle of layout for shift or caps lock // Handle toggle of layout for shift or caps lock
const toggleLayout = () => { 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") { if (key === "CtrlAltDelete") {
@ -143,10 +185,17 @@ function KeyboardWrapper() {
return; return;
} }
if (isKeyShift || isKeyCaps) { if (isKeyShift || (!(layoutName == "shift" || layoutName == "mappedUpper") && isCapsLockActive)) {
toggleLayout(); toggleLayout();
}
if (isCapsLockActive) { if (layoutName == "shift" || layoutName == "mappedUpper") {
if (!isCapsLockActive) {
toggleLayout();
}
if (isKeyCaps && isCapsLockActive) {
toggleLayout();
setIsCapsLockActive(false); setIsCapsLockActive(false);
sendKeyboardEvent([keys["CapsLock"]], []); sendKeyboardEvent([keys["CapsLock"]], []);
return; return;
@ -155,25 +204,30 @@ function KeyboardWrapper() {
// Handle caps lock state change // Handle caps lock state change
if (isKeyCaps) { if (isKeyCaps) {
toggleLayout();
setIsCapsLockActive(!isCapsLockActive); setIsCapsLockActive(!isCapsLockActive);
} }
// Collect new active keys and modifiers // Collect new active keys and modifiers
const newKeys = keys[cleanKey] ? [keys[cleanKey]] : []; const newKeys = keys[mappedKey ?? cleanKey] ? [keys[mappedKey ?? cleanKey]] : [];
const newModifiers = 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 // 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 shift was used as a modifier and caps lock is not active, revert to default layout
if (keyHasShiftModifier && !isCapsLockActive) { if (keyHasShiftModifier && !isCapsLockActive) {
setLayoutName("default"); setLayoutName(mappingsEnabled ? "mappedLower" : "default");
} }
setTimeout(resetKeyboardState, 100); setTimeout(resetKeyboardState, 100);
}, },
[isCapsLockActive, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive], [isCapsLockActive, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive, mappingsEnabled, chars, keys, modifiers, layoutName],
); );
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled); const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
@ -280,6 +334,25 @@ function KeyboardWrapper() {
"ShiftLeft (KeyZ) (KeyX) (KeyC) (KeyV) (KeyB) (KeyN) (KeyM) (Comma) (Period) (Slash) ShiftRight", "ShiftLeft (KeyZ) (KeyX) (KeyC) (KeyV) (KeyB) (KeyN) (KeyM) (Comma) (Period) (Slash) ShiftRight",
"ControlLeft AltLeft MetaLeft Space MetaRight AltRight", "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} disableButtonHold={true}
mergeDisplay={true} mergeDisplay={true}
@ -332,4 +405,4 @@ function KeyboardWrapper() {
); );
} }
export default KeyboardWrapper; export default KeyboardWrapper;

View File

@ -7,8 +7,8 @@ import {
useRTCStore, useRTCStore,
useSettingsStore, useSettingsStore,
useVideoStore, useVideoStore,
useKeyboardMappingsStore,
} from "@/hooks/stores"; } from "@/hooks/stores";
import { keys, modifiers } from "@/keyboardMappings";
import { useResizeObserver } from "@/hooks/useResizeObserver"; import { useResizeObserver } from "@/hooks/useResizeObserver";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import VirtualKeyboard from "@components/VirtualKeyboard"; import VirtualKeyboard from "@components/VirtualKeyboard";
@ -24,7 +24,30 @@ import {
NoAutoplayPermissionsOverlay, NoAutoplayPermissionsOverlay,
} from "./VideoOverlay"; } 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() { 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<Map<string, { mappedKey: string; modifiers: { shift: boolean, altLeft?: boolean, altRight?: boolean}; }>>(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 // Video and stream related refs and states
const videoElm = useRef<HTMLVideoElement>(null); const videoElm = useRef<HTMLVideoElement>(null);
const mediaStream = useRTCStore(state => state.mediaStream); const mediaStream = useRTCStore(state => state.mediaStream);
@ -233,13 +256,13 @@ export default function WebRTCVideo() {
sendAbsMouseMovement(0, 0, 0); sendAbsMouseMovement(0, 0, 0);
}, [sendAbsMouseMovement]); }, [sendAbsMouseMovement]);
// TODO this needs reworked ot work with mappings
// Keyboard-related // Keyboard-related
const handleModifierKeys = useCallback( 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 { shiftKey, ctrlKey, altKey, metaKey } = e;
const filteredModifiers = activeModifiers.filter(Boolean); const filteredModifiers = activeModifiers.filter(Boolean);
// Example: activeModifiers = [0x01, 0x02, 0x04, 0x08] // Example: activeModifiers = [0x01, 0x02, 0x04, 0x08]
// Assuming 0x01 = ControlLeft, 0x02 = ShiftLeft, 0x04 = AltLeft, 0x08 = MetaLeft // Assuming 0x01 = ControlLeft, 0x02 = ShiftLeft, 0x04 = AltLeft, 0x08 = MetaLeft
return ( return (
@ -250,6 +273,7 @@ export default function WebRTCVideo() {
.filter( .filter(
modifier => modifier =>
shiftKey || shiftKey ||
mappedKeyModifers.shift ||
(modifier !== modifiers["ShiftLeft"] && (modifier !== modifiers["ShiftLeft"] &&
modifier !== modifiers["ShiftRight"]), modifier !== modifiers["ShiftRight"]),
) )
@ -268,7 +292,14 @@ export default function WebRTCVideo() {
.filter( .filter(
modifier => modifier =>
altKey || 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 // Meta: Keep if Meta is pressed or if the key isn't a Meta key
// Example: If metaKey is true, keep all modifiers // Example: If metaKey is true, keep all modifiers
@ -287,33 +318,45 @@ export default function WebRTCVideo() {
async (e: KeyboardEvent) => { async (e: KeyboardEvent) => {
e.preventDefault(); e.preventDefault();
const prev = useHidStore.getState(); const prev = useHidStore.getState();
let code = e.code; const code = e.code;
const key = e.key; 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); // console.log("KEYUP: Not focusing on the video", document.activeElement);
// return; // return;
// } // }
console.log(document.activeElement); //
// console.log(document.activeElement);
setIsNumLockActive(e.getModifierState("NumLock")); setIsNumLockActive(e.getModifierState("NumLock"));
setIsCapsLockActive(e.getModifierState("CapsLock")); setIsCapsLockActive(e.getModifierState("CapsLock"));
setIsScrollLockActive(e.getModifierState("ScrollLock")); setIsScrollLockActive(e.getModifierState("ScrollLock"));
if (code == "IntlBackslash" && ["`", "~"].includes(key)) { /*if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
code = "Backquote"; code = "Backquote";
} else if (code == "Backquote" && ["§", "±"].includes(key)) { } else if (code == "Backquote" && ["§", "±"].includes(key)) {
code = "IntlBackslash"; 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 // 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 // Add the modifier to the active modifiers
const newModifiers = handleModifierKeys(e, [ const newModifiers = handleModifierKeys(e, [
...prev.activeModifiers, ...prev.activeModifiers,
modifiers[code], 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 // 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 // event, so we need to clear the keys after a short delay
@ -323,6 +366,8 @@ export default function WebRTCVideo() {
setTimeout(() => { setTimeout(() => {
const prev = useHidStore.getState(); const prev = useHidStore.getState();
sendKeyboardEvent([], newModifiers || prev.activeModifiers); sendKeyboardEvent([], newModifiers || prev.activeModifiers);
activeKeyState.current.delete("MetaLeft");
activeKeyState.current.delete("MetaRight");
}, 10); }, 10);
} }
@ -334,6 +379,10 @@ export default function WebRTCVideo() {
setIsScrollLockActive, setIsScrollLockActive,
handleModifierKeys, handleModifierKeys,
sendKeyboardEvent, sendKeyboardEvent,
chars,
keys,
modifiers,
settings,
], ],
); );
@ -346,14 +395,125 @@ export default function WebRTCVideo() {
setIsCapsLockActive(e.getModifierState("CapsLock")); setIsCapsLockActive(e.getModifierState("CapsLock"));
setIsScrollLockActive(e.getModifierState("ScrollLock")); 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]) // 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 // Filter out the modifier that was just released
const newModifiers = handleModifierKeys( const newModifiers = handleModifierKeys(
e, e,
prev.activeModifiers.filter(k => k !== modifiers[e.code]), prev.activeModifiers.filter(k => k !== modifiers[e.code]),
); );
*/
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
}, },
@ -363,6 +523,9 @@ export default function WebRTCVideo() {
setIsScrollLockActive, setIsScrollLockActive,
handleModifierKeys, handleModifierKeys,
sendKeyboardEvent, sendKeyboardEvent,
chars,
keys,
modifiers,
], ],
); );
@ -444,6 +607,7 @@ export default function WebRTCVideo() {
return () => { return () => {
abortController.abort(); abortController.abort();
activeKeyState.current.clear();
}; };
}, },
[keyDownHandler, keyUpHandler, resetKeyboardState, sendKeyboardEvent], [keyDownHandler, keyUpHandler, resetKeyboardState, sendKeyboardEvent],

View File

@ -8,8 +8,7 @@ import { GridCard } from "@components/Card";
import { TextAreaWithLabel } from "@components/TextArea"; import { TextAreaWithLabel } from "@components/TextArea";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { useJsonRpc } from "@/hooks/useJsonRpc"; import { useJsonRpc } from "@/hooks/useJsonRpc";
import { useHidStore, useRTCStore, useUiStore } from "@/hooks/stores"; import { useHidStore, useRTCStore, useUiStore, useKeyboardMappingsStore } from "@/hooks/stores";
import { chars, keys, modifiers } from "@/keyboardMappings";
import notifications from "@/notifications"; import notifications from "@/notifications";
const hidKeyboardPayload = (keys: number[], modifier: number) => { const hidKeyboardPayload = (keys: number[], modifier: number) => {
@ -17,6 +16,19 @@ const hidKeyboardPayload = (keys: number[], modifier: number) => {
}; };
export default function PasteModal() { 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<HTMLTextAreaElement>(null); const TextAreaRef = useRef<HTMLTextAreaElement>(null);
const setPasteMode = useHidStore(state => state.setPasteModeEnabled); const setPasteMode = useHidStore(state => state.setPasteModeEnabled);
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
@ -42,13 +54,19 @@ export default function PasteModal() {
try { try {
for (const char of text) { for (const char of text) {
const { key, shift } = chars[char] ?? {}; const { key, shift, altLeft, altRight } = chars[char] ?? {};
if (!key) continue; 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<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
send( send(
"keyboardReport", "keyboardReport",
hidKeyboardPayload([keys[key]], shift ? modifiers["ShiftLeft"] : 0), hidKeyboardPayload([keys[key]], modifier),
params => { params => {
if ("error" in params) return reject(params.error); if ("error" in params) return reject(params.error);
send("keyboardReport", hidKeyboardPayload([], 0), params => { send("keyboardReport", hidKeyboardPayload([], 0), params => {
@ -63,7 +81,7 @@ export default function PasteModal() {
console.error(error); console.error(error);
notifications.error("Failed to paste text"); notifications.error("Failed to paste text");
} }
}, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode]); }, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, chars, keys, modifiers]);
useEffect(() => { useEffect(() => {
if (TextAreaRef.current) { if (TextAreaRef.current) {
@ -125,9 +143,12 @@ export default function PasteModal() {
<div className="mt-2 flex items-center gap-x-2"> <div className="mt-2 flex items-center gap-x-2">
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" /> <ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
<span className="text-xs text-red-500 dark:text-red-400"> <span className="text-xs text-red-500 dark:text-red-400">
The following characters won&apos;t be pasted:{" "} The following characters won&apos;t be pasted as the current keyboard layout does not contain a valid mapping:{" "}
{invalidChars.join(", ")} {invalidChars.join(", ")}
</span> </span>
<span className="text-xs text-red-500 dark:text-red-400">
Tip: You can set your desired keyboard layout in settings, and remember to enable keyboard mapping.
</span>
</div> </div>
)} )}
</div> </div>

View File

@ -1,5 +1,6 @@
import { create } from "zustand"; import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware"; 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"; import { MAX_STEPS_PER_MACRO, MAX_TOTAL_MACROS, MAX_KEYS_PER_STEP } from "@/constants/macros";
// Define the JsonRpc types for better type checking // Define the JsonRpc types for better type checking
@ -285,6 +286,9 @@ interface SettingsState {
mouseMode: string; mouseMode: string;
setMouseMode: (mode: string) => void; setMouseMode: (mode: string) => void;
keyboardMappingEnabled: boolean;
setkeyboardMappingEnabled: (enabled: boolean) => void;
debugMode: boolean; debugMode: boolean;
setDebugMode: (enabled: boolean) => void; setDebugMode: (enabled: boolean) => void;
@ -299,6 +303,9 @@ interface SettingsState {
export const useSettingsStore = create( export const useSettingsStore = create(
persist<SettingsState>( persist<SettingsState>(
set => ({ set => ({
keyboardMappingEnabled: false,
setkeyboardMappingEnabled: enabled => set({keyboardMappingEnabled: enabled}),
isCursorHidden: false, isCursorHidden: false,
setCursorVisibility: enabled => set({ isCursorHidden: enabled }), setCursorVisibility: enabled => set({ isCursorHidden: enabled }),
@ -631,6 +638,69 @@ export const useUsbConfigModalStore = create<UsbConfigModalState>(set => ({
setErrorMessage: message => set({ errorMessage: message }), 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 { interface LocalAuthModalState {
modalView: modalView:
| "createPassword" | "createPassword"
@ -804,4 +874,4 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
set({ loading: false }); set({ loading: false });
} }
} }
})); }));

View File

@ -1,8 +1,8 @@
import { useCallback } from "react"; import { useCallback, useState, useEffect } from "react";
import { useHidStore, useRTCStore } from "@/hooks/stores"; import { useHidStore, useRTCStore } from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc"; import { useJsonRpc } from "@/hooks/useJsonRpc";
import { keys, modifiers } from "@/keyboardMappings"; import { useKeyboardMappingsStore } from "@/hooks/stores";
export default function useKeyboard() { export default function useKeyboard() {
const [send] = useJsonRpc(); const [send] = useJsonRpc();
@ -12,6 +12,17 @@ export default function useKeyboard() {
state => state.updateActiveKeysAndModifiers, 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( const sendKeyboardEvent = useCallback(
(keys: number[], modifiers: number[]) => { (keys: number[], modifiers: number[]) => {
if (rpcDataChannel?.readyState !== "open") return; if (rpcDataChannel?.readyState !== "open") return;

View File

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

View File

@ -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<string, string> = {
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<string, string>;
export const keyDisplayMap: Record<string, string> = {
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",
"!": "!",
"@": "@",
"#": "#",
"$": "$",
"%": "%",
"^": "^",
"&": "&",
"*": "*",
"(": "(",
")": ")",
"-": "-",
"_": "_",
"[": "[",
"]": "]",
"{": "{",
"}": "}",
"|": "|",
";": ";",
":": ":",
"'": "'",
"\"": "\"",
",": ",",
"<": "<",
".": ".",
">": ">",
"/": "/",
"?": "?",
"`": "`",
"~": "~",
"\\": "\\"
};

View File

@ -0,0 +1,69 @@
import { charsUS, keysUS, modifiersUS } from "./us";
export const keysDE_T1 = {
...keysUS,
} as Record<string, number>;
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<string, { key: string; shift: boolean; altLeft?: boolean; altRight?: boolean }>;
export const modifiersDE_T1 = {
...modifiersUS,
} as Record<string, number>;

View File

@ -0,0 +1,24 @@
import { charsUS, keysUS, modifiersUS } from "./us";
export const keysUK = {
...keysUS,
} as Record<string, number>;
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<string, { key: string; shift: boolean; altLeft?: boolean; altRight?: boolean; }>;
export const modifiersUK = {
...modifiersUS,
} as Record<string, number>;

View File

@ -0,0 +1,25 @@
import { charsUS, keysUS, modifiersUS } from "./us";
// Extend US Keys with UK Apple-specific changes
export const keysUKApple = {
...keysUS,
} as Record<string, number>;
// 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<string, { key: string; shift: boolean; altLeft?: boolean; altRight?: boolean; }>;
// Modifiers are typically the same between UK and US layouts
export const modifiersUKApple = {
...modifiersUS,
} as Record<string, number>;

View File

@ -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<string, number>;
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<string, { key: string; shift: boolean; altLeft?: boolean; altRight?: boolean; }>;
export const modifiersUS = {
ControlLeft: 0x01,
ControlRight: 0x10,
ShiftLeft: 0x02,
ShiftRight: 0x20,
AltLeft: 0x04,
AltRight: 0x40,
MetaLeft: 0x08,
MetaRight: 0x80,
} as Record<string, number>;

View File

@ -8,7 +8,7 @@ import { Button } from "@/components/Button";
import EmptyCard from "@/components/EmptyCard"; import EmptyCard from "@/components/EmptyCard";
import Card from "@/components/Card"; import Card from "@/components/Card";
import { MAX_TOTAL_MACROS, COPY_SUFFIX, DEFAULT_DELAY } from "@/constants/macros"; 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 notifications from "@/notifications";
import { ConfirmDialog } from "@/components/ConfirmDialog"; import { ConfirmDialog } from "@/components/ConfirmDialog";
import LoadingSpinner from "@/components/LoadingSpinner"; import LoadingSpinner from "@/components/LoadingSpinner";
@ -27,6 +27,7 @@ export default function SettingsMacrosRoute() {
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [macroToDelete, setMacroToDelete] = useState<KeySequence | null>(null); const [macroToDelete, setMacroToDelete] = useState<KeySequence | null>(null);
const isMaxMacrosReached = useMemo(() => const isMaxMacrosReached = useMemo(() =>
macros.length >= MAX_TOTAL_MACROS, macros.length >= MAX_TOTAL_MACROS,
[macros.length] [macros.length]

View File

@ -5,7 +5,7 @@ import MouseIcon from "@/assets/mouse-icon.svg";
import PointingFinger from "@/assets/pointing-finger.svg"; import PointingFinger from "@/assets/pointing-finger.svg";
import { GridCard } from "@/components/Card"; import { GridCard } from "@/components/Card";
import { Checkbox } from "@/components/Checkbox"; import { Checkbox } from "@/components/Checkbox";
import { useDeviceSettingsStore, useSettingsStore } from "@/hooks/stores"; import { useDeviceSettingsStore, useSettingsStore, useKeyboardMappingsStore } from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc"; import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
@ -36,6 +36,39 @@ export default function SettingsKeyboardMouseRoute() {
const [send] = useJsonRpc(); 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(() => { useEffect(() => {
send("getJigglerState", {}, resp => { send("getJigglerState", {}, resp => {
if ("error" in resp) return; if ("error" in resp) return;
@ -48,7 +81,21 @@ export default function SettingsKeyboardMouseRoute() {
setScrollSensitivity(resp.result as ScrollSensitivity); 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) => { const handleJigglerChange = (enabled: boolean) => {
send("setJigglerState", { enabled }, resp => { send("setJigglerState", { enabled }, resp => {
@ -78,6 +125,7 @@ export default function SettingsKeyboardMouseRoute() {
[send, setScrollSensitivity], [send, setScrollSensitivity],
); );
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
@ -183,6 +231,44 @@ export default function SettingsKeyboardMouseRoute() {
</div> </div>
</div> </div>
</div> </div>
<div className="space-y-4">
<SettingsPageHeader
title="Keyboard"
description="Customize keyboard behaviour"
/>
<div className="space-y-4">
<SettingsItem
title="Enable Keyboard Mapping"
description="Enables mapping of keys from your native layout to the layout of the target device"
>
<Checkbox
checked={kbMappingEnabled}
onChange={e => {
handleKeyboardMappingChange(e.target.checked);
}}
/>
</SettingsItem>
<SettingsItem
title="Keyboard Layout"
description="Set keyboard layout (this should match the target machine)"
>
<SelectMenuBasic
size="SM_Wide"
label=""
// TODO figure out how to make this selector wider like the EDID one?, (done but not sure if in desired way.)
//fullWidth
value={keyboardLayout}
options={[
{ value: "en-US", label: "US" },
{ value: "en-GB", label: "UK" },
{ value: "en-GB_apple", label: "UK (Apple)" },
{ value: "de_DE", label: "German (T1)" },
]}
onChange={e => handleKeyboardLayoutChange(e.target.value)}
/>
</SettingsItem>
</div>
</div>
</div> </div>
); );
} }

View File

@ -148,7 +148,7 @@ export default function SettingsRoute() {
> >
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent"> <div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent">
<LuKeyboard className="h-4 w-4 shrink-0" /> <LuKeyboard className="h-4 w-4 shrink-0" />
<h1>Mouse</h1> <h1>Mouse & Keyboard</h1>
</div> </div>
</NavLink> </NavLink>
</div> </div>