Skip to content

Commit e7c2d2f

Browse files
IDisposableadammkelly
authored andcommitted
refactor(ui): Refactor the keyboardLayouts (jetkvm#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.
1 parent 9e8e142 commit e7c2d2f

21 files changed

+232
-148
lines changed

ui/src/components/Terminal.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,19 +67,19 @@ function Terminal({
6767
}) {
6868
const enableTerminal = useUiStore(state => state.terminalType == type);
6969
const setTerminalType = useUiStore(state => state.setTerminalType);
70-
const setDisableKeyboardFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
70+
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
7171

7272
const { instance, ref } = useXTerm({ options: TERMINAL_CONFIG });
7373

7474
useEffect(() => {
7575
setTimeout(() => {
76-
setDisableKeyboardFocusTrap(enableTerminal);
76+
setDisableVideoFocusTrap(enableTerminal);
7777
}, 500);
7878

7979
return () => {
80-
setDisableKeyboardFocusTrap(false);
80+
setDisableVideoFocusTrap(false);
8181
};
82-
}, [ref, instance, enableTerminal, setDisableKeyboardFocusTrap, type]);
82+
}, [enableTerminal, setDisableVideoFocusTrap]);
8383

8484
const readyState = dataChannel.readyState;
8585
useEffect(() => {
@@ -116,7 +116,7 @@ function Terminal({
116116
const { domEvent } = e;
117117
if (domEvent.key === "Escape") {
118118
setTerminalType("none");
119-
setDisableKeyboardFocusTrap(false);
119+
setDisableVideoFocusTrap(false);
120120
domEvent.preventDefault();
121121
}
122122
});
@@ -131,7 +131,7 @@ function Terminal({
131131
onDataHandler.dispose();
132132
onKeyHandler.dispose();
133133
};
134-
}, [instance, dataChannel, readyState, setDisableKeyboardFocusTrap, setTerminalType]);
134+
}, [dataChannel, instance, readyState, setDisableVideoFocusTrap, setTerminalType]);
135135

136136
useEffect(() => {
137137
if (!instance) return;
@@ -158,7 +158,7 @@ function Terminal({
158158
return () => {
159159
window.removeEventListener("resize", handleResize);
160160
};
161-
}, [ref, instance]);
161+
}, [instance]);
162162

163163
return (
164164
<div

ui/src/components/popovers/PasteModal.tsx

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import { SettingsPageHeader } from "@components/SettingsPageheader";
1010
import { useJsonRpc } from "@/hooks/useJsonRpc";
1111
import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores";
1212
import { keys, modifiers } from "@/keyboardMappings";
13-
import { layouts, chars } from "@/keyboardLayouts";
13+
import { KeyStroke, KeyboardLayout, selectedKeyboard } from "@/keyboardLayouts";
1414
import notifications from "@/notifications";
1515

16-
const hidKeyboardPayload = (keys: number[], modifier: number) => {
17-
return { keys, modifier };
16+
const hidKeyboardPayload = (modifier: number, keys: number[]) => {
17+
return { modifier, keys };
1818
};
1919

2020
const modifierCode = (shift?: boolean, altRight?: boolean) => {
@@ -62,49 +62,56 @@ export default function PasteModal() {
6262
const onConfirmPaste = useCallback(async () => {
6363
setPasteMode(false);
6464
setDisableVideoFocusTrap(false);
65+
6566
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
66-
if (!safeKeyboardLayout) return;
67-
if (!chars[safeKeyboardLayout]) return;
67+
const keyboard: KeyboardLayout = selectedKeyboard(safeKeyboardLayout);
68+
if (!keyboard) return;
69+
6870
const text = TextAreaRef.current.value;
6971

7072
try {
7173
for (const char of text) {
72-
const { key, shift, altRight, deadKey, accentKey } = chars[safeKeyboardLayout][char]
73-
if (!key) continue;
74+
const keyprops = keyboard.chars[char];
75+
if (!keyprops) continue;
7476

75-
const keyz = [ keys[key] ];
76-
const modz = [ modifierCode(shift, altRight) ];
77+
const { key, shift, altRight, deadKey, accentKey } = keyprops;
78+
if (!key) continue;
7779

78-
if (deadKey) {
79-
keyz.push(keys["Space"]);
80-
modz.push(noModifier);
81-
}
80+
// if this is an accented character, we need to send that accent FIRST
8281
if (accentKey) {
83-
keyz.unshift(keys[accentKey.key])
84-
modz.unshift(modifierCode(accentKey.shift, accentKey.altRight))
82+
await sendKeystroke({modifier: modifierCode(accentKey.shift, accentKey.altRight), keys: [ keys[accentKey.key] ] })
8583
}
8684

87-
for (const [index, kei] of keyz.entries()) {
88-
await new Promise<void>((resolve, reject) => {
89-
send(
90-
"keyboardReport",
91-
hidKeyboardPayload([kei], modz[index]),
92-
params => {
93-
if ("error" in params) return reject(params.error);
94-
send("keyboardReport", hidKeyboardPayload([], 0), params => {
95-
if ("error" in params) return reject(params.error);
96-
resolve();
97-
});
98-
},
99-
);
100-
});
85+
// now send the actual key
86+
await sendKeystroke({ modifier: modifierCode(shift, altRight), keys: [ keys[key] ]});
87+
88+
// if what was requested was a dead key, we need to send an unmodified space to emit
89+
// just the accent character
90+
if (deadKey) {
91+
await sendKeystroke({ modifier: noModifier, keys: [ keys["Space"] ] });
10192
}
93+
94+
// now send a message with no keys down to "release" the keys
95+
await sendKeystroke({ modifier: 0, keys: [] });
10296
}
10397
} catch (error) {
104-
console.error(error);
98+
console.error("Failed to paste text:", error);
10599
notifications.error("Failed to paste text");
106100
}
107-
}, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, safeKeyboardLayout]);
101+
102+
async function sendKeystroke(stroke: KeyStroke) {
103+
await new Promise<void>((resolve, reject) => {
104+
send(
105+
"keyboardReport",
106+
hidKeyboardPayload(stroke.modifier, stroke.keys),
107+
params => {
108+
if ("error" in params) return reject(params.error);
109+
resolve();
110+
}
111+
);
112+
});
113+
}
114+
}, [rpcDataChannel?.readyState, safeKeyboardLayout, send, setDisableVideoFocusTrap, setPasteMode]);
108115

109116
useEffect(() => {
110117
if (TextAreaRef.current) {
@@ -154,7 +161,7 @@ export default function PasteModal() {
154161
// @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments
155162
[...new Intl.Segmenter().segment(value)]
156163
.map(x => x.segment)
157-
.filter(char => !chars[safeKeyboardLayout][char]),
164+
.filter(char => !selectedKeyboard(safeKeyboardLayout).chars[char]),
158165
),
159166
];
160167

@@ -175,7 +182,7 @@ export default function PasteModal() {
175182
</div>
176183
<div className="space-y-4">
177184
<p className="text-xs text-slate-600 dark:text-slate-400">
178-
Sending text using keyboard layout: {layouts[safeKeyboardLayout]}
185+
Sending text using keyboard layout: {selectedKeyboard(safeKeyboardLayout).name}
179186
</p>
180187
</div>
181188
</div>

ui/src/components/popovers/WakeOnLan/Index.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import AddDeviceForm from "./AddDeviceForm";
1414
export default function WakeOnLanModal() {
1515
const [storedDevices, setStoredDevices] = useState<StoredDevice[]>([]);
1616
const [showAddForm, setShowAddForm] = useState(false);
17-
const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
17+
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
1818

1919
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
2020

@@ -24,9 +24,9 @@ export default function WakeOnLanModal() {
2424
const [addDeviceErrorMessage, setAddDeviceErrorMessage] = useState<string | null>(null);
2525

2626
const onCancelWakeOnLanModal = useCallback(() => {
27+
setDisableVideoFocusTrap(false);
2728
close();
28-
setDisableFocusTrap(false);
29-
}, [close, setDisableFocusTrap]);
29+
}, [close, setDisableVideoFocusTrap]);
3030

3131
const onSendMagicPacket = useCallback(
3232
(macAddress: string) => {
@@ -43,12 +43,12 @@ export default function WakeOnLanModal() {
4343
}
4444
} else {
4545
notifications.success("Magic Packet sent successfully");
46-
setDisableFocusTrap(false);
46+
setDisableVideoFocusTrap(false);
4747
close();
4848
}
4949
});
5050
},
51-
[close, rpcDataChannel?.readyState, send, setDisableFocusTrap],
51+
[close, rpcDataChannel?.readyState, send, setDisableVideoFocusTrap],
5252
);
5353

5454
const syncStoredDevices = useCallback(() => {
@@ -78,7 +78,7 @@ export default function WakeOnLanModal() {
7878
}
7979
});
8080
},
81-
[storedDevices, send, syncStoredDevices],
81+
[send, storedDevices, syncStoredDevices],
8282
);
8383

8484
const onAddDevice = useCallback(

ui/src/hooks/stores.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -936,5 +936,5 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
936936
} finally {
937937
set({ loading: false });
938938
}
939-
},
939+
}
940940
}));

ui/src/keyboardLayouts.ts

Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,32 @@
1-
import { chars as chars_fr_BE, name as name_fr_BE } from "@/keyboardLayouts/fr_BE"
2-
import { chars as chars_cs_CZ, name as name_cs_CZ } from "@/keyboardLayouts/cs_CZ"
3-
import { chars as chars_en_UK, name as name_en_UK } from "@/keyboardLayouts/en_UK"
4-
import { chars as chars_en_US, name as name_en_US } from "@/keyboardLayouts/en_US"
5-
import { chars as chars_fr_FR, name as name_fr_FR } from "@/keyboardLayouts/fr_FR"
6-
import { chars as chars_de_DE, name as name_de_DE } from "@/keyboardLayouts/de_DE"
7-
import { chars as chars_it_IT, name as name_it_IT } from "@/keyboardLayouts/it_IT"
8-
import { chars as chars_nb_NO, name as name_nb_NO } from "@/keyboardLayouts/nb_NO"
9-
import { chars as chars_es_ES, name as name_es_ES } from "@/keyboardLayouts/es_ES"
10-
import { chars as chars_sv_SE, name as name_sv_SE } from "@/keyboardLayouts/sv_SE"
11-
import { chars as chars_fr_CH, name as name_fr_CH } from "@/keyboardLayouts/fr_CH"
12-
import { chars as chars_de_CH, name as name_de_CH } from "@/keyboardLayouts/de_CH"
1+
export interface KeyStroke { modifier: number; keys: number[]; }
2+
export interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean }
3+
export interface KeyCombo extends KeyInfo { deadKey?: boolean, accentKey?: KeyInfo }
4+
export interface KeyboardLayout { isoCode: string, name: string, chars: Record<string, KeyCombo> }
135

14-
interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean }
15-
export type KeyCombo = KeyInfo & { deadKey?: boolean, accentKey?: KeyInfo }
6+
// to add a new layout, create a file like the above and add it to the list
7+
import { cs_CZ } from "@/keyboardLayouts/cs_CZ"
8+
import { de_CH } from "@/keyboardLayouts/de_CH"
9+
import { de_DE } from "@/keyboardLayouts/de_DE"
10+
import { en_US } from "@/keyboardLayouts/en_US"
11+
import { en_UK } from "@/keyboardLayouts/en_UK"
12+
import { es_ES } from "@/keyboardLayouts/es_ES"
13+
import { fr_BE } from "@/keyboardLayouts/fr_BE"
14+
import { fr_CH } from "@/keyboardLayouts/fr_CH"
15+
import { fr_FR } from "@/keyboardLayouts/fr_FR"
16+
import { it_IT } from "@/keyboardLayouts/it_IT"
17+
import { nb_NO } from "@/keyboardLayouts/nb_NO"
18+
import { sv_SE } from "@/keyboardLayouts/sv_SE"
1619

17-
export const layouts: Record<string, string> = {
18-
be_FR: name_fr_BE,
19-
cs_CZ: name_cs_CZ,
20-
en_UK: name_en_UK,
21-
en_US: name_en_US,
22-
fr_FR: name_fr_FR,
23-
de_DE: name_de_DE,
24-
it_IT: name_it_IT,
25-
nb_NO: name_nb_NO,
26-
es_ES: name_es_ES,
27-
sv_SE: name_sv_SE,
28-
fr_CH: name_fr_CH,
29-
de_CH: name_de_CH,
30-
}
20+
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 ];
3121

32-
export const chars: Record<string, Record<string, KeyCombo>> = {
33-
be_FR: chars_fr_BE,
34-
cs_CZ: chars_cs_CZ,
35-
en_UK: chars_en_UK,
36-
en_US: chars_en_US,
37-
fr_FR: chars_fr_FR,
38-
de_DE: chars_de_DE,
39-
it_IT: chars_it_IT,
40-
nb_NO: chars_nb_NO,
41-
es_ES: chars_es_ES,
42-
sv_SE: chars_sv_SE,
43-
fr_CH: chars_fr_CH,
44-
de_CH: chars_de_CH,
22+
export const selectedKeyboard = (isoCode: string): KeyboardLayout => {
23+
// fallback to original behaviour of en-US if no isoCode given
24+
return keyboards.find(keyboard => keyboard.isoCode == isoCode)
25+
?? keyboards.find(keyboard => keyboard.isoCode == "en-US")!;
4526
};
27+
28+
export const keyboardOptions = () => {
29+
return keyboards.map((keyboard) => {
30+
return { label: keyboard.name, value: keyboard.isoCode }
31+
});
32+
}

ui/src/keyboardLayouts/cs_CZ.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { KeyCombo } from "../keyboardLayouts"
1+
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
22

3-
export const name = "Čeština";
3+
const name = "Čeština";
44

55
const keyTrema = { key: "Backslash" } // tréma (umlaut), two dots placed above a vowel
66
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
1313
const keyHook = { key: "Digit6", shift: true, altRight: true } // ogonoek (little hook), mark ˛ placed beneath a letter
1414
const keyCedille = { key: "Equal", shift: true, altRight: true } // accent cedille (cedilla), mark ¸ placed beneath a letter
1515

16-
export const chars = {
16+
const chars = {
1717
A: { key: "KeyA", shift: true },
1818
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
1919
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
@@ -242,3 +242,9 @@ export const chars = {
242242
Enter: { key: "Enter" },
243243
Tab: { key: "Tab" },
244244
} as Record<string, KeyCombo>;
245+
246+
export const cs_CZ: KeyboardLayout = {
247+
isoCode: "cs-CZ",
248+
name: name,
249+
chars: chars
250+
};

ui/src/keyboardLayouts/de_CH.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import { KeyCombo } from "../keyboardLayouts"
1+
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
22

3-
export const name = "Schwiizerdütsch";
3+
const name = "Schwiizerdütsch";
44

55
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
66
const keyAcute = { key: "Minus", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
77
const keyHat = { key: "Equal" } // accent circonflexe (accent hat), mark ^ placed above the letter
88
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
99
const keyTilde = { key: "Equal", altRight: true } // tilde, mark ~ placed above the letter
1010

11-
export const chars = {
11+
const chars = {
1212
A: { key: "KeyA", shift: true },
1313
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
1414
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
@@ -163,3 +163,9 @@ export const chars = {
163163
Enter: { key: "Enter" },
164164
Tab: { key: "Tab" },
165165
} as Record<string, KeyCombo>;
166+
167+
export const de_CH: KeyboardLayout = {
168+
isoCode: "de-CH",
169+
name: name,
170+
chars: chars
171+
};

ui/src/keyboardLayouts/de_DE.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { KeyCombo } from "../keyboardLayouts"
1+
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
22

3-
export const name = "Deutsch";
3+
const name = "Deutsch";
44

55
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
66
const keyHat = { key: "Backquote" } // accent circonflexe (accent hat), mark ^ placed above the letter
77
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
88

9-
export const chars = {
9+
const chars = {
1010
A: { key: "KeyA", shift: true },
1111
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
1212
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
@@ -150,3 +150,9 @@ export const chars = {
150150
Enter: { key: "Enter" },
151151
Tab: { key: "Tab" },
152152
} as Record<string, KeyCombo>;
153+
154+
export const de_DE: KeyboardLayout = {
155+
isoCode: "de-DE",
156+
name: name,
157+
chars: chars
158+
};

0 commit comments

Comments
 (0)