From a9affa81ffaa650f874499e174d8382ac1e4d1c4 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Fri, 11 Jul 2025 10:49:06 -0500 Subject: [PATCH] refactor(ui): Refactor the keyboardLayouts (#497) Add missing keyboard mappings for most layouts Change pasteModel.tsx to use the new structure and vastly clarified the way that keys are emitted. Make each layout export just the KeyboardLayout object (which is a package of isoCode, name, and chars) Made keyboardLayouts.ts export a function to select keyboard by `isoCode`, export the keyboards as label . value pairs (for a select list) and the list of keyboards. Changed devices.$id.settings.keyboard.tsx use the exported keyboard option list. --- ui/src/components/Terminal.tsx | 14 ++-- ui/src/components/popovers/PasteModal.tsx | 73 ++++++++++--------- .../components/popovers/WakeOnLan/Index.tsx | 12 +-- ui/src/hooks/stores.ts | 2 +- ui/src/keyboardLayouts.ts | 69 +++++++----------- ui/src/keyboardLayouts/cs_CZ.ts | 12 ++- ui/src/keyboardLayouts/de_CH.ts | 12 ++- ui/src/keyboardLayouts/de_DE.ts | 12 ++- ui/src/keyboardLayouts/en_UK.ts | 12 ++- ui/src/keyboardLayouts/en_US.ts | 12 ++- ui/src/keyboardLayouts/es_ES.ts | 12 ++- ui/src/keyboardLayouts/fr_BE.ts | 12 ++- ui/src/keyboardLayouts/fr_CH.ts | 16 ++-- ui/src/keyboardLayouts/fr_FR.ts | 12 ++- ui/src/keyboardLayouts/it_IT.ts | 12 ++- ui/src/keyboardLayouts/nb_NO.ts | 12 ++- ui/src/keyboardLayouts/sv_SE.ts | 12 ++- ui/src/keyboardMappings.ts | 52 ++++++++----- .../routes/devices.$id.settings.keyboard.tsx | 4 +- ui/src/routes/devices.$id.settings.tsx | 2 +- ui/src/routes/devices.$id.tsx | 4 +- 21 files changed, 232 insertions(+), 148 deletions(-) diff --git a/ui/src/components/Terminal.tsx b/ui/src/components/Terminal.tsx index 5451afea..f5d662d4 100644 --- a/ui/src/components/Terminal.tsx +++ b/ui/src/components/Terminal.tsx @@ -67,19 +67,19 @@ function Terminal({ }) { const enableTerminal = useUiStore(state => state.terminalType == type); const setTerminalType = useUiStore(state => state.setTerminalType); - const setDisableKeyboardFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); + const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); const { instance, ref } = useXTerm({ options: TERMINAL_CONFIG }); useEffect(() => { setTimeout(() => { - setDisableKeyboardFocusTrap(enableTerminal); + setDisableVideoFocusTrap(enableTerminal); }, 500); return () => { - setDisableKeyboardFocusTrap(false); + setDisableVideoFocusTrap(false); }; - }, [ref, instance, enableTerminal, setDisableKeyboardFocusTrap, type]); + }, [enableTerminal, setDisableVideoFocusTrap]); const readyState = dataChannel.readyState; useEffect(() => { @@ -116,7 +116,7 @@ function Terminal({ const { domEvent } = e; if (domEvent.key === "Escape") { setTerminalType("none"); - setDisableKeyboardFocusTrap(false); + setDisableVideoFocusTrap(false); domEvent.preventDefault(); } }); @@ -131,7 +131,7 @@ function Terminal({ onDataHandler.dispose(); onKeyHandler.dispose(); }; - }, [instance, dataChannel, readyState, setDisableKeyboardFocusTrap, setTerminalType]); + }, [dataChannel, instance, readyState, setDisableVideoFocusTrap, setTerminalType]); useEffect(() => { if (!instance) return; @@ -158,7 +158,7 @@ function Terminal({ return () => { window.removeEventListener("resize", handleResize); }; - }, [ref, instance]); + }, [instance]); return (
{ - return { keys, modifier }; +const hidKeyboardPayload = (modifier: number, keys: number[]) => { + return { modifier, keys }; }; const modifierCode = (shift?: boolean, altRight?: boolean) => { @@ -62,49 +62,56 @@ export default function PasteModal() { const onConfirmPaste = useCallback(async () => { setPasteMode(false); setDisableVideoFocusTrap(false); + if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return; - if (!safeKeyboardLayout) return; - if (!chars[safeKeyboardLayout]) return; + const keyboard: KeyboardLayout = selectedKeyboard(safeKeyboardLayout); + if (!keyboard) return; + const text = TextAreaRef.current.value; try { for (const char of text) { - const { key, shift, altRight, deadKey, accentKey } = chars[safeKeyboardLayout][char] + const keyprops = keyboard.chars[char]; + if (!keyprops) continue; + + const { key, shift, altRight, deadKey, accentKey } = keyprops; if (!key) continue; - const keyz = [ keys[key] ]; - const modz = [ modifierCode(shift, altRight) ]; - - if (deadKey) { - keyz.push(keys["Space"]); - modz.push(noModifier); - } + // if this is an accented character, we need to send that accent FIRST if (accentKey) { - keyz.unshift(keys[accentKey.key]) - modz.unshift(modifierCode(accentKey.shift, accentKey.altRight)) + await sendKeystroke({modifier: modifierCode(accentKey.shift, accentKey.altRight), keys: [ keys[accentKey.key] ] }) } - for (const [index, kei] of keyz.entries()) { - await new Promise((resolve, reject) => { - send( - "keyboardReport", - hidKeyboardPayload([kei], modz[index]), - params => { - if ("error" in params) return reject(params.error); - send("keyboardReport", hidKeyboardPayload([], 0), params => { - if ("error" in params) return reject(params.error); - resolve(); - }); - }, - ); - }); + // now send the actual key + await sendKeystroke({ modifier: modifierCode(shift, altRight), keys: [ keys[key] ]}); + + // if what was requested was a dead key, we need to send an unmodified space to emit + // just the accent character + if (deadKey) { + await sendKeystroke({ modifier: noModifier, keys: [ keys["Space"] ] }); } + + // now send a message with no keys down to "release" the keys + await sendKeystroke({ modifier: 0, keys: [] }); } } catch (error) { - console.error(error); + console.error("Failed to paste text:", error); notifications.error("Failed to paste text"); } - }, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, safeKeyboardLayout]); + + async function sendKeystroke(stroke: KeyStroke) { + await new Promise((resolve, reject) => { + send( + "keyboardReport", + hidKeyboardPayload(stroke.modifier, stroke.keys), + params => { + if ("error" in params) return reject(params.error); + resolve(); + } + ); + }); + } + }, [rpcDataChannel?.readyState, safeKeyboardLayout, send, setDisableVideoFocusTrap, setPasteMode]); useEffect(() => { if (TextAreaRef.current) { @@ -154,7 +161,7 @@ export default function PasteModal() { // @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments [...new Intl.Segmenter().segment(value)] .map(x => x.segment) - .filter(char => !chars[safeKeyboardLayout][char]), + .filter(char => !selectedKeyboard(safeKeyboardLayout).chars[char]), ), ]; @@ -175,7 +182,7 @@ export default function PasteModal() {

- Sending text using keyboard layout: {layouts[safeKeyboardLayout]} + Sending text using keyboard layout: {selectedKeyboard(safeKeyboardLayout).name}

diff --git a/ui/src/components/popovers/WakeOnLan/Index.tsx b/ui/src/components/popovers/WakeOnLan/Index.tsx index f4f49518..1cf7f184 100644 --- a/ui/src/components/popovers/WakeOnLan/Index.tsx +++ b/ui/src/components/popovers/WakeOnLan/Index.tsx @@ -14,7 +14,7 @@ import AddDeviceForm from "./AddDeviceForm"; export default function WakeOnLanModal() { const [storedDevices, setStoredDevices] = useState([]); const [showAddForm, setShowAddForm] = useState(false); - const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); + const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); @@ -24,9 +24,9 @@ export default function WakeOnLanModal() { const [addDeviceErrorMessage, setAddDeviceErrorMessage] = useState(null); const onCancelWakeOnLanModal = useCallback(() => { + setDisableVideoFocusTrap(false); close(); - setDisableFocusTrap(false); - }, [close, setDisableFocusTrap]); + }, [close, setDisableVideoFocusTrap]); const onSendMagicPacket = useCallback( (macAddress: string) => { @@ -43,12 +43,12 @@ export default function WakeOnLanModal() { } } else { notifications.success("Magic Packet sent successfully"); - setDisableFocusTrap(false); + setDisableVideoFocusTrap(false); close(); } }); }, - [close, rpcDataChannel?.readyState, send, setDisableFocusTrap], + [close, rpcDataChannel?.readyState, send, setDisableVideoFocusTrap], ); const syncStoredDevices = useCallback(() => { @@ -78,7 +78,7 @@ export default function WakeOnLanModal() { } }); }, - [storedDevices, send, syncStoredDevices], + [send, storedDevices, syncStoredDevices], ); const onAddDevice = useCallback( diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 7d656667..aa29528a 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -936,5 +936,5 @@ export const useMacrosStore = create((set, get) => ({ } finally { set({ loading: false }); } - }, + } })); diff --git a/ui/src/keyboardLayouts.ts b/ui/src/keyboardLayouts.ts index 3b835b1f..4ae3ad94 100644 --- a/ui/src/keyboardLayouts.ts +++ b/ui/src/keyboardLayouts.ts @@ -1,45 +1,32 @@ -import { chars as chars_fr_BE, name as name_fr_BE } from "@/keyboardLayouts/fr_BE" -import { chars as chars_cs_CZ, name as name_cs_CZ } from "@/keyboardLayouts/cs_CZ" -import { chars as chars_en_UK, name as name_en_UK } from "@/keyboardLayouts/en_UK" -import { chars as chars_en_US, name as name_en_US } from "@/keyboardLayouts/en_US" -import { chars as chars_fr_FR, name as name_fr_FR } from "@/keyboardLayouts/fr_FR" -import { chars as chars_de_DE, name as name_de_DE } from "@/keyboardLayouts/de_DE" -import { chars as chars_it_IT, name as name_it_IT } from "@/keyboardLayouts/it_IT" -import { chars as chars_nb_NO, name as name_nb_NO } from "@/keyboardLayouts/nb_NO" -import { chars as chars_es_ES, name as name_es_ES } from "@/keyboardLayouts/es_ES" -import { chars as chars_sv_SE, name as name_sv_SE } from "@/keyboardLayouts/sv_SE" -import { chars as chars_fr_CH, name as name_fr_CH } from "@/keyboardLayouts/fr_CH" -import { chars as chars_de_CH, name as name_de_CH } from "@/keyboardLayouts/de_CH" +export interface KeyStroke { modifier: number; keys: number[]; } +export interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean } +export interface KeyCombo extends KeyInfo { deadKey?: boolean, accentKey?: KeyInfo } +export interface KeyboardLayout { isoCode: string, name: string, chars: Record } -interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean } -export type KeyCombo = KeyInfo & { deadKey?: boolean, accentKey?: KeyInfo } +// to add a new layout, create a file like the above and add it to the list +import { cs_CZ } from "@/keyboardLayouts/cs_CZ" +import { de_CH } from "@/keyboardLayouts/de_CH" +import { de_DE } from "@/keyboardLayouts/de_DE" +import { en_US } from "@/keyboardLayouts/en_US" +import { en_UK } from "@/keyboardLayouts/en_UK" +import { es_ES } from "@/keyboardLayouts/es_ES" +import { fr_BE } from "@/keyboardLayouts/fr_BE" +import { fr_CH } from "@/keyboardLayouts/fr_CH" +import { fr_FR } from "@/keyboardLayouts/fr_FR" +import { it_IT } from "@/keyboardLayouts/it_IT" +import { nb_NO } from "@/keyboardLayouts/nb_NO" +import { sv_SE } from "@/keyboardLayouts/sv_SE" -export const layouts: Record = { - be_FR: name_fr_BE, - cs_CZ: name_cs_CZ, - en_UK: name_en_UK, - en_US: name_en_US, - fr_FR: name_fr_FR, - de_DE: name_de_DE, - it_IT: name_it_IT, - nb_NO: name_nb_NO, - es_ES: name_es_ES, - sv_SE: name_sv_SE, - fr_CH: name_fr_CH, - de_CH: name_de_CH, -} +export const keyboards: KeyboardLayout[] = [ cs_CZ, de_CH, de_DE, en_UK, en_US, es_ES, fr_BE, fr_CH, fr_FR, it_IT, nb_NO, sv_SE ]; -export const chars: Record> = { - be_FR: chars_fr_BE, - cs_CZ: chars_cs_CZ, - en_UK: chars_en_UK, - en_US: chars_en_US, - fr_FR: chars_fr_FR, - de_DE: chars_de_DE, - it_IT: chars_it_IT, - nb_NO: chars_nb_NO, - es_ES: chars_es_ES, - sv_SE: chars_sv_SE, - fr_CH: chars_fr_CH, - de_CH: chars_de_CH, +export const selectedKeyboard = (isoCode: string): KeyboardLayout => { + // fallback to original behaviour of en-US if no isoCode given + return keyboards.find(keyboard => keyboard.isoCode == isoCode) + ?? keyboards.find(keyboard => keyboard.isoCode == "en-US")!; }; + +export const keyboardOptions = () => { + return keyboards.map((keyboard) => { + return { label: keyboard.name, value: keyboard.isoCode } + }); +} diff --git a/ui/src/keyboardLayouts/cs_CZ.ts b/ui/src/keyboardLayouts/cs_CZ.ts index a289d75f..e4f8822d 100644 --- a/ui/src/keyboardLayouts/cs_CZ.ts +++ b/ui/src/keyboardLayouts/cs_CZ.ts @@ -1,6 +1,6 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Čeština"; +const name = "Čeština"; const keyTrema = { key: "Backslash" } // tréma (umlaut), two dots placed above a vowel const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter @@ -13,7 +13,7 @@ const keyOverdot = { key: "Digit8", shift: true, altRight: true } // overdot (do const keyHook = { key: "Digit6", shift: true, altRight: true } // ogonoek (little hook), mark ˛ placed beneath a letter const keyCedille = { key: "Equal", shift: true, altRight: true } // accent cedille (cedilla), mark ¸ placed beneath a letter -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, "Ä": { key: "KeyA", shift: true, accentKey: keyTrema }, "Á": { key: "KeyA", shift: true, accentKey: keyAcute }, @@ -242,3 +242,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const cs_CZ: KeyboardLayout = { + isoCode: "cs-CZ", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/de_CH.ts b/ui/src/keyboardLayouts/de_CH.ts index 06c06192..4743bcf2 100644 --- a/ui/src/keyboardLayouts/de_CH.ts +++ b/ui/src/keyboardLayouts/de_CH.ts @@ -1,6 +1,6 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Schwiizerdütsch"; +const name = "Schwiizerdütsch"; const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel const keyAcute = { key: "Minus", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter @@ -8,7 +8,7 @@ const keyHat = { key: "Equal" } // accent circonflexe (accent hat), mark ^ place const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter const keyTilde = { key: "Equal", altRight: true } // tilde, mark ~ placed above the letter -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, "Ä": { key: "KeyA", shift: true, accentKey: keyTrema }, "Á": { key: "KeyA", shift: true, accentKey: keyAcute }, @@ -163,3 +163,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const de_CH: KeyboardLayout = { + isoCode: "de-CH", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/de_DE.ts b/ui/src/keyboardLayouts/de_DE.ts index 87a8d2e5..89b7eed2 100644 --- a/ui/src/keyboardLayouts/de_DE.ts +++ b/ui/src/keyboardLayouts/de_DE.ts @@ -1,12 +1,12 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Deutsch"; +const name = "Deutsch"; const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter const keyHat = { key: "Backquote" } // accent circonflexe (accent hat), mark ^ placed above the letter const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, "Á": { key: "KeyA", shift: true, accentKey: keyAcute }, "Â": { key: "KeyA", shift: true, accentKey: keyHat }, @@ -150,3 +150,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const de_DE: KeyboardLayout = { + isoCode: "de-DE", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/en_UK.ts b/ui/src/keyboardLayouts/en_UK.ts index ed0c8dd0..a5ef7791 100644 --- a/ui/src/keyboardLayouts/en_UK.ts +++ b/ui/src/keyboardLayouts/en_UK.ts @@ -1,8 +1,8 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "English (UK)"; +const name = "English (UK)"; -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, B: { key: "KeyB", shift: true }, C: { key: "KeyC", shift: true }, @@ -105,3 +105,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record + +export const en_UK: KeyboardLayout = { + isoCode: "en-UK", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/en_US.ts b/ui/src/keyboardLayouts/en_US.ts index 592bf27e..cd7aaf6d 100644 --- a/ui/src/keyboardLayouts/en_US.ts +++ b/ui/src/keyboardLayouts/en_US.ts @@ -1,8 +1,8 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "English (US)"; +const name = "English (US)"; -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, B: { key: "KeyB", shift: true }, C: { key: "KeyC", shift: true }, @@ -111,3 +111,9 @@ export const chars = { Insert: { key: "Insert", shift: false }, Delete: { key: "Delete", shift: false }, } as Record + +export const en_US: KeyboardLayout = { + isoCode: "en-US", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/es_ES.ts b/ui/src/keyboardLayouts/es_ES.ts index 47fc2303..9eb1d6a0 100644 --- a/ui/src/keyboardLayouts/es_ES.ts +++ b/ui/src/keyboardLayouts/es_ES.ts @@ -1,6 +1,6 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Español"; +const name = "Español"; const keyTrema = { key: "Quote", shift: true } // tréma (umlaut), two dots placed above a vowel const keyAcute = { key: "Quote" } // accent aigu (acute accent), mark ´ placed above the letter @@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen const keyGrave = { key: "BracketRight" } // accent grave, mark ` placed above the letter const keyTilde = { key: "Key4", altRight: true } // tilde, mark ~ placed above the letter -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, "Ä": { key: "KeyA", shift: true, accentKey: keyTrema }, "Á": { key: "KeyA", shift: true, accentKey: keyAcute }, @@ -166,3 +166,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const es_ES: KeyboardLayout = { + isoCode: "es-ES", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/fr_BE.ts b/ui/src/keyboardLayouts/fr_BE.ts index 2b8b34c3..bd417e0d 100644 --- a/ui/src/keyboardLayouts/fr_BE.ts +++ b/ui/src/keyboardLayouts/fr_BE.ts @@ -1,6 +1,6 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Belgisch Nederlands"; +const name = "Belgisch Nederlands"; const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter @@ -8,7 +8,7 @@ const keyAcute = { key: "Semicolon", altRight: true } // accent aigu (acute acce const keyGrave = { key: "Quote", shift: true } // accent grave, mark ` placed above the letter const keyTilde = { key: "Slash", altRight: true } // tilde, mark ~ placed above the letter -export const chars = { +const chars = { A: { key: "KeyQ", shift: true }, "Ä": { key: "KeyQ", shift: true, accentKey: keyTrema }, "Â": { key: "KeyQ", shift: true, accentKey: keyHat }, @@ -165,3 +165,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const fr_BE: KeyboardLayout = { + isoCode: "fr-BE", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/fr_CH.ts b/ui/src/keyboardLayouts/fr_CH.ts index cf1d3df7..0ba8cb4c 100644 --- a/ui/src/keyboardLayouts/fr_CH.ts +++ b/ui/src/keyboardLayouts/fr_CH.ts @@ -1,11 +1,11 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -import { chars as chars_de_CH } from "./de_CH" +import { de_CH } from "./de_CH" -export const name = "Français de Suisse"; +const name = "Français de Suisse"; -export const chars = { - ...chars_de_CH, +const chars = { + ...de_CH.chars, "è": { key: "BracketLeft" }, "ü": { key: "BracketLeft", shift: true }, "é": { key: "Semicolon" }, @@ -13,3 +13,9 @@ export const chars = { "à": { key: "Quote" }, "ä": { key: "Quote", shift: true }, } as Record; + +export const fr_CH: KeyboardLayout = { + isoCode: "fr-CH", + name: name, + chars: chars +}; diff --git a/ui/src/keyboardLayouts/fr_FR.ts b/ui/src/keyboardLayouts/fr_FR.ts index 27a03fda..29d51040 100644 --- a/ui/src/keyboardLayouts/fr_FR.ts +++ b/ui/src/keyboardLayouts/fr_FR.ts @@ -1,11 +1,11 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Français"; +const name = "Français"; const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter -export const chars = { +const chars = { A: { key: "KeyQ", shift: true }, "Ä": { key: "KeyQ", shift: true, accentKey: keyTrema }, "Â": { key: "KeyQ", shift: true, accentKey: keyHat }, @@ -137,3 +137,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const fr_FR: KeyboardLayout = { + isoCode: "fr-FR", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/it_IT.ts b/ui/src/keyboardLayouts/it_IT.ts index 9de61c58..0ff6e244 100644 --- a/ui/src/keyboardLayouts/it_IT.ts +++ b/ui/src/keyboardLayouts/it_IT.ts @@ -1,8 +1,8 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Italiano"; +const name = "Italiano"; -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, B: { key: "KeyB", shift: true }, C: { key: "KeyC", shift: true }, @@ -111,3 +111,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const it_IT: KeyboardLayout = { + isoCode: "it-IT", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/nb_NO.ts b/ui/src/keyboardLayouts/nb_NO.ts index 83918b23..4dae9c8f 100644 --- a/ui/src/keyboardLayouts/nb_NO.ts +++ b/ui/src/keyboardLayouts/nb_NO.ts @@ -1,6 +1,6 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Norsk bokmål"; +const name = "Norsk bokmål"; const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel const keyAcute = { key: "Equal", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter @@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, "Ä": { key: "KeyA", shift: true, accentKey: keyTrema }, "Á": { key: "KeyA", shift: true, accentKey: keyAcute }, @@ -165,3 +165,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const nb_NO: KeyboardLayout = { + isoCode: "nb-NO", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/sv_SE.ts b/ui/src/keyboardLayouts/sv_SE.ts index 75197cb7..fbde3d09 100644 --- a/ui/src/keyboardLayouts/sv_SE.ts +++ b/ui/src/keyboardLayouts/sv_SE.ts @@ -1,6 +1,6 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Svenska"; +const name = "Svenska"; const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter @@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, "Á": { key: "KeyA", shift: true, accentKey: keyAcute }, "Â": { key: "KeyA", shift: true, accentKey: keyHat }, @@ -162,3 +162,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const sv_SE: KeyboardLayout = { + isoCode: "sv-SE", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardMappings.ts b/ui/src/keyboardMappings.ts index 891b96e7..bb24fbb2 100644 --- a/ui/src/keyboardMappings.ts +++ b/ui/src/keyboardMappings.ts @@ -1,17 +1,19 @@ // Key codes and modifiers correspond to definitions in the // [Linux USB HID gadget driver](https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt) +// [Section 10. Keyboard/Keypad Page 0x07](https://usb.org/sites/default/files/hut1_21.pdf) export const keys = { ArrowDown: 0x51, ArrowLeft: 0x50, ArrowRight: 0x4f, ArrowUp: 0x52, - Backquote: 0x35, + Backquote: 0x35, // aka Grave Backslash: 0x31, Backspace: 0x2a, - BracketLeft: 0x2f, - BracketRight: 0x30, + BracketLeft: 0x2f, // aka LeftBrace + BracketRight: 0x30, // aka RightBrace CapsLock: 0x39, Comma: 0x36, + Compose: 0x65, ContextMenu: 0, Delete: 0x4c, Digit0: 0x27, @@ -40,10 +42,21 @@ export const keys = { F10: 0x43, F11: 0x44, F12: 0x45, - F13: 0x68, + F14: 0x69, + F15: 0x6a, + F16: 0x6b, + F17: 0x6c, + F18: 0x6d, + F19: 0x6e, + F20: 0x6f, + F21: 0x70, + F22: 0x71, + F23: 0x72, + F24: 0x73, Home: 0x4a, + HashTilde: 0x32, // non-US # and ~ Insert: 0x49, - IntlBackslash: 0x64, + IntlBackslash: 0x64, // non-US \ and | KeyA: 0x04, KeyB: 0x05, KeyC: 0x06, @@ -72,30 +85,35 @@ export const keys = { KeyZ: 0x1d, KeypadExclamation: 0xcf, Minus: 0x2d, - NumLock: 0x53, - Numpad0: 0x62, - Numpad1: 0x59, - Numpad2: 0x5a, - Numpad3: 0x5b, - Numpad4: 0x5c, + None: 0x00, + NumLock: 0x53, // and Clear + Numpad0: 0x62, // and Insert + Numpad1: 0x59, // and End + Numpad2: 0x5a, // and Down Arrow + Numpad3: 0x5b, // and Page Down + Numpad4: 0x5c, // and Left Arrow Numpad5: 0x5d, - Numpad6: 0x5e, - Numpad7: 0x5f, - Numpad8: 0x60, - Numpad9: 0x61, + Numpad6: 0x5e, // and Right Arrow + Numpad7: 0x5f, // and Home + Numpad8: 0x60, // and Up Arrow + Numpad9: 0x61, // and Page Up NumpadAdd: 0x57, + NumpadComma: 0x85, + NumpadDecimal: 0x63, NumpadDivide: 0x54, NumpadEnter: 0x58, NumpadEqual: 0x67, + NumpadLeftParen: 0xb6, NumpadMultiply: 0x55, + NumpadRightParen: 0xb7, NumpadSubtract: 0x56, - NumpadDecimal: 0x63, PageDown: 0x4e, PageUp: 0x4b, Period: 0x37, PrintScreen: 0x46, Pause: 0x48, - Quote: 0x34, + Power: 0x66, + Quote: 0x34, // aka Single Quote or Apostrophe ScrollLock: 0x47, Semicolon: 0x33, Slash: 0x38, diff --git a/ui/src/routes/devices.$id.settings.keyboard.tsx b/ui/src/routes/devices.$id.settings.keyboard.tsx index 6e68f818..e797ce74 100644 --- a/ui/src/routes/devices.$id.settings.keyboard.tsx +++ b/ui/src/routes/devices.$id.settings.keyboard.tsx @@ -4,7 +4,7 @@ import { KeyboardLedSync, useSettingsStore } from "@/hooks/stores"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import notifications from "@/notifications"; import { SettingsPageHeader } from "@components/SettingsPageheader"; -import { layouts } from "@/keyboardLayouts"; +import { keyboardOptions } from "@/keyboardLayouts"; import { Checkbox } from "@/components/Checkbox"; import { SelectMenuBasic } from "../components/SelectMenuBasic"; @@ -32,7 +32,7 @@ export default function SettingsKeyboardRoute() { return "en_US"; }, [keyboardLayout]); - const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } }) + const layoutOptions = keyboardOptions(); const ledSyncOptions = [ { value: "auto", label: "Automatic" }, { value: "browser", label: "Browser Only" }, diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx index 1e888f60..b729bf0c 100644 --- a/ui/src/routes/devices.$id.settings.tsx +++ b/ui/src/routes/devices.$id.settings.tsx @@ -79,7 +79,7 @@ export default function SettingsRoute() { return () => { setDisableVideoFocusTrap(false); }; - }, [setDisableVideoFocusTrap, sendKeyboardEvent]); + }, [sendKeyboardEvent, setDisableVideoFocusTrap]); return (
diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index 8cdb5b3e..1785bcde 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -707,7 +707,7 @@ export default function KvmIdRoute() { }, [diskChannel, file]); // System update - const disableKeyboardFocusTrap = useUiStore(state => state.disableVideoFocusTrap); + const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap); const [kvmTerminal, setKvmTerminal] = useState(null); const [serialConsole, setSerialConsole] = useState(null); @@ -805,7 +805,7 @@ export default function KvmIdRoute() { )}