Skip to content

Commit c3087ab

Browse files
committed
Enable multiple keyboard layouts for paste text from host
1 parent d79f359 commit c3087ab

File tree

12 files changed

+413
-115
lines changed

12 files changed

+413
-115
lines changed

config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ type Config struct {
8787
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
8888
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
8989
KeyboardMacros []KeyboardMacro `json:"keyboard_macros"`
90+
KeyboardLayout string `json:"keyboard_layout"`
9091
EdidString string `json:"hdmi_edid_string"`
9192
ActiveExtension string `json:"active_extension"`
9293
DisplayMaxBrightness int `json:"display_max_brightness"`
@@ -107,6 +108,7 @@ var defaultConfig = &Config{
107108
AutoUpdateEnabled: true, // Set a default value
108109
ActiveExtension: "",
109110
KeyboardMacros: []KeyboardMacro{},
111+
KeyboardLayout: "en-US",
110112
DisplayMaxBrightness: 64,
111113
DisplayDimAfterSec: 120, // 2 minutes
112114
DisplayOffAfterSec: 1800, // 30 minutes

jsonrpc.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,18 @@ func rpcSetScrollSensitivity(sensitivity string) error {
863863
return nil
864864
}
865865

866+
func rpcGetKeyboardLayout() (string, error) {
867+
return config.KeyboardLayout, nil
868+
}
869+
870+
func rpcSetKeyboardLayout(layout string) error {
871+
config.KeyboardLayout = layout
872+
if err := SaveConfig(); err != nil {
873+
return fmt.Errorf("failed to save config: %w", err)
874+
}
875+
return nil
876+
}
877+
866878
func getKeyboardMacros() (interface{}, error) {
867879
macros := make([]KeyboardMacro, len(config.KeyboardMacros))
868880
copy(macros, config.KeyboardMacros)
@@ -1028,6 +1040,8 @@ var rpcHandlers = map[string]RPCHandler{
10281040
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
10291041
"getScrollSensitivity": {Func: rpcGetScrollSensitivity},
10301042
"setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}},
1043+
"getKeyboardLayout": {Func: rpcGetKeyboardLayout},
1044+
"setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}},
10311045
"getKeyboardMacros": {Func: getKeyboardMacros},
10321046
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
10331047
}

ui/src/components/popovers/PasteModal.tsx

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import { GridCard } from "@components/Card";
88
import { TextAreaWithLabel } from "@components/TextArea";
99
import { SettingsPageHeader } from "@components/SettingsPageheader";
1010
import { useJsonRpc } from "@/hooks/useJsonRpc";
11-
import { useHidStore, useRTCStore, useUiStore } from "@/hooks/stores";
12-
import { chars, keys, modifiers } from "@/keyboardMappings";
11+
import { useHidStore, useRTCStore, useUiStore, useDeviceSettingsStore } from "@/hooks/stores";
12+
import { keys, modifiers } from "@/keyboardMappings";
13+
import { layouts, chars } from "@/keyboardLayouts";
1314
import notifications from "@/notifications";
1415

1516
const hidKeyboardPayload = (keys: number[], modifier: number) => {
@@ -27,6 +28,11 @@ export default function PasteModal() {
2728
const [invalidChars, setInvalidChars] = useState<string[]>([]);
2829
const close = useClose();
2930

31+
const keyboardLayout = useDeviceSettingsStore(state => state.keyboardLayout);
32+
const setKeyboardLayout = useDeviceSettingsStore(
33+
state => state.setKeyboardLayout,
34+
);
35+
3036
const onCancelPasteMode = useCallback(() => {
3137
setPasteMode(false);
3238
setDisableVideoFocusTrap(false);
@@ -42,13 +48,25 @@ export default function PasteModal() {
4248

4349
try {
4450
for (const char of text) {
45-
const { key, shift } = chars[char] ?? {};
51+
const { key, shift, altRight, space, capsLock } = chars[keyboardLayout][char] ?? {};
4652
if (!key) continue;
4753

54+
const keyz = [keys[key]];
55+
if (space) {
56+
keyz.push(keys["Space"]);
57+
}
58+
if (capsLock) {
59+
keyz.unshift(keys["CapsLock"]);
60+
keyz.push(keys["CapsLock"]);
61+
}
62+
63+
const modz = shift ? modifiers["ShiftLeft"] : 0
64+
| (altRight ? modifiers["AltRight"] : 0);
65+
4866
await new Promise<void>((resolve, reject) => {
4967
send(
5068
"keyboardReport",
51-
hidKeyboardPayload([keys[key]], shift ? modifiers["ShiftLeft"] : 0),
69+
hidKeyboardPayload(keyz, modz),
5270
params => {
5371
if ("error" in params) return reject(params.error);
5472
send("keyboardReport", hidKeyboardPayload([], 0), params => {
@@ -69,6 +87,11 @@ export default function PasteModal() {
6987
if (TextAreaRef.current) {
7088
TextAreaRef.current.focus();
7189
}
90+
91+
send("getKeyboardLayout", {}, resp => {
92+
if ("error" in resp) return;
93+
setKeyboardLayout(resp.result as string);
94+
});
7295
}, []);
7396

7497
return (
@@ -113,7 +136,7 @@ export default function PasteModal() {
113136
// @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments
114137
[...new Intl.Segmenter().segment(value)]
115138
.map(x => x.segment)
116-
.filter(char => !chars[char]),
139+
.filter(char => !chars[keyboardLayout][char]),
117140
),
118141
];
119142

@@ -132,6 +155,11 @@ export default function PasteModal() {
132155
)}
133156
</div>
134157
</div>
158+
<div className="space-y-4">
159+
<p className="text-xs text-slate-600 dark:text-slate-400">
160+
Sending key codes for keyboard layout {layouts[keyboardLayout]}
161+
</p>
162+
</div>
135163
</div>
136164
</div>
137165
</div>

ui/src/hooks/stores.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,8 @@ export interface DeviceSettingsState {
336336
trackpadThreshold: number;
337337
scrollSensitivity: "low" | "default" | "high";
338338
setScrollSensitivity: (sensitivity: DeviceSettingsState["scrollSensitivity"]) => void;
339+
keyboardLayout: string;
340+
setKeyboardLayout: (layout: string) => void;
339341
}
340342

341343
export const useDeviceSettingsStore = create<DeviceSettingsState>(set => ({
@@ -397,6 +399,9 @@ export const useDeviceSettingsStore = create<DeviceSettingsState>(set => ({
397399
scrollSensitivity: sensitivity,
398400
});
399401
},
402+
403+
keyboardLayout: "en-US",
404+
setKeyboardLayout: layout => set({ keyboardLayout: layout }),
400405
}));
401406

402407
export interface RemoteVirtualMediaState {
@@ -893,4 +898,4 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
893898
set({ loading: false });
894899
}
895900
}
896-
}));
901+
}));

ui/src/keyboardLayouts.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { chars as chars_en_US } from "@/keyboardLayouts/en_US"
2+
import { chars as chars_de_CH } from "@/keyboardLayouts/de_CH"
3+
4+
export const layouts = {
5+
"en_US": "English (US)",
6+
"de_CH": "Swiss German"
7+
} as Record<string, string>;
8+
9+
export const chars = {
10+
"en_US": chars_en_US,
11+
"de_CH": chars_de_CH,
12+
} as Record<string, Record<string, { key: string | number; shift?: boolean, altRight?: boolean, space?: boolean, capsLock?: boolean }>>;

ui/src/keyboardLayouts/de_CH.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
export const chars = {
2+
A: { key: "KeyA", shift: true },
3+
B: { key: "KeyB", shift: true },
4+
C: { key: "KeyC", shift: true },
5+
D: { key: "KeyD", shift: true },
6+
E: { key: "KeyE", shift: true },
7+
F: { key: "KeyF", shift: true },
8+
G: { key: "KeyG", shift: true },
9+
H: { key: "KeyH", shift: true },
10+
I: { key: "KeyI", shift: true },
11+
J: { key: "KeyJ", shift: true },
12+
K: { key: "KeyK", shift: true },
13+
L: { key: "KeyL", shift: true },
14+
M: { key: "KeyM", shift: true },
15+
N: { key: "KeyN", shift: true },
16+
O: { key: "KeyO", shift: true },
17+
P: { key: "KeyP", shift: true },
18+
Q: { key: "KeyQ", shift: true },
19+
R: { key: "KeyR", shift: true },
20+
S: { key: "KeyS", shift: true },
21+
T: { key: "KeyT", shift: true },
22+
U: { key: "KeyU", shift: true },
23+
V: { key: "KeyV", shift: true },
24+
W: { key: "KeyW", shift: true },
25+
X: { key: "KeyX", shift: true },
26+
Y: { key: "KeyZ", shift: true },
27+
Z: { key: "KeyY", shift: true },
28+
a: { key: "KeyA" },
29+
"æ": { key: "KeyA", altRight: true },
30+
b: { key: "KeyB" },
31+
c: { key: "KeyC" },
32+
d: { key: "KeyD" },
33+
"ð": { key: "KeyD", altRight: true },
34+
e: { key: "KeyE" },
35+
f: { key: "KeyF" },
36+
"đ": { key: "KeyF", altRight: true },
37+
g: { key: "KeyG" },
38+
"ŋ": { key: "KeyG", altRight: true },
39+
h: { key: "KeyH" },
40+
"ħ": { key: "KeyH", altRight: true },
41+
i: { key: "KeyI" },
42+
"→": { key: "KeyI", altRight: true },
43+
j: { key: "KeyJ" },
44+
k: { key: "KeyK" },
45+
"ĸ": { key: "KeyK", altRight: true },
46+
l: { key: "KeyL" },
47+
"ł": { key: "KeyL", altRight: true },
48+
m: { key: "KeyM" },
49+
"µ": { key: "KeyM", altRight: true },
50+
n: { key: "KeyN" },
51+
o: { key: "KeyO" },
52+
"œ": { key: "KeyO", altRight: true },
53+
p: { key: "KeyP" },
54+
"þ": { key: "KeyP", altRight: true },
55+
q: { key: "KeyQ" },
56+
r: { key: "KeyR" },
57+
"¶": { key: "KeyR", altRight: true },
58+
s: { key: "KeyS" },
59+
"ß": { key: "KeyS", altRight: true },
60+
t: { key: "KeyT" },
61+
"ŧ": { key: "KeyT", altRight: true },
62+
u: { key: "KeyU" },
63+
"↓": { key: "KeyU", altRight: true },
64+
v: { key: "KeyV" },
65+
"„": { key: "KeyV", altRight: true },
66+
w: { key: "KeyW" },
67+
"ſ": { key: "KeyW", altRight: true },
68+
x: { key: "KeyX" },
69+
"»": { key: "KeyX", altRight: true },
70+
y: { key: "KeyZ" },
71+
"←": { key: "KeyZ", altRight: true },
72+
z: { key: "KeyY" },
73+
"«": { key: "KeyY", altRight: true },
74+
"§": { key: "Backquote" },
75+
"°": { key: "Backquote", shift: true },
76+
1: { key: "Digit1" },
77+
"+": { key: "Digit1", shift: true },
78+
"|": { key: "Digit1", altRight: true },
79+
2: { key: "Digit2" },
80+
"\"": { key: "Digit2", shift: true },
81+
"@": { key: "Digit2", altRight: true },
82+
3: { key: "Digit3" },
83+
"*": { key: "Digit3", shift: true },
84+
"#": { key: "Digit3", altRight: true },
85+
4: { key: "Digit4" },
86+
"ç": { key: "Digit4", shift: true },
87+
"¼": { key: "Digit4", altRight: true },
88+
5: { key: "Digit5" },
89+
"%": { key: "Digit5", shift: true },
90+
"½": { key: "Digit5", altRight: true },
91+
6: { key: "Digit6" },
92+
"&": { key: "Digit6", shift: true },
93+
"¬": { key: "Digit6", altRight: true },
94+
7: { key: "Digit7" },
95+
"/": { key: "Digit7", shift: true },
96+
8: { key: "Digit8" },
97+
"(": { key: "Digit8", shift: true },
98+
"¢": { key: "Digit8", altRight: true },
99+
9: { key: "Digit9" },
100+
")": { key: "Digit9", shift: true },
101+
0: { key: "Digit0" },
102+
"=": { key: "Digit0", shift: true },
103+
"'": { key: "Minus" },
104+
"?": { key: "Minus", shift: true },
105+
"^": { key: "Equal", space: true }, // dead key
106+
"`": { key: "Equal", shift: true },
107+
"~": { key: "Equal", altRight: true, space: true }, // dead key
108+
"ü": { key: "BracketLeft" },
109+
"è": { key: "BracketLeft", shift: true },
110+
"[": { key: "BracketLeft", altRight: true },
111+
"Ü": { key: "BracketLeft", capsLock: true },
112+
"!": { key: "BracketRight", shift: true },
113+
"]": { key: "BracketRight", altRight: true },
114+
"ö": { key: "Semicolon" },
115+
"é": { key: "Semicolon", shift: true },
116+
"Ö": { key: "Semicolon", capsLock: true },
117+
"ä": { key: "Quote" },
118+
"à": { key: "Quote", shift: true },
119+
"{": { key: "Quote", altRight: true },
120+
"Ä": { key: "Quote", capsLock: true },
121+
"$": { key: "Backslash" },
122+
"£": { key: "Backslash", shift: true },
123+
"}": { key: "Backslash", altRight: true },
124+
",": { key: "Comma" },
125+
";": { key: "Comma", shift: true },
126+
"•": { key: "Comma", altRight: true },
127+
".": { key: "Period" },
128+
":": { key: "Period", shift: true },
129+
"·": { key: "Period", altRight: true },
130+
"-": { key: "Slash" },
131+
"_": { key: "Slash", shift: true },
132+
"<": { key: "IntlBackslash" },
133+
">": { key: "IntlBackslash", shift: true },
134+
"\\": { key: "IntlBackslash", altRight: true },
135+
"€": { key: "KeyE", altRight: true },
136+
" ": { key: "Space" },
137+
"\n": { key: "Enter" },
138+
Enter: { key: "Enter" },
139+
Tab: { key: "Tab" },
140+
} as Record<string, { key: string | number; shift?: boolean, altRight?: boolean, space?: boolean, capsLock?: boolean }>

0 commit comments

Comments
 (0)