Compare commits

...

6 Commits

Author SHA1 Message Date
William Johnstone 9b8f3ea79e
Merge 8732a6aff8 into 951173ba19 2025-02-13 19:23:43 +01:00
William Johnstone 8732a6aff8
Fix ghost keys issue, properly implemented modifer mapping 2025-02-05 01:26:24 +00:00
William Johnstone f1de6639ef
Continued work on keyboard overhaul, found many more issues caused by my changes (YAY!).
Also spent 4 hours troubleshooting to find out I didn't realise how useCallback works... :/

Anway, not much longer before work on just the mappings can begin.
2025-02-04 01:45:34 +00:00
William Johnstone 5f1e53f24a
Fully implemented layout setting and switching in the UI. Updated PasteModal to add more clarity to error message. Begin working on key remapping in WebRTC (working to a reasonable degree). 2025-02-03 00:39:53 +00:00
William Johnstone 7c40e2e011
Move keyboardmapping store to stores.ts, simplified some things, updated settings.tsx to set the keyboard layout properly. 2025-02-01 18:38:40 +00:00
William Johnstone 0e855adc35
Early implementation of different keyboard layouts.
Store is functioning as expected, adding new layouts should be trivial and easily scalable.

Implementation is different for each function that uses the keyboard (PasteModal vs Typing in the WebRTC window) these will all require their own testing.
2025-01-29 01:52:13 +00:00
14 changed files with 597 additions and 234 deletions

View File

@ -17,6 +17,7 @@ type Config struct {
GoogleIdentity string `json:"google_identity"`
JigglerEnabled bool `json:"jiggler_enabled"`
AutoUpdateEnabled bool `json:"auto_update_enabled"`
KeyboardLayout string `json:"keyboard_layout"`
IncludePreRelease bool `json:"include_pre_release"`
HashedPassword string `json:"hashed_password"`
LocalAuthToken string `json:"local_auth_token"`
@ -30,6 +31,7 @@ const configPath = "/userdata/kvm_config.json"
var defaultConfig = &Config{
CloudURL: "https://api.jetkvm.com",
AutoUpdateEnabled: true, // Set a default value
KeyboardLayout: "us",
}
var config *Config

View File

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

View File

@ -131,6 +131,18 @@ func rpcGetDeviceID() (string, error) {
return GetDeviceID(), nil
}
func rpcGetKeyboardLayout() (string, error) {
return config.KeyboardLayout, nil
}
func rpcSetKeyboardLayout(KeyboardLayout string) (string, error) {
config.KeyboardLayout = KeyboardLayout
if err := SaveConfig(); err != nil {
return config.KeyboardLayout, fmt.Errorf("failed to save config: %w", err)
}
return KeyboardLayout, nil
}
var streamFactor = 1.0
func rpcGetStreamQualityFactor() (float64, error) {
@ -529,6 +541,8 @@ var rpcHandlers = map[string]RPCHandler{
"setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}},
"getJigglerState": {Func: rpcGetJigglerState},
"sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}},
"getKeyboardLayout": {Func: rpcGetKeyboardLayout},
"setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"kbLayout"}},
"getStreamQualityFactor": {Func: rpcGetStreamQualityFactor},
"setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}},
"getAutoUpdateState": {Func: rpcGetAutoUpdateState},

View File

@ -5,11 +5,22 @@ import {
useRTCStore,
useSettingsStore,
useVideoStore,
useKeyboardMappingsStore,
} from "@/hooks/stores";
import { useEffect } from "react";
import { keys, modifiers } from "@/keyboardMappings";
import { useEffect, useState } from "react";
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);

View File

@ -4,10 +4,9 @@ import { Button } from "@components/Button";
import Card from "@components/Card";
import { ChevronDownIcon } from "@heroicons/react/16/solid";
import "react-simple-keyboard/build/css/index.css";
import { useHidStore, useUiStore } from "@/hooks/stores";
import { useHidStore, useUiStore, useKeyboardMappingsStore } from "@/hooks/stores";
import { Transition } from "@headlessui/react";
import { cx } from "@/cva.config";
import { keys, modifiers } from "@/keyboardMappings";
import useKeyboard from "@/hooks/useKeyboard";
import DetachIconRaw from "@/assets/detach-icon.svg";
import AttachIconRaw from "@/assets/attach-icon.svg";
@ -21,6 +20,20 @@ const AttachIcon = ({ className }: { className?: string }) => {
};
function KeyboardWrapper() {
// TODO implement virtual keyboard mapping
const [keys, setKeys] = useState(useKeyboardMappingsStore.keys);
//const [chars, setChars] = useState(useKeyboardMappingsStore.chars);
const [modifiers, setModifiers] = useState(useKeyboardMappingsStore.modifiers);
useEffect(() => {
const unsubscribeKeyboardStore = useKeyboardMappingsStore.subscribe(() => {
setKeys(useKeyboardMappingsStore.keys);
//setChars(useKeyboardMappingsStore.chars);
setModifiers(useKeyboardMappingsStore.modifiers);
});
return unsubscribeKeyboardStore; // Cleanup on unmount
}, []);
const [layoutName, setLayoutName] = useState("default");
const keyboardRef = useRef<HTMLDivElement>(null);

View File

@ -6,8 +6,8 @@ import {
useSettingsStore,
useUiStore,
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";
@ -18,6 +18,22 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
import { ConnectionErrorOverlay, HDMIErrorOverlay, LoadingOverlay } from "./VideoOverlay";
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
const videoElm = useRef<HTMLVideoElement>(null);
const mediaStream = useRTCStore(state => state.mediaStream);
@ -148,6 +164,7 @@ export default function WebRTCVideo() {
if (blockWheelEvent) return;
e.preventDefault();
// TODO this should be user controllable
// Define a scaling factor to adjust scrolling sensitivity
const scrollSensitivity = 0.8; // Adjust this value to change scroll speed
@ -163,9 +180,11 @@ export default function WebRTCVideo() {
// Invert the scroll value to match expected behavior
const invertedScroll = -roundedScroll;
// TODO remove debug logs
console.log("wheelReport", { wheelY: invertedScroll });
send("wheelReport", { wheelY: invertedScroll });
// TODO this is making scrolling feel slow and sluggish, also throwing a violation in chrome
setBlockWheelEvent(true);
setTimeout(() => setBlockWheelEvent(false), 50);
},
@ -176,13 +195,16 @@ export default function WebRTCVideo() {
sendMouseMovement(0, 0, 0);
}, [sendMouseMovement]);
// TODO this needs reworked ot work with mappings
// Keyboard-related
const handleModifierKeys = useCallback(
(e: KeyboardEvent, activeModifiers: number[]) => {
(e: KeyboardEvent, activeModifiers: number[], mappedKeyModifers: { shift: boolean; altLeft: boolean; altRight: boolean; }) => {
const { shiftKey, ctrlKey, altKey, metaKey } = e;
const filteredModifiers = activeModifiers.filter(Boolean);
// TODO remove debug logging
console.log(shiftKey + " " +ctrlKey + " " +altKey + " " +metaKey + " " +mappedKeyModifers.shift + " "+mappedKeyModifers.altLeft + " "+mappedKeyModifers.altRight + " ")
const filteredModifiers = activeModifiers.filter(Boolean);3
// Example: activeModifiers = [0x01, 0x02, 0x04, 0x08]
// Assuming 0x01 = ControlLeft, 0x02 = ShiftLeft, 0x04 = AltLeft, 0x08 = MetaLeft
return (
@ -193,6 +215,7 @@ export default function WebRTCVideo() {
.filter(
modifier =>
shiftKey ||
mappedKeyModifers.shift ||
(modifier !== modifiers["ShiftLeft"] &&
modifier !== modifiers["ShiftRight"]),
)
@ -211,6 +234,8 @@ export default function WebRTCVideo() {
.filter(
modifier =>
altKey ||
mappedKeyModifers.altLeft ||
mappedKeyModifers.altRight ||
(modifier !== modifiers["AltLeft"] && modifier !== modifiers["AltRight"]),
)
// Meta: Keep if Meta is pressed or if the key isn't a Meta key
@ -231,37 +256,59 @@ export default function WebRTCVideo() {
e.preventDefault();
const prev = useHidStore.getState();
let code = e.code;
const key = e.key;
const localisedKey = e.key;
console.log(e);
console.log("Localised Key: " + localisedKey);
// 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;
console.log("Mapped Key: " + mappedKey)
console.log("Current KB Layout:" + useKeyboardMappingsStore.getLayout());
console.log(chars[localisedKey]);
console.log("Shift: " + shift + ", altLeft: " + altLeft + ", altRight: " + altRight)
// Add the mapped key to keyState
activeKeyState.current.set(e.code, { mappedKey, modifiers: {shift, altLeft, altRight}});
console.log(activeKeyState)
// Add the key to the active keys
const newKeys = [...prev.activeKeys, keys[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
// https://bugs.chromium.org/p/chromium/issues/detail?id=28089
// https://bugzilla.mozilla.org/show_bug.cgi?id=1299553
// TODO add this to the activekey state
// TODO set this to remove from activekeystate as well
if (e.metaKey) {
setTimeout(() => {
const prev = useHidStore.getState();
@ -277,12 +324,16 @@ export default function WebRTCVideo() {
setIsScrollLockActive,
handleModifierKeys,
sendKeyboardEvent,
chars,
keys,
modifiers,
],
);
const keyUpHandler = useCallback(
(e: KeyboardEvent) => {
e.preventDefault();
console.log(e)
const prev = useHidStore.getState();
// if (document.activeElement?.id !== "videoFocusTrap") {
@ -294,15 +345,132 @@ 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) {
console.log("ITS A MODIFER")
// 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);
};
});
console.log("prev.activemodifers: " + prev.activeModifiers)
console.log("prev.activemodifers.filtered: " + prev.activeModifiers.filter(k => k !== modifiers[e.code]))
const newModifiers = handleModifierKeys(
e,
prev.activeModifiers.filter(k => k !== modifiers[e.code]),
{shift: false, altLeft: false, altRight: false}
);
console.log("New modifiers in keyup: " + newModifiers)
// Update the keyState
/*activeKeyState.current.delete(code);/*.set(code, {
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);
console.log(activeKeyState)
// Filter out the associated modifier
//const newModifiers = prev.activeModifiers.filter(k => k !== modifier).filter(Boolean);
const newModifiers = handleModifierKeys(
e,
prev.activeModifiers.filter(k => {
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]),
);
*/
console.log(e.key);
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
},
[
@ -311,6 +479,9 @@ export default function WebRTCVideo() {
setIsScrollLockActive,
handleModifierKeys,
sendKeyboardEvent,
chars,
keys,
modifiers,
],
);
@ -331,6 +502,7 @@ export default function WebRTCVideo() {
return () => {
abortController.abort();
activeKeyState.current.clear();
};
},
[keyDownHandler, keyUpHandler, resetKeyboardState, sendKeyboardEvent],

View File

@ -3,19 +3,31 @@ import { GridCard } from "@components/Card";
import { TextAreaWithLabel } from "@components/TextArea";
import { SectionHeader } from "@components/SectionHeader";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { useHidStore, useRTCStore, useUiStore } from "@/hooks/stores";
import { useHidStore, useRTCStore, useUiStore, useKeyboardMappingsStore } from "@/hooks/stores";
import notifications from "../../notifications";
import { useCallback, useEffect, useRef, useState } from "react";
import { LuCornerDownLeft } from "react-icons/lu";
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
import { useClose } from "@headlessui/react";
import { chars, keys, modifiers } from "@/keyboardMappings";
const hidKeyboardPayload = (keys: number[], modifier: number) => {
return { keys, modifier };
};
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 setPasteMode = useHidStore(state => state.setPasteModeEnabled);
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
@ -41,13 +53,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<void>((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 => {
@ -123,7 +141,7 @@ export default function PasteModal() {
<div className="flex items-center mt-2 gap-x-2">
<ExclamationCircleIcon className="w-4 h-4 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(", ")}
</span>
</div>

View File

@ -4,6 +4,7 @@ import {
useSettingsStore,
useUiStore,
useUpdateStore,
useKeyboardMappingsStore,
} from "@/hooks/stores";
import { Checkbox } from "@components/Checkbox";
import { Button, LinkButton } from "@components/Button";
@ -77,6 +78,7 @@ export default function SettingsSidebar() {
const setSidebarView = useUiStore(state => state.setSidebarView);
const settings = useSettingsStore();
const [send] = useJsonRpc();
const [keyboardLayout, setKeyboardLayout] = useState("us");
const [streamQuality, setStreamQuality] = useState("1");
const [autoUpdate, setAutoUpdate] = useState(true);
const [devChannel, setDevChannel] = useState(false);
@ -146,6 +148,19 @@ export default function SettingsSidebar() {
});
};
const handleKeyboardLayoutChange = (keyboardLayout: string) => {
send("setKeyboardLayout", { kbLayout: keyboardLayout }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to set keyboard layout: ${resp.error.data || "Unknown error"}`,
);
return;
}
useKeyboardMappingsStore.setLayout(keyboardLayout)
setKeyboardLayout(keyboardLayout);
});
};
const handleStreamQualityChange = (factor: string) => {
send("setStreamQualityFactor", { factor: Number(factor) }, resp => {
if ("error" in resp) {
@ -274,6 +289,12 @@ export default function SettingsSidebar() {
setDevChannel(resp.result as boolean);
});
send("getKeyboardLayout", {}, resp => {
if ("error" in resp) return;
setKeyboardLayout(String(resp.result));
useKeyboardMappingsStore.setLayout(String(resp.result))
});
send("getStreamQualityFactor", {}, resp => {
if ("error" in resp) return;
setStreamQuality(String(resp.result));
@ -509,6 +530,33 @@ export default function SettingsSidebar() {
</div>
</div>
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
<div className="pb-2 space-y-4">
<SectionHeader
title="Keyboard"
description="Customize keyboard behaviour"
/>
<div className="space-y-4">
<SettingsItem
title="Keyboard Layout"
description="Set keyboard layout (this should match the target machine)"
>
<SelectMenuBasic
size="SM"
label=""
// TODO figure out how to make this selector wider like the EDID one?
//fullWidthƒ
value={keyboardLayout}
options={[
{ value: "uk", label: "GB" },
{ value: "uk_apple", label: "GB Apple" },
{ value: "us", label: "US" },
]}
onChange={e => handleKeyboardLayoutChange(e.target.value)}
/>
</SettingsItem>
</div>
</div>
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
<div className="pb-2 space-y-4">
<SectionHeader
title="Video"

View File

@ -1,5 +1,6 @@
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { getKeyboardMappings } from "@/keyboardMappings/KeyboardLayouts";
// Utility function to append stats to a Map
const appendStatToMap = <T extends { timestamp: number }>(
@ -386,6 +387,8 @@ export const useHidStore = create<HidState>(set => ({
activeKeys: [],
activeModifiers: [],
updateActiveKeysAndModifiers: ({ keys, modifiers }) => {
// TODO remove debug logs
console.log("keys: " + keys + "modifiers: " + modifiers)
return set({ activeKeys: keys, activeModifiers: modifiers });
},
@ -528,3 +531,39 @@ export const useLocalAuthModalStore = create<LocalAuthModalState>(set => ({
setModalView: view => set({ modalView: view }),
setErrorMessage: message => set({ errorMessage: message }),
}));
class KeyboardMappingsStore {
private _layout: string = 'us';
private _subscribers: (() => void)[] = [];
public keys = getKeyboardMappings(this._layout).keys;
public chars = getKeyboardMappings(this._layout).chars;
public modifiers = getKeyboardMappings(this._layout).modifiers;
setLayout(newLayout: string) {
if (this._layout === newLayout) return;
this._layout = newLayout;
const updatedMappings = getKeyboardMappings(newLayout);
this.keys = updatedMappings.keys;
this.chars = updatedMappings.chars;
this.modifiers = updatedMappings.modifiers;
this._notifySubscribers();
}
getLayout() {
return this._layout;
}
subscribe(callback: () => void) {
this._subscribers.push(callback);
return () => {
this._subscribers = this._subscribers.filter(sub => sub !== callback); // Cleanup
};
}
private _notifySubscribers() {
this._subscribers.forEach(callback => callback());
}
}
export const useKeyboardMappingsStore = new KeyboardMappingsStore();

View File

@ -1,214 +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>;

View File

@ -0,0 +1,20 @@
import {keysUKApple, charsUKApple, modifiersUKApple } from './layouts/uk_apple';
import {keysUS, charsUS, modifiersUS } from './layouts/us';
export function getKeyboardMappings(layout: string) {
switch (layout) {
case "uk_apple":
return {
keys: keysUKApple,
chars: charsUKApple,
modifiers: modifiersUKApple,
};
case "us":
default:
return {
keys: keysUS,
chars: charsUS,
modifiers: modifiersUS,
};
}
}

View File

View File

@ -0,0 +1,24 @@
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 },
} 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>;