refactor(hid): improve keyboard layout compatibility in HID handling functions

Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
luckfox-eng29
2026-04-29 20:03:13 +08:00
parent a1da483b27
commit 6292537c23
33 changed files with 2226 additions and 96 deletions

47
internal/hidrpc/hidrpc.go Normal file
View File

@@ -0,0 +1,47 @@
package hidrpc
import "fmt"
type Handler interface {
HandleKeyboardReport(modifier byte, keys []byte) error
HandleKeypressReport(key byte, press bool) error
HandleKeypressKeepAlive() error
HandleKeyboardMacroReport(data []byte) error
HandleCancelKeyboardMacro() error
}
type Server struct {
handler Handler
}
func NewServer(handler Handler) *Server {
return &Server{handler: handler}
}
func (s *Server) HandleMessage(data []byte) error {
msg, err := UnmarshalMessage(data)
if err != nil {
return err
}
switch msg.Type {
case MessageTypeKeyboardReport:
if len(msg.Data) < 7 {
return fmt.Errorf("invalid keyboard report length: %d", len(msg.Data))
}
return s.handler.HandleKeyboardReport(msg.Data[0], msg.Data[1:7])
case MessageTypeKeypressReport:
if len(msg.Data) < 2 {
return fmt.Errorf("invalid keypress report length: %d", len(msg.Data))
}
return s.handler.HandleKeypressReport(msg.Data[0], msg.Data[1] != 0)
case MessageTypeKeypressKeepAlive:
return s.handler.HandleKeypressKeepAlive()
case MessageTypeKeyboardMacroReport:
return s.handler.HandleKeyboardMacroReport(msg.Data)
case MessageTypeCancelKeyboardMacro:
return s.handler.HandleCancelKeyboardMacro()
default:
return fmt.Errorf("unknown message type: 0x%02x", msg.Type)
}
}

View File

@@ -0,0 +1,66 @@
package hidrpc
import "fmt"
const (
MessageTypeHandshake = 0x01
MessageTypeKeyboardReport = 0x02
MessageTypePointerReport = 0x03
MessageTypeWheelReport = 0x04
MessageTypeKeypressReport = 0x05
MessageTypeMouseReport = 0x06
MessageTypeKeyboardMacroReport = 0x07
MessageTypeCancelKeyboardMacro = 0x08
MessageTypeKeypressKeepAlive = 0x09
MessageTypeKeyboardLedState = 0x32
MessageTypeKeysDownState = 0x33
MessageTypeKeyboardMacroState = 0x34
)
type Message struct {
Type byte
Data []byte
}
func MarshalKeyboardReport(modifier byte, keys []byte) []byte {
data := make([]byte, 8)
data[0] = MessageTypeKeyboardReport
data[1] = modifier
copy(data[2:], keys)
return data
}
func MarshalKeypressReport(key byte, press bool) []byte {
data := make([]byte, 3)
data[0] = MessageTypeKeypressReport
data[1] = key
if press {
data[2] = 1
} else {
data[2] = 0
}
return data
}
func MarshalKeypressKeepAlive() []byte {
return []byte{MessageTypeKeypressKeepAlive}
}
func MarshalKeyboardLedState(state byte) []byte {
return []byte{MessageTypeKeyboardLedState, state}
}
func MarshalKeysDownState(modifier byte, keys []byte) []byte {
data := make([]byte, 8)
data[0] = MessageTypeKeysDownState
data[1] = modifier
copy(data[2:], keys)
return data
}
func UnmarshalMessage(data []byte) (Message, error) {
if len(data) < 1 {
return Message{}, fmt.Errorf("empty message")
}
return Message{Type: data[0], Data: data[1:]}, nil
}

View File

@@ -226,15 +226,153 @@ func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
return nil
}
func (u *UsbGadget) KeyboardReport(modifier uint8, keys []uint8) error {
type autoReleaseTimer struct {
timer *time.Timer
key byte
active bool
}
type KeysDownState struct {
Modifier byte
Keys [6]byte
}
func (u *UsbGadget) scheduleAutoRelease(key byte) {
// Cancel existing timer for this key
for i := range u.autoReleaseTimers {
if u.autoReleaseTimers[i].key == key && u.autoReleaseTimers[i].active {
u.autoReleaseTimers[i].timer.Stop()
u.autoReleaseTimers[i].active = false
}
}
// Schedule new timer
timer := time.AfterFunc(100*time.Millisecond, func() {
u.autoReleaseKey(key)
})
u.autoReleaseTimers = append(u.autoReleaseTimers, autoReleaseTimer{
timer: timer,
key: key,
active: true,
})
}
func (u *UsbGadget) autoReleaseKey(key byte) {
u.keyboardLock.Lock()
defer u.keyboardLock.Unlock()
// Remove key from buffer
found := false
for i := 0; i < len(u.keysDownState.Keys); i++ {
if u.keysDownState.Keys[i] == key {
found = true
}
if found && i < len(u.keysDownState.Keys)-1 {
u.keysDownState.Keys[i] = u.keysDownState.Keys[i+1]
}
}
if found {
u.keysDownState.Keys[len(u.keysDownState.Keys)-1] = 0
u.keyboardWriteHidFileLocked(u.keysDownState.Modifier, u.keysDownState.Keys[:])
}
// Mark timer as inactive
for i := range u.autoReleaseTimers {
if u.autoReleaseTimers[i].key == key && u.autoReleaseTimers[i].active {
u.autoReleaseTimers[i].active = false
}
}
}
func (u *UsbGadget) cancelAutoRelease(key byte) {
for i := range u.autoReleaseTimers {
if u.autoReleaseTimers[i].key == key && u.autoReleaseTimers[i].active {
u.autoReleaseTimers[i].timer.Stop()
u.autoReleaseTimers[i].active = false
}
}
}
func (u *UsbGadget) resetAllAutoReleaseTimers() {
for i := range u.autoReleaseTimers {
if u.autoReleaseTimers[i].active {
u.autoReleaseTimers[i].timer.Stop()
u.autoReleaseTimers[i].active = false
}
}
}
func (u *UsbGadget) KeypressReport(key byte, press bool) error {
u.keyboardLock.Lock()
defer u.keyboardLock.Unlock()
if press {
// Check if key already in buffer
for _, k := range u.keysDownState.Keys {
if k == key {
return nil // Already pressed
}
}
// Find empty slot
emptySlot := -1
for i, k := range u.keysDownState.Keys {
if k == 0 {
emptySlot = i
break
}
}
if emptySlot == -1 {
// Buffer full - ErrorRollOver
u.keysDownState.Keys = [6]byte{0x01, 0x01, 0x01, 0x01, 0x01, 0x01}
} else {
u.keysDownState.Keys[emptySlot] = key
}
u.scheduleAutoRelease(key)
} else {
// Remove key from buffer
found := false
for i := 0; i < len(u.keysDownState.Keys); i++ {
if u.keysDownState.Keys[i] == key {
found = true
}
if found && i < len(u.keysDownState.Keys)-1 {
u.keysDownState.Keys[i] = u.keysDownState.Keys[i+1]
}
}
if found {
u.keysDownState.Keys[len(u.keysDownState.Keys)-1] = 0
}
u.cancelAutoRelease(key)
}
return u.keyboardWriteHidFileLocked(u.keysDownState.Modifier, u.keysDownState.Keys[:])
}
func (u *UsbGadget) KeypressKeepAlive() error {
u.keyboardLock.Lock()
defer u.keyboardLock.Unlock()
// Reset auto-release timers for all currently held keys
for _, key := range u.keysDownState.Keys {
if key != 0 {
u.scheduleAutoRelease(key)
}
}
return nil
}
func (u *UsbGadget) keyboardWriteHidFileLocked(modifier byte, keys []byte) error {
if len(keys) > 6 {
keys = keys[:6]
}
if len(keys) < 6 {
keys = append(keys, make([]uint8, 6-len(keys))...)
keys = append(keys, make([]byte, 6-len(keys))...)
}
err := u.keyboardWriteHidFile([]byte{modifier, 0, keys[0], keys[1], keys[2], keys[3], keys[4], keys[5]})
@@ -245,3 +383,13 @@ func (u *UsbGadget) KeyboardReport(modifier uint8, keys []uint8) error {
u.resetUserInputTime()
return nil
}
func (u *UsbGadget) KeyboardReport(modifier uint8, keys []uint8) error {
u.keyboardLock.Lock()
defer u.keyboardLock.Unlock()
u.keysDownState.Modifier = modifier
copy(u.keysDownState.Keys[:], keys)
return u.keyboardWriteHidFileLocked(modifier, keys)
}

View File

@@ -82,6 +82,9 @@ type UsbGadget struct {
onKeyboardStateChange *func(state KeyboardState)
onHidDeviceMissing *func(device string, err error)
keysDownState KeysDownState
autoReleaseTimers []autoReleaseTimer
log *zerolog.Logger
logSuppressionCounter map[string]int

70
ui/src/hooks/hidRpc.ts Normal file
View File

@@ -0,0 +1,70 @@
export const MessageType = {
Handshake: 0x01,
KeyboardReport: 0x02,
PointerReport: 0x03,
WheelReport: 0x04,
KeypressReport: 0x05,
MouseReport: 0x06,
KeyboardMacroReport: 0x07,
CancelKeyboardMacro: 0x08,
KeypressKeepAlive: 0x09,
KeyboardLedState: 0x32,
KeysDownState: 0x33,
KeyboardMacroState: 0x34,
} as const;
export function marshalKeypressReport(key: number, press: boolean): Uint8Array {
return new Uint8Array([MessageType.KeypressReport, key, press ? 1 : 0]);
}
export function marshalKeyboardReport(modifier: number, keys: number[]): Uint8Array {
const data = new Uint8Array(8);
data[0] = MessageType.KeyboardReport;
data[1] = modifier;
for (let i = 0; i < Math.min(keys.length, 6); i++) {
data[2 + i] = keys[i];
}
return data;
}
export function marshalKeypressKeepAlive(): Uint8Array {
return new Uint8Array([MessageType.KeypressKeepAlive]);
}
export function marshalHandshake(version: number): Uint8Array {
return new Uint8Array([MessageType.Handshake, version]);
}
export interface HidRpcMessage {
type: number;
data: Uint8Array;
}
export function unmarshalMessage(data: ArrayBuffer): HidRpcMessage {
const view = new Uint8Array(data);
return {
type: view[0],
data: view.slice(1),
};
}
export interface KeyboardLedState {
num_lock: boolean;
caps_lock: boolean;
scroll_lock: boolean;
compose: boolean;
kana: boolean;
shift: boolean;
}
export function parseKeyboardLedState(data: Uint8Array): KeyboardLedState {
const raw = data[0] || 0;
return {
num_lock: !!(raw & (1 << 0)),
caps_lock: !!(raw & (1 << 1)),
scroll_lock: !!(raw & (1 << 2)),
compose: !!(raw & (1 << 3)),
kana: !!(raw & (1 << 4)),
shift: !!(raw & (1 << 6)),
};
}

View File

@@ -185,6 +185,9 @@ interface RTCState {
serialConsole: RTCDataChannel | null;
setSerialConsole: (channel: RTCDataChannel | null) => void;
hidChannel: RTCDataChannel | null;
setHidChannel: (channel: RTCDataChannel | null) => void;
}
export const useRTCStore = create<RTCState>(set => ({
@@ -194,6 +197,9 @@ export const useRTCStore = create<RTCState>(set => ({
rpcDataChannel: null,
setRpcDataChannel: channel => set({ rpcDataChannel: channel }),
hidChannel: null,
setHidChannel: channel => set({ hidChannel: channel }),
transceiver: null,
setTransceiver: transceiver => set({ transceiver }),
@@ -566,6 +572,12 @@ export interface HidState {
keyboardLedStateSyncAvailable: boolean;
setKeyboardLedStateSyncAvailable: (available: boolean) => void;
rpcHidReady: boolean;
setRpcHidReady: (ready: boolean) => void;
keysDownState?: { modifier: number; keys: number[] };
setKeysDownState: (state: { modifier: number; keys: number[] }) => void;
isVirtualKeyboardEnabled: boolean;
setVirtualKeyboardEnabled: (enabled: boolean) => void;
@@ -622,6 +634,12 @@ export const useHidStore = create<HidState>((set, get) => ({
set({ keyboardLedState });
},
rpcHidReady: false,
setRpcHidReady: ready => set({ rpcHidReady: ready }),
keysDownState: undefined,
setKeysDownState: state => set({ keysDownState: state }),
keyboardLedStateSyncAvailable: false,
setKeyboardLedStateSyncAvailable: available => set({ keyboardLedStateSyncAvailable: available }),

109
ui/src/hooks/useHidRpc.ts Normal file
View File

@@ -0,0 +1,109 @@
import { useCallback, useEffect, useRef } from "react";
import { useRTCStore, useHidStore } from "./stores";
import {
marshalKeypressReport,
marshalKeyboardReport,
marshalKeypressKeepAlive,
marshalHandshake,
unmarshalMessage,
MessageType,
parseKeyboardLedState,
} from "./hidRpc";
export function useHidRpc() {
const hidChannel = useRTCStore(state => state.hidChannel);
const setRpcHidReady = useHidStore(state => state.setRpcHidReady);
const setKeyboardLedState = useHidStore(state => state.setKeyboardLedState);
const setKeysDownState = useHidStore(state => state.setKeysDownState);
const rpcHidReadyRef = useRef(false);
// Send keypress event
const reportKeypressEvent = useCallback(
(key: number, press: boolean) => {
if (!rpcHidReadyRef.current || !hidChannel || hidChannel.readyState !== "open") {
return false;
}
const data = marshalKeypressReport(key, press);
hidChannel.send(data);
return true;
},
[hidChannel]
);
// Send keyboard report (for legacy compatibility)
const reportKeyboardEvent = useCallback(
(modifier: number, keys: number[]) => {
if (!rpcHidReadyRef.current || !hidChannel || hidChannel.readyState !== "open") {
return false;
}
const data = marshalKeyboardReport(modifier, keys);
hidChannel.send(data);
return true;
},
[hidChannel]
);
// Send keepalive
const reportKeypressKeepAlive = useCallback(() => {
if (!rpcHidReadyRef.current || !hidChannel || hidChannel.readyState !== "open") {
return false;
}
const data = marshalKeypressKeepAlive();
hidChannel.send(data);
return true;
}, [hidChannel]);
// Handle incoming HID-RPC messages
useEffect(() => {
if (!hidChannel) return;
const messageHandler = (event: MessageEvent) => {
const msg = unmarshalMessage(event.data);
switch (msg.type) {
case MessageType.Handshake:
if (msg.data[0] === 1) {
rpcHidReadyRef.current = true;
setRpcHidReady(true);
}
break;
case MessageType.KeyboardLedState:
setKeyboardLedState(parseKeyboardLedState(msg.data));
break;
case MessageType.KeysDownState:
// Parse modifier + 6 keys
if (msg.data.length >= 7) {
setKeysDownState({
modifier: msg.data[0],
keys: Array.from(msg.data.slice(1, 7)),
});
}
break;
}
};
hidChannel.addEventListener("message", messageHandler);
// Send handshake
if (hidChannel.readyState === "open") {
hidChannel.send(marshalHandshake(1));
} else {
hidChannel.addEventListener("open", () => {
hidChannel.send(marshalHandshake(1));
}, { once: true });
}
return () => {
hidChannel.removeEventListener("message", messageHandler);
rpcHidReadyRef.current = false;
setRpcHidReady(false);
};
}, [hidChannel, setRpcHidReady, setKeyboardLedState, setKeysDownState]);
return {
rpcHidReady: rpcHidReadyRef.current,
reportKeypressEvent,
reportKeyboardEvent,
reportKeypressKeepAlive,
};
}

View File

@@ -1,12 +1,14 @@
import { useCallback } from "react";
import { useCallback, useEffect, useRef } from "react";
import notifications from "@/notifications";
import { useHidStore, useRTCStore, useSettingsStore } from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { useHidRpc } from "@/hooks/useHidRpc";
import { keys, modifiers } from "@/keyboardMappings";
export default function useKeyboard() {
const [send] = useJsonRpc();
const { rpcHidReady, reportKeypressEvent, reportKeyboardEvent, reportKeypressKeepAlive } = useHidRpc();
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
const forceHttp = useSettingsStore(state => state.forceHttp);
@@ -16,6 +18,10 @@ export default function useKeyboard() {
const isReinitializingGadget = useHidStore(state => state.isReinitializingGadget);
const usbState = useHidStore(state => state.usbState);
// Track held keys for keepalive
const heldKeysRef = useRef<Set<number>>(new Set());
const keepaliveIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const sendKeyboardEvent = useCallback(
(keys: number[], modifiers: number[]) => {
if (!forceHttp && rpcDataChannel?.readyState !== "open") return;
@@ -24,24 +30,87 @@ export default function useKeyboard() {
if (usbState !== "configured") return;
const accModifier = modifiers.reduce((acc, val) => acc + val, 0);
send("keyboardReport", { keys, modifier: accModifier }, resp => {
if ("error" in resp) {
const msg = (resp.error.data as string) || resp.error.message || "";
if (msg.includes("cannot send after transport endpoint shutdown") && usbState === "configured") {
notifications.error("Please check if the cable and connection are stable.", { duration: 5000 });
// Try HID-RPC first
if (rpcHidReady && !forceHttp) {
reportKeyboardEvent(accModifier, keys);
} else {
// Fallback to JSON-RPC
send("keyboardReport", { keys, modifier: accModifier }, resp => {
if ("error" in resp) {
const msg = (resp.error.data as string) || resp.error.message || "";
if (msg.includes("cannot send after transport endpoint shutdown") && usbState === "configured") {
notifications.error("Please check if the cable and connection are stable.", { duration: 5000 });
}
}
}
});
});
}
// We do this for the info bar to display the currently pressed keys for the user
updateActiveKeysAndModifiers({ keys: keys, modifiers: modifiers });
},
[forceHttp, rpcDataChannel?.readyState, send, updateActiveKeysAndModifiers, isReinitializingGadget, usbState],
[forceHttp, rpcDataChannel?.readyState, rpcHidReady, reportKeyboardEvent, send, updateActiveKeysAndModifiers, isReinitializingGadget, usbState],
);
// Send per-key press/release (new HID-RPC method)
const sendKeypress = useCallback(
(key: number, press: boolean) => {
if (isReinitializingGadget || usbState !== "configured") return;
if (rpcHidReady && !forceHttp) {
reportKeypressEvent(key, press);
// Track held keys for keepalive
if (press) {
heldKeysRef.current.add(key);
// Start keepalive interval if not already running
if (!keepaliveIntervalRef.current) {
keepaliveIntervalRef.current = setInterval(() => {
if (heldKeysRef.current.size > 0) {
reportKeypressKeepAlive();
}
}, 50);
}
} else {
heldKeysRef.current.delete(key);
if (heldKeysRef.current.size === 0 && keepaliveIntervalRef.current) {
clearInterval(keepaliveIntervalRef.current);
keepaliveIntervalRef.current = null;
}
}
} else {
// Legacy: simulate device-side key handling
// This maintains the 6-key buffer on the frontend for legacy compatibility
// ... (existing logic would go here, but for now use sendKeyboardEvent)
// For simplicity in migration, we fall back to full state reports
const modifier = press ? 0 : 0; // Simplified - would need proper modifier tracking
sendKeyboardEvent(press ? [key] : [], [modifier]);
}
},
[rpcHidReady, forceHttp, reportKeypressEvent, reportKeypressKeepAlive, isReinitializingGadget, usbState, sendKeyboardEvent]
);
const resetKeyboardState = useCallback(() => {
sendKeyboardEvent([], []);
}, [sendKeyboardEvent]);
// Release all held keys
if (rpcHidReady && !forceHttp) {
heldKeysRef.current.forEach(key => {
reportKeypressEvent(key, false);
});
} else {
sendKeyboardEvent([], []);
}
heldKeysRef.current.clear();
if (keepaliveIntervalRef.current) {
clearInterval(keepaliveIntervalRef.current);
keepaliveIntervalRef.current = null;
}
}, [rpcHidReady, forceHttp, reportKeypressEvent, sendKeyboardEvent]);
// Cleanup on unmount
useEffect(() => {
return () => {
resetKeyboardState();
};
}, [resetKeyboardState]);
const executeMacro = async (steps: { keys: string[] | null; modifiers: string[] | null; delay: number }[]) => {
for (const [index, step] of steps.entries()) {
@@ -66,5 +135,5 @@ export default function useKeyboard() {
}
};
return { sendKeyboardEvent, resetKeyboardState, executeMacro };
return { sendKeyboardEvent, sendKeypress, resetKeyboardState, executeMacro };
}

View File

@@ -1,45 +1,85 @@
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"
interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean }
export type KeyCombo = KeyInfo & { deadKey?: boolean, accentKey?: KeyInfo }
export const layouts: Record<string, string> = {
en_UK: name_en_UK,
en_US: name_en_US,
fr_FR: name_fr_FR,
be_FR: name_fr_BE,
cs_CZ: name_cs_CZ,
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 interface KeyStroke {
modifier: number;
keys: number[];
}
export const chars: Record<string, Record<string, KeyCombo>> = {
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 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<string, KeyCombo>;
modifierDisplayMap: Record<string, string>;
keyDisplayMap: Record<string, string>;
virtualKeyboard: {
main: { default: string[]; shift: string[] };
control?: { default: string[]; shift?: string[] };
arrows?: { default: string[] };
numpad?: {
numlocked: string[];
default: string[];
};
};
}
// Import all layouts
import { cs_CZ } from "./keyboardLayouts/cs_CZ";
import { da_DK } from "./keyboardLayouts/da_DK";
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 { hu_HU } from "./keyboardLayouts/hu_HU";
import { it_IT } from "./keyboardLayouts/it_IT";
import { ja_JP } from "./keyboardLayouts/ja_JP";
import { nb_NO } from "./keyboardLayouts/nb_NO";
import { pl_PL } from "./keyboardLayouts/pl_PL";
import { pt_PT } from "./keyboardLayouts/pt_PT";
import { sv_SE } from "./keyboardLayouts/sv_SE";
import { sl_SI } from "./keyboardLayouts/sl_SI";
import { ru_RU } from "./keyboardLayouts/ru_RU";
export const keyboards: KeyboardLayout[] = [
cs_CZ,
da_DK,
de_CH,
de_DE,
en_UK,
en_US,
es_ES,
fr_BE,
fr_CH,
fr_FR,
hu_HU,
it_IT,
ja_JP,
nb_NO,
pl_PL,
pt_PT,
sv_SE,
sl_SI,
ru_RU,
];
// Backward-compatible maps
export const layouts: Record<string, string> = {};
export const chars: Record<string, Record<string, KeyCombo>> = {};
keyboards.forEach(kb => {
const oldCode = kb.isoCode.replace("-", "_");
layouts[oldCode] = kb.name;
chars[oldCode] = kb.chars;
});

View File

@@ -1,6 +1,8 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
import { modifierDisplayMap, keyDisplayMap, virtualKeyboard } from "./en_US"
export const name = "Čeština";
const name = "Čeština";
const isoCode = "cs-CZ";
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
@@ -242,3 +244,12 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const cs_CZ: KeyboardLayout = {
isoCode,
name,
chars,
keyDisplayMap,
modifierDisplayMap,
virtualKeyboard,
};

View File

@@ -0,0 +1,186 @@
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts";
import { en_US } from "./en_US"; // for fallback of keyDisplayMap, modifierDisplayMap, and virtualKeyboard
export const name = "Dansk";
const isoCode = "da-DK";
const keyTrema = { key: "BracketRight" };
const keyAcute = { key: "Equal", altRight: true };
const keyHat = { key: "BracketRight", shift: true };
const keyGrave = { key: "Equal", shift: true };
const keyTilde = { key: "BracketRight", altRight: true };
export const chars = {
A: { key: "KeyA", shift: true },
Ä: { key: "KeyA", shift: true, accentKey: keyTrema },
Á: { key: "KeyA", shift: true, accentKey: keyAcute },
Â: { key: "KeyA", shift: true, accentKey: keyHat },
À: { key: "KeyA", shift: true, accentKey: keyGrave },
Ã: { key: "KeyA", shift: true, accentKey: keyTilde },
B: { key: "KeyB", shift: true },
C: { key: "KeyC", shift: true },
D: { key: "KeyD", shift: true },
E: { key: "KeyE", shift: true },
Ë: { key: "KeyE", shift: true, accentKey: keyTrema },
É: { key: "KeyE", shift: true, accentKey: keyAcute },
Ê: { key: "KeyE", shift: true, accentKey: keyHat },
È: { key: "KeyE", shift: true, accentKey: keyGrave },
: { key: "KeyE", shift: true, accentKey: keyTilde },
F: { key: "KeyF", shift: true },
G: { key: "KeyG", shift: true },
H: { key: "KeyH", shift: true },
I: { key: "KeyI", shift: true },
Ï: { key: "KeyI", shift: true, accentKey: keyTrema },
Í: { key: "KeyI", shift: true, accentKey: keyAcute },
Î: { key: "KeyI", shift: true, accentKey: keyHat },
Ì: { key: "KeyI", shift: true, accentKey: keyGrave },
Ĩ: { key: "KeyI", shift: true, accentKey: keyTilde },
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 },
Ö: { key: "KeyO", shift: true, accentKey: keyTrema },
Ó: { key: "KeyO", shift: true, accentKey: keyAcute },
Ô: { key: "KeyO", shift: true, accentKey: keyHat },
Ò: { key: "KeyO", shift: true, accentKey: keyGrave },
Õ: { key: "KeyO", shift: true, accentKey: keyTilde },
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 },
Ü: { key: "KeyU", shift: true, accentKey: keyTrema },
Ú: { key: "KeyU", shift: true, accentKey: keyAcute },
Û: { key: "KeyU", shift: true, accentKey: keyHat },
Ù: { key: "KeyU", shift: true, accentKey: keyGrave },
Ũ: { key: "KeyU", shift: true, accentKey: keyTilde },
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" },
ä: { key: "KeyA", accentKey: keyTrema },
á: { key: "KeyA", accentKey: keyAcute },
â: { key: "KeyA", accentKey: keyHat },
à: { key: "KeyA", accentKey: keyGrave },
ã: { key: "KeyA", accentKey: keyTilde },
b: { key: "KeyB" },
c: { key: "KeyC" },
d: { key: "KeyD" },
e: { key: "KeyE" },
ë: { key: "KeyE", accentKey: keyTrema },
é: { key: "KeyE", accentKey: keyAcute },
ê: { key: "KeyE", accentKey: keyHat },
è: { key: "KeyE", accentKey: keyGrave },
: { key: "KeyE", accentKey: keyTilde },
"€": { key: "KeyE", altRight: true },
f: { key: "KeyF" },
g: { key: "KeyG" },
h: { key: "KeyH" },
i: { key: "KeyI" },
ï: { key: "KeyI", accentKey: keyTrema },
í: { key: "KeyI", accentKey: keyAcute },
î: { key: "KeyI", accentKey: keyHat },
ì: { key: "KeyI", accentKey: keyGrave },
ĩ: { key: "KeyI", accentKey: keyTilde },
j: { key: "KeyJ" },
k: { key: "KeyK" },
l: { key: "KeyL" },
m: { key: "KeyM" },
n: { key: "KeyN" },
o: { key: "KeyO" },
ö: { key: "KeyO", accentKey: keyTrema },
ó: { key: "KeyO", accentKey: keyAcute },
ô: { key: "KeyO", accentKey: keyHat },
ò: { key: "KeyO", accentKey: keyGrave },
õ: { key: "KeyO", accentKey: keyTilde },
p: { key: "KeyP" },
q: { key: "KeyQ" },
r: { key: "KeyR" },
s: { key: "KeyS" },
t: { key: "KeyT" },
u: { key: "KeyU" },
ü: { key: "KeyU", accentKey: keyTrema },
ú: { key: "KeyU", accentKey: keyAcute },
û: { key: "KeyU", accentKey: keyHat },
ù: { key: "KeyU", accentKey: keyGrave },
ũ: { key: "KeyU", accentKey: keyTilde },
v: { key: "KeyV" },
w: { key: "KeyW" },
x: { key: "KeyX" },
y: { key: "KeyY" }, // <-- corrected
z: { key: "KeyZ" }, // <-- corrected
"½": { key: "Backquote" },
"§": { key: "Backquote", shift: true },
1: { key: "Digit1" },
"!": { key: "Digit1", shift: true },
2: { key: "Digit2" },
'"': { key: "Digit2", shift: true },
"@": { key: "Digit2", altRight: true },
3: { key: "Digit3" },
"#": { key: "Digit3", shift: true },
"£": { key: "Digit3", altRight: true },
4: { key: "Digit4" },
"¤": { key: "Digit4", shift: true },
$: { key: "Digit4", altRight: true },
5: { key: "Digit5" },
"%": { key: "Digit5", shift: true },
6: { key: "Digit6" },
"&": { key: "Digit6", shift: true },
7: { key: "Digit7" },
"/": { key: "Digit7", shift: true },
"{": { key: "Digit7", altRight: true },
8: { key: "Digit8" },
"(": { key: "Digit8", shift: true },
"[": { key: "Digit8", altRight: true },
9: { key: "Digit9" },
")": { key: "Digit9", shift: true },
"]": { key: "Digit9", altRight: true },
0: { key: "Digit0" },
"=": { key: "Digit0", shift: true },
"}": { key: "Digit0", altRight: true },
"+": { key: "Minus" },
"?": { key: "Minus", shift: true },
"\\": { key: "Equal" },
å: { key: "BracketLeft" },
Å: { key: "BracketLeft", shift: true },
ø: { key: "Semicolon" },
Ø: { key: "Semicolon", shift: true },
æ: { key: "Quote" },
Æ: { key: "Quote", shift: true },
"'": { key: "Backslash" },
"*": { key: "Backslash", shift: true },
",": { key: "Comma" },
";": { key: "Comma", shift: true },
".": { key: "Period" },
":": { key: "Period", shift: true },
"-": { key: "Slash" },
_: { key: "Slash", shift: true },
"<": { key: "IntlBackslash" },
">": { key: "IntlBackslash", shift: true },
"~": { key: "BracketRight", deadKey: true, altRight: true },
"^": { key: "BracketRight", deadKey: true, shift: true },
"¨": { key: "BracketRight", deadKey: true },
"|": { key: "Equal", deadKey: true, altRight: true },
"`": { key: "Equal", deadKey: true, shift: true },
"´": { key: "Equal", deadKey: true },
" ": { key: "Space" },
"\n": { key: "Enter" },
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const da_DK: KeyboardLayout = {
isoCode: isoCode,
name: name,
chars: chars,
// TODO need to localize these maps and layouts
keyDisplayMap: en_US.keyDisplayMap,
modifierDisplayMap: en_US.modifierDisplayMap,
virtualKeyboard: en_US.virtualKeyboard,
};

View File

@@ -1,6 +1,8 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
import { modifierDisplayMap, keyDisplayMap, virtualKeyboard } from "./en_US"
export const name = "Schwiizerdütsch";
const name = "Schwiizerdütsch";
const isoCode = "de-CH";
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
@@ -163,3 +165,12 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const de_CH: KeyboardLayout = {
isoCode,
name,
chars,
keyDisplayMap,
modifierDisplayMap,
virtualKeyboard,
};

View File

@@ -1,6 +1,8 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
import { modifierDisplayMap, keyDisplayMap, virtualKeyboard } from "./en_US"
export const name = "Deutsch";
const name = "Deutsch";
const isoCode = "de-DE";
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
@@ -150,3 +152,12 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const de_DE: KeyboardLayout = {
isoCode,
name,
chars,
keyDisplayMap,
modifierDisplayMap,
virtualKeyboard,
};

View File

@@ -1,6 +1,8 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
import { modifierDisplayMap, keyDisplayMap, virtualKeyboard } from "./en_US"
export const name = "English (UK)";
const name = "English (UK)";
const isoCode = "en-GB";
export const chars = {
A: { key: "KeyA", shift: true },
@@ -104,4 +106,13 @@ export const chars = {
"\n": { key: "Enter" },
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>
} as Record<string, KeyCombo>;
export const en_UK: KeyboardLayout = {
isoCode,
name,
chars,
keyDisplayMap,
modifierDisplayMap,
virtualKeyboard,
};

View File

@@ -1,6 +1,7 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "English (US)";
const name = "English (US)";
const isoCode = "en-US";
export const chars = {
A: { key: "KeyA", shift: true },
@@ -89,25 +90,283 @@ export const chars = {
">": { key: "Period", shift: true },
";": { key: "Semicolon" },
":": { key: "Semicolon", shift: true },
"¶": { key: "Semicolon", altRight: true }, // pilcrow sign
"[": { key: "BracketLeft" },
"{": { key: "BracketLeft", shift: true },
"«": { key: "BracketLeft", altRight: true }, // double left quote sign
"]": { key: "BracketRight" },
"}": { key: "BracketRight", shift: true },
"»": { key: "BracketRight", altRight: true }, // double right quote sign
"\\": { key: "Backslash" },
"|": { key: "Backslash", shift: true },
"¬": { key: "Backslash", altRight: true }, // not sign
"`": { key: "Backquote" },
"~": { key: "Backquote", shift: true },
"§": { key: "IntlBackslash" },
"±": { key: "IntlBackslash", shift: true },
" ": { key: "Space", shift: false },
"\n": { key: "Enter", shift: false },
Enter: { key: "Enter", shift: false },
Tab: { key: "Tab", shift: false },
PrintScreen: { key: "Prt Sc", shift: false },
" ": { key: "Space" },
"\n": { key: "Enter" },
Enter: { key: "Enter" },
Escape: { key: "Escape" },
Tab: { key: "Tab" },
PrintScreen: { key: "Prt Sc" },
SystemRequest: { key: "Prt Sc", shift: true },
ScrollLock: { key: "ScrollLock", shift: false},
Pause: { key: "Pause", shift: false },
ScrollLock: { key: "ScrollLock" },
Pause: { key: "Pause" },
Break: { key: "Pause", shift: true },
Insert: { key: "Insert", shift: false },
Delete: { key: "Delete", shift: false },
} as Record<string, KeyCombo>
Insert: { key: "Insert" },
Delete: { key: "Delete" },
} as Record<string, KeyCombo>;
export const modifierDisplayMap: Record<string, string> = {
ControlLeft: "Left Ctrl",
ControlRight: "Right Ctrl",
ShiftLeft: "Left Shift",
ShiftRight: "Right Shift",
AltLeft: "Left Alt",
AltRight: "Right Alt",
MetaLeft: "Left Meta",
MetaRight: "Right Meta",
AltGr: "AltGr",
} as Record<string, string>;
export const keyDisplayMap: Record<string, string> = {
CtrlAltDelete: "Ctrl + Alt + Delete",
AltMetaEscape: "Alt + Meta + Escape",
CtrlAltBackspace: "Ctrl + Alt + Backspace",
AltGr: "AltGr",
AltLeft: "Alt ⌥",
AltRight: "⌥ Alt",
ArrowDown: "↓",
ArrowLeft: "←",
ArrowRight: "→",
ArrowUp: "↑",
Backspace: "Backspace",
"(Backspace)": "Backspace",
CapsLock: "Caps Lock ⇪",
Clear: "Clear",
ControlLeft: "Ctrl ⌃",
ControlRight: "⌃ Ctrl",
Delete: "Delete ⌦",
End: "End",
Enter: "Enter",
Escape: "Esc",
Home: "Home",
Insert: "Insert",
Menu: "Menu",
MetaLeft: "Meta ⌘",
MetaRight: "⌘ Meta",
PageDown: "PgDn",
PageUp: "PgUp",
ShiftLeft: "Shift ⇧",
ShiftRight: "⇧ Shift",
Space: " ",
Tab: "Tab ⇥",
// Letters
KeyA: "a",
KeyB: "b",
KeyC: "c",
KeyD: "d",
KeyE: "e",
KeyF: "f",
KeyG: "g",
KeyH: "h",
KeyI: "i",
KeyJ: "j",
KeyK: "k",
KeyL: "l",
KeyM: "m",
KeyN: "n",
KeyO: "o",
KeyP: "p",
KeyQ: "q",
KeyR: "r",
KeyS: "s",
KeyT: "t",
KeyU: "u",
KeyV: "v",
KeyW: "w",
KeyX: "x",
KeyY: "y",
KeyZ: "z",
// Capital letters
"(KeyA)": "A",
"(KeyB)": "B",
"(KeyC)": "C",
"(KeyD)": "D",
"(KeyE)": "E",
"(KeyF)": "F",
"(KeyG)": "G",
"(KeyH)": "H",
"(KeyI)": "I",
"(KeyJ)": "J",
"(KeyK)": "K",
"(KeyL)": "L",
"(KeyM)": "M",
"(KeyN)": "N",
"(KeyO)": "O",
"(KeyP)": "P",
"(KeyQ)": "Q",
"(KeyR)": "R",
"(KeyS)": "S",
"(KeyT)": "T",
"(KeyU)": "U",
"(KeyV)": "V",
"(KeyW)": "W",
"(KeyX)": "X",
"(KeyY)": "Y",
"(KeyZ)": "Z",
// Numbers
Digit1: "1",
Digit2: "2",
Digit3: "3",
Digit4: "4",
Digit5: "5",
Digit6: "6",
Digit7: "7",
Digit8: "8",
Digit9: "9",
Digit0: "0",
// Shifted Numbers
"(Digit1)": "!",
"(Digit2)": "@",
"(Digit3)": "#",
"(Digit4)": "$",
"(Digit5)": "%",
"(Digit6)": "^",
"(Digit7)": "&",
"(Digit8)": "*",
"(Digit9)": "(",
"(Digit0)": ")",
// Symbols
Minus: "-",
"(Minus)": "_",
Equal: "=",
"(Equal)": "+",
BracketLeft: "[",
"(BracketLeft)": "{",
BracketRight: "]",
"(BracketRight)": "}",
Backslash: "\\",
"(Backslash)": "|",
Semicolon: ";",
"(Semicolon)": ":",
Quote: "'",
"(Quote)": '"',
Comma: ",",
"(Comma)": "<",
Period: ".",
"(Period)": ">",
Slash: "/",
"(Slash)": "?",
Backquote: "`",
"(Backquote)": "~",
IntlBackslash: "\\",
// Function keys
F1: "F1",
F2: "F2",
F3: "F3",
F4: "F4",
F5: "F5",
F6: "F6",
F7: "F7",
F8: "F8",
F9: "F9",
F10: "F10",
F11: "F11",
F12: "F12",
// Numpad
Numpad0: "Num 0",
Numpad1: "Num 1",
Numpad2: "Num 2",
Numpad3: "Num 3",
Numpad4: "Num 4",
Numpad5: "Num 5",
Numpad6: "Num 6",
Numpad7: "Num 7",
Numpad8: "Num 8",
Numpad9: "Num 9",
NumpadAdd: "Num +",
NumpadSubtract: "Num -",
NumpadMultiply: "Num *",
NumpadDivide: "Num /",
NumpadDecimal: "Num .",
NumpadEqual: "Num =",
NumpadEnter: "Num Enter",
NumpadInsert: "Ins",
NumpadDelete: "Del",
NumLock: "Num Lock",
// Modals
PrintScreen: "Prt Sc",
ScrollLock: "Scr Lk",
Pause: "Pause",
"(PrintScreen)": "Sys Rq",
"(Pause)": "Break",
SystemRequest: "Sys Rq",
Break: "Break",
};
export const virtualKeyboard = {
main: {
default: [
"CtrlAltDelete AltMetaEscape CtrlAltBackspace",
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
"Backquote Digit1 Digit2 Digit3 Digit4 Digit5 Digit6 Digit7 Digit8 Digit9 Digit0 Minus Equal Backspace",
"Tab KeyQ KeyW KeyE KeyR KeyT KeyY KeyU KeyI KeyO KeyP BracketLeft BracketRight Backslash",
"CapsLock KeyA KeyS KeyD KeyF KeyG KeyH KeyJ KeyK KeyL Semicolon Quote Enter",
"ShiftLeft KeyZ KeyX KeyC KeyV KeyB KeyN KeyM Comma Period Slash ShiftRight",
"ControlLeft MetaLeft AltLeft Space AltGr MetaRight Menu ControlRight",
],
shift: [
"CtrlAltDelete AltMetaEscape CtrlAltBackspace",
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
"(Backquote) (Digit1) (Digit2) (Digit3) (Digit4) (Digit5) (Digit6) (Digit7) (Digit8) (Digit9) (Digit0) (Minus) (Equal) (Backspace)",
"Tab (KeyQ) (KeyW) (KeyE) (KeyR) (KeyT) (KeyY) (KeyU) (KeyI) (KeyO) (KeyP) (BracketLeft) (BracketRight) (Backslash)",
"CapsLock (KeyA) (KeyS) (KeyD) (KeyF) (KeyG) (KeyH) (KeyJ) (KeyK) (KeyL) (Semicolon) (Quote) Enter",
"ShiftLeft (KeyZ) (KeyX) (KeyC) (KeyV) (KeyB) (KeyN) (KeyM) (Comma) (Period) (Slash) ShiftRight",
"ControlLeft MetaLeft AltLeft Space AltGr MetaRight Menu ControlRight",
],
},
control: {
default: ["PrintScreen ScrollLock Pause", "Insert Home PageUp", "Delete End PageDown"],
shift: ["(PrintScreen) ScrollLock (Pause)", "Insert Home PageUp", "Delete End PageDown"],
},
arrows: {
default: ["ArrowUp", "ArrowLeft ArrowDown ArrowRight"],
},
numpad: {
numlocked: [
"NumLock NumpadDivide NumpadMultiply NumpadSubtract",
"Numpad7 Numpad8 Numpad9 NumpadAdd",
"Numpad4 Numpad5 Numpad6",
"Numpad1 Numpad2 Numpad3 NumpadEnter",
"Numpad0 NumpadDecimal",
],
default: [
"NumLock NumpadDivide NumpadMultiply NumpadSubtract",
"Home ArrowUp PageUp NumpadAdd",
"ArrowLeft Clear ArrowRight",
"End ArrowDown PageDown NumpadEnter",
"NumpadInsert NumpadDelete",
],
},
};
export const en_US: KeyboardLayout = {
isoCode,
name,
chars,
keyDisplayMap,
modifierDisplayMap,
virtualKeyboard,
};

View File

@@ -1,6 +1,8 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
import { modifierDisplayMap, keyDisplayMap, virtualKeyboard } from "./en_US"
export const name = "Español";
const name = "Español";
const isoCode = "es-ES";
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
@@ -166,3 +168,12 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const es_ES: KeyboardLayout = {
isoCode,
name,
chars,
keyDisplayMap,
modifierDisplayMap,
virtualKeyboard,
};

View File

@@ -1,6 +1,8 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
import { modifierDisplayMap, keyDisplayMap, virtualKeyboard } from "./en_US"
export const name = "Belgisch Nederlands";
const name = "Belgisch Nederlands";
const isoCode = "fr-BE";
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
@@ -165,3 +167,12 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const fr_BE: KeyboardLayout = {
isoCode,
name,
chars,
keyDisplayMap,
modifierDisplayMap,
virtualKeyboard,
};

View File

@@ -1,8 +1,9 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
import { chars as chars_de_CH } from "./de_CH"
import { modifierDisplayMap, keyDisplayMap, virtualKeyboard } from "./en_US"
export const name = "Français de Suisse";
const name = "Français de Suisse";
const isoCode = "fr-CH";
export const chars = {
...chars_de_CH,
@@ -13,3 +14,12 @@ export const chars = {
"à": { key: "Quote" },
"ä": { key: "Quote", shift: true },
} as Record<string, KeyCombo>;
export const fr_CH: KeyboardLayout = {
isoCode,
name,
chars,
keyDisplayMap,
modifierDisplayMap,
virtualKeyboard,
};

View File

@@ -1,6 +1,8 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
import { modifierDisplayMap, keyDisplayMap, virtualKeyboard } from "./en_US"
export const name = "Français";
const name = "Français";
const isoCode = "fr-FR";
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
@@ -137,3 +139,12 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const fr_FR: KeyboardLayout = {
isoCode,
name,
chars,
keyDisplayMap,
modifierDisplayMap,
virtualKeyboard,
};

View File

@@ -0,0 +1,177 @@
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts";
import { en_US } from "./en_US";
const name = "Magyar";
const isoCode = "hu-HU";
const keyAcute: KeyCombo = { key: "Digit9", altRight: true };
const keyDoubleAcute: KeyCombo = { key: "Equal", shift: true };
const keyTrema: KeyCombo = { key: "Equal", altRight: true };
const chars = {
A: { key: "KeyA", shift: true },
Á: { key: "Semicolon", shift: true, accentKey: keyAcute },
B: { key: "KeyB", shift: true },
C: { key: "KeyC", shift: true },
D: { key: "KeyD", shift: true },
E: { key: "KeyE", shift: true },
É: { key: "Quote", shift: true, accentKey: keyAcute },
F: { key: "KeyF", shift: true },
G: { key: "KeyG", shift: true },
H: { key: "KeyH", shift: true },
I: { key: "KeyI", shift: true },
Í: { key: "IntlBackslash", shift: true, accentKey: keyAcute },
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 },
Ó: { key: "BracketLeft", shift: true, accentKey: keyAcute },
Ö: { key: "Minus", shift: true, accentKey: keyTrema },
Ő: { key: "BracketRight", shift: true, accentKey: keyDoubleAcute },
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 },
Ú: { key: "Backslash", shift: true, accentKey: keyAcute },
Ü: { key: "Equal", shift: true, accentKey: keyTrema },
Ű: { key: "Backquote", shift: true, accentKey: keyDoubleAcute },
V: { key: "KeyV", shift: true },
W: { key: "KeyW", shift: true },
X: { key: "KeyX", shift: true },
Y: { key: "KeyZ", shift: true },
Z: { key: "KeyY", shift: true },
a: { key: "KeyA" },
á: { key: "Semicolon", accentKey: keyAcute },
b: { key: "KeyB" },
c: { key: "KeyC" },
d: { key: "KeyD" },
e: { key: "KeyE" },
é: { key: "Quote", accentKey: keyAcute },
f: { key: "KeyF" },
g: { key: "KeyG" },
h: { key: "KeyH" },
i: { key: "KeyI" },
í: { key: "IntlBackslash", accentKey: keyAcute },
j: { key: "KeyJ" },
k: { key: "KeyK" },
l: { key: "KeyL" },
m: { key: "KeyM" },
n: { key: "KeyN" },
o: { key: "KeyO" },
ó: { key: "BracketLeft", accentKey: keyAcute },
ö: { key: "Minus", accentKey: keyTrema },
ő: { key: "BracketRight", accentKey: keyDoubleAcute },
p: { key: "KeyP" },
q: { key: "KeyQ" },
r: { key: "KeyR" },
s: { key: "KeyS" },
t: { key: "KeyT" },
u: { key: "KeyU" },
ú: { key: "Backslash", accentKey: keyAcute },
ü: { key: "Equal", accentKey: keyTrema },
ű: { key: "Backquote", accentKey: keyDoubleAcute },
v: { key: "KeyV" },
w: { key: "KeyW" },
x: { key: "KeyX" },
y: { key: "KeyZ" },
z: { key: "KeyY" },
// Numbers and top row symbols
0: { key: "Digit0" },
"§": { key: "Digit0", shift: true },
1: { key: "Digit1" },
"'": { key: "Digit1", shift: true },
2: { key: "Digit2" },
'"': { key: "Digit2", shift: true },
3: { key: "Digit3" },
"+": { key: "Digit3", shift: true },
4: { key: "Digit4" },
"!": { key: "Digit4", shift: true },
5: { key: "Digit5" },
"%": { key: "Digit5", shift: true },
6: { key: "Digit6" },
"/": { key: "Digit6", shift: true },
7: { key: "Digit7" },
"=": { key: "Digit7", shift: true },
8: { key: "Digit8" },
"(": { key: "Digit8", shift: true },
9: { key: "Digit9" },
")": { key: "Digit9", shift: true },
// AltGr symbols
"~": { key: "Digit1", altRight: true },
ˇ: { key: "Digit2", altRight: true },
"^": { key: "Digit3", altRight: true },
"˘": { key: "Digit4", altRight: true },
"°": { key: "Digit5", altRight: true },
"˛": { key: "Digit6", altRight: true },
"`": { key: "Digit7", altRight: true },
"˙": { key: "Digit8", altRight: true },
"´": { key: "Digit9", altRight: true },
"˝": { key: "Digit0", altRight: true },
"„": { key: "KeyO", altRight: true },
"\\": { key: "KeyQ", altRight: true },
"|": { key: "KeyW", altRight: true },
"€": { key: "KeyU", altRight: true },
đ: { key: "KeyS", altRight: true },
"[": { key: "KeyF", altRight: true },
"]": { key: "KeyG", altRight: true },
ß: { key: "Semicolon", altRight: true },
$: { key: "Quote", altRight: true },
"¤": { key: "Backquote", altRight: true },
"@": { key: "KeyV", altRight: true },
"{": { key: "KeyB", altRight: true },
"}": { key: "KeyN", altRight: true },
"<": { key: "IntlBackslash", altRight: true },
">": { key: "KeyZ", altRight: true },
"#": { key: "KeyX", altRight: true },
"&": { key: "KeyC", altRight: true },
";": { key: "Comma", altRight: true },
"*": { key: "Period", altRight: true },
"÷": { key: "BracketRight", altRight: true },
"×": { key: "Backslash", altRight: true },
// Punctuation
",": { key: "Comma" },
"?": { key: "Comma", shift: true },
".": { key: "Period" },
":": { key: "Period", shift: true },
"-": { key: "Slash" },
_: { key: "Slash", shift: true },
" ": { key: "Space" },
"\n": { key: "Enter" },
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
const keyDisplayMap = {
...en_US.keyDisplayMap,
Digit0: "0",
Backquote: "ű",
Minus: "ö",
Equal: "ü",
BracketLeft: "ó",
BracketRight: "ő",
Semicolon: "á",
Quote: "é",
Backslash: "ú",
IntlBackslash: "í",
KeyY: "Z",
KeyZ: "Y",
} as Record<string, string>;
export const hu_HU: KeyboardLayout = {
isoCode,
name,
chars,
keyDisplayMap,
modifierDisplayMap: {
...en_US.modifierDisplayMap,
altRight: "AltGr",
},
virtualKeyboard: en_US.virtualKeyboard,
};

View File

@@ -1,6 +1,8 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
import { modifierDisplayMap, keyDisplayMap, virtualKeyboard } from "./en_US"
export const name = "Italiano";
const name = "Italiano";
const isoCode = "it-IT";
export const chars = {
A: { key: "KeyA", shift: true },
@@ -111,3 +113,12 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const it_IT: KeyboardLayout = {
isoCode,
name,
chars,
keyDisplayMap,
modifierDisplayMap,
virtualKeyboard,
};

View File

@@ -0,0 +1,124 @@
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts";
import { en_US } from "./en_US";
const name = "Japanese";
const isoCode = "ja-JP";
// NOTE:
// This layout is primarily implemented with primarily targets Windows/Linux in mind on common JIS 106/109 keyboards.
// Across Windows, Linux, and macOS, there are small but important differences in:
// - how backslash ("\\") vs yen ("¥") are produced / interpreted, and
// - how Japanese IME mode switching keys behave (e.g. Henkan/Muhenkan/KatakanaHiragana).
//
// For Windows/Linux friendliness, we intentionally map both "\\" and "¥" to the Yen key,
// since many environments/applications render the Yen key as a backslash.
//
// TODO:
// If macOS-specific behavior is required, consider adding a dedicated macOS JIS layout
// (e.g. ja_JP_mac) and adjust mappings (often mapping "\\" to Backslash instead of Yen),
// plus any IME-key semantics differences as needed.
export const chars = {
...en_US.chars,
'"': { key: "Digit2", shift: true },
"&": { key: "Digit6", shift: true },
"'": { key: "Digit7", shift: true },
"(": { key: "Digit8", shift: true },
")": { key: "Digit9", shift: true },
"=": { key: "Minus", shift: true },
"^": { key: "Equal" },
"~": { key: "Equal", shift: true },
"\\": { key: "Yen" },
"¥": { key: "Yen" },
"|": { key: "Yen", shift: true },
"@": { key: "BracketLeft" },
"`": { key: "BracketLeft", shift: true },
"[": { key: "BracketRight" },
"{": { key: "BracketRight", shift: true },
";": { key: "Semicolon" },
"+": { key: "Semicolon", shift: true },
":": { key: "Quote" },
"*": { key: "Quote", shift: true },
"]": { key: "Backslash" },
"}": { key: "Backslash", shift: true },
_: { key: "KeyRO", shift: true },
} as Record<string, KeyCombo>;
// NOTE:
// We intentionally avoid providing Hiragana glyph labels on keycaps in the UI.
// Only about 5.1% of users typed with Kana input as of 2015; thus Kana legends are
// generally omitted to reduce visual clutter while keeping IME-related keys functional
// (Henkan/Muhenkan/KatakanaHiragana) for users who need them.
// Source: https://ja.wikipedia.org/wiki/%E3%81%8B%E3%81%AA%E5%85%A5%E5%8A%9B#%E3%81%8B%E3%81%AA%E5%85%A5%E5%8A%9B%E3%81%AE%E5%88%A9%E7%94%A8%E7%8A%B6%E6%B3%81
export const keyDisplayMap: Record<string, string> = {
...en_US.keyDisplayMap,
"(Digit2)": '"',
"(Digit6)": "&",
"(Digit7)": "'",
"(Digit8)": "(",
"(Digit9)": ")",
"(Minus)": "=",
Equal: "^",
"(Equal)": "~",
Yen: "¥",
"(Yen)": "|",
KeyRO: "\\",
"(KeyRO)": "_",
Henkan: "変換",
Muhenkan: "無変換",
KatakanaHiragana: "ひらがな",
Backquote: "半角/全角",
"(KatakanaHiragana)": "ローマ字",
BracketLeft: "@",
"(BracketLeft)": "`",
BracketRight: "[",
"(BracketRight)": "{",
Semicolon: ";",
"(Semicolon)": "+",
Quote: ":",
"(Quote)": "*",
Backslash: "]",
"(Backslash)": "}",
ContextMenu: "Menu",
// UI-only notes:
// - Keep a placeholder label for shifted Digit0 to avoid a "missing" keycap in the UI.
// - Use "⏎" to hint at the tall, JIS/ISO-style L-shaped Enter key in the UI,
// while internally representing it with two virtual buttons.
"(Digit0)": " ",
"(Enter)": "⏎",
};
export const virtualKeyboard = {
...en_US.virtualKeyboard,
main: {
default: [
"CtrlAltDelete AltMetaEscape CtrlAltBackspace",
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
"Backquote Digit1 Digit2 Digit3 Digit4 Digit5 Digit6 Digit7 Digit8 Digit9 Digit0 Minus Equal Yen Backspace",
"Tab KeyQ KeyW KeyE KeyR KeyT KeyY KeyU KeyI KeyO KeyP BracketLeft BracketRight Enter",
"CapsLock KeyA KeyS KeyD KeyF KeyG KeyH KeyJ KeyK KeyL Semicolon Quote Backslash (Enter)",
"ShiftLeft KeyZ KeyX KeyC KeyV KeyB KeyN KeyM Comma Period Slash KeyRO ShiftRight",
"ControlLeft MetaLeft AltLeft Muhenkan Space Henkan KatakanaHiragana AltRight MetaRight ContextMenu ControlRight",
],
shift: [
"CtrlAltDelete AltMetaEscape CtrlAltBackspace",
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
"Backquote (Digit1) (Digit2) (Digit3) (Digit4) (Digit5) (Digit6) (Digit7) (Digit8) (Digit9) (Digit0) (Minus) (Equal) (Yen) (Backspace)",
"Tab (KeyQ) (KeyW) (KeyE) (KeyR) (KeyT) (KeyY) (KeyU) (KeyI) (KeyO) (KeyP) (BracketLeft) (BracketRight) Enter",
"CapsLock (KeyA) (KeyS) (KeyD) (KeyF) (KeyG) (KeyH) (KeyJ) (KeyK) (KeyL) (Semicolon) (Quote) (Backslash) (Enter)",
"ShiftLeft (KeyZ) (KeyX) (KeyC) (KeyV) (KeyB) (KeyN) (KeyM) (Comma) (Period) (Slash) (KeyRO) ShiftRight",
"ControlLeft MetaLeft AltLeft Muhenkan Space Henkan (KatakanaHiragana) AltRight MetaRight ContextMenu ControlRight",
],
},
};
export const ja_JP: KeyboardLayout = {
isoCode,
name,
chars,
keyDisplayMap,
modifierDisplayMap: en_US.modifierDisplayMap,
virtualKeyboard,
};

View File

@@ -1,6 +1,8 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
import { modifierDisplayMap, keyDisplayMap, virtualKeyboard } from "./en_US"
export const name = "Norsk bokmål";
const name = "Norsk bokmål";
const isoCode = "nb-NO";
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
@@ -165,3 +167,12 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const nb_NO: KeyboardLayout = {
isoCode,
name,
chars,
keyDisplayMap,
modifierDisplayMap,
virtualKeyboard,
};

View File

@@ -0,0 +1,40 @@
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts";
import { en_US, chars as en_US_chars } from "./en_US";
const name = "Polski";
const isoCode = "pl-PL";
// Polish Programmer layout (kbdpl1): QWERTY + AltGr diacritics, no dead keys
const chars: Record<string, KeyCombo> = {
...en_US_chars,
// lowercase diacritics (AltGr + letter)
ą: { key: "KeyA", altRight: true },
ć: { key: "KeyC", altRight: true },
ę: { key: "KeyE", altRight: true },
ł: { key: "KeyL", altRight: true },
ń: { key: "KeyN", altRight: true },
ó: { key: "KeyO", altRight: true },
ś: { key: "KeyS", altRight: true },
ż: { key: "KeyZ", altRight: true },
ź: { key: "KeyX", altRight: true },
// uppercase diacritics (Shift + AltGr + letter)
Ą: { key: "KeyA", shift: true, altRight: true },
Ć: { key: "KeyC", shift: true, altRight: true },
Ę: { key: "KeyE", shift: true, altRight: true },
Ł: { key: "KeyL", shift: true, altRight: true },
Ń: { key: "KeyN", shift: true, altRight: true },
Ó: { key: "KeyO", shift: true, altRight: true },
Ś: { key: "KeyS", shift: true, altRight: true },
Ż: { key: "KeyZ", shift: true, altRight: true },
Ź: { key: "KeyX", shift: true, altRight: true },
};
export const pl_PL: KeyboardLayout = {
isoCode: isoCode,
name: name,
chars: chars,
keyDisplayMap: en_US.keyDisplayMap,
modifierDisplayMap: en_US.modifierDisplayMap,
virtualKeyboard: en_US.virtualKeyboard,
};

View File

@@ -0,0 +1,209 @@
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts";
import { en_US } from "./en_US"; // for fallback of keyDisplayMap, modifierDisplayMap, and virtualKeyboard
const name = "Português";
const isoCode = "pt-PT";
// Dead keys
const keyAcute: KeyCombo = { key: "BracketRight" }; // ´ (dead) on SC 1B base
const keyGrave: KeyCombo = { key: "BracketRight", shift: true }; // ` (dead) on SC 1B shift
const keyTrema: KeyCombo = { key: "BracketLeft", altRight: true }; // ¨ (dead) on SC 1A AltGr
const keyTilde: KeyCombo = { key: "Backslash" }; // ~ (dead) on SC 2B base
const keyHat: KeyCombo = { key: "Backslash", shift: true }; // ^ (dead) on SC 2B shift
const chars = {
// Uppercase letters
A: { key: "KeyA", shift: true },
Á: { key: "KeyA", shift: true, accentKey: keyAcute },
À: { key: "KeyA", shift: true, accentKey: keyGrave },
Ä: { key: "KeyA", shift: true, accentKey: keyTrema },
Ã: { key: "KeyA", shift: true, accentKey: keyTilde },
Â: { key: "KeyA", shift: true, accentKey: keyHat },
B: { key: "KeyB", shift: true },
C: { key: "KeyC", shift: true },
D: { key: "KeyD", shift: true },
E: { key: "KeyE", shift: true },
É: { key: "KeyE", shift: true, accentKey: keyAcute },
È: { key: "KeyE", shift: true, accentKey: keyGrave },
Ë: { key: "KeyE", shift: true, accentKey: keyTrema },
Ê: { key: "KeyE", shift: true, accentKey: keyHat },
F: { key: "KeyF", shift: true },
G: { key: "KeyG", shift: true },
H: { key: "KeyH", shift: true },
I: { key: "KeyI", shift: true },
Í: { key: "KeyI", shift: true, accentKey: keyAcute },
Ì: { key: "KeyI", shift: true, accentKey: keyGrave },
Ï: { key: "KeyI", shift: true, accentKey: keyTrema },
Î: { key: "KeyI", shift: true, accentKey: keyHat },
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 },
Ñ: { key: "KeyN", shift: true, accentKey: keyTilde },
O: { key: "KeyO", shift: true },
Ó: { key: "KeyO", shift: true, accentKey: keyAcute },
Ò: { key: "KeyO", shift: true, accentKey: keyGrave },
Ö: { key: "KeyO", shift: true, accentKey: keyTrema },
Õ: { key: "KeyO", shift: true, accentKey: keyTilde },
Ô: { key: "KeyO", shift: true, accentKey: keyHat },
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 },
Ú: { key: "KeyU", shift: true, accentKey: keyAcute },
Ù: { key: "KeyU", shift: true, accentKey: keyGrave },
Ü: { key: "KeyU", shift: true, accentKey: keyTrema },
Û: { key: "KeyU", shift: true, accentKey: keyHat },
V: { key: "KeyV", shift: true },
W: { key: "KeyW", shift: true },
X: { key: "KeyX", shift: true },
Y: { key: "KeyY", shift: true },
Ý: { key: "KeyY", shift: true, accentKey: keyAcute },
Z: { key: "KeyZ", shift: true },
// Lowercase letters
a: { key: "KeyA" },
á: { key: "KeyA", accentKey: keyAcute },
à: { key: "KeyA", accentKey: keyGrave },
ä: { key: "KeyA", accentKey: keyTrema },
ã: { key: "KeyA", accentKey: keyTilde },
â: { key: "KeyA", accentKey: keyHat },
b: { key: "KeyB" },
c: { key: "KeyC" },
d: { key: "KeyD" },
e: { key: "KeyE" },
é: { key: "KeyE", accentKey: keyAcute },
è: { key: "KeyE", accentKey: keyGrave },
ë: { key: "KeyE", accentKey: keyTrema },
ê: { key: "KeyE", accentKey: keyHat },
"€": { key: "KeyE", altRight: true },
f: { key: "KeyF" },
g: { key: "KeyG" },
h: { key: "KeyH" },
i: { key: "KeyI" },
í: { key: "KeyI", accentKey: keyAcute },
ì: { key: "KeyI", accentKey: keyGrave },
ï: { key: "KeyI", accentKey: keyTrema },
î: { key: "KeyI", accentKey: keyHat },
j: { key: "KeyJ" },
k: { key: "KeyK" },
l: { key: "KeyL" },
m: { key: "KeyM" },
n: { key: "KeyN" },
ñ: { key: "KeyN", accentKey: keyTilde },
o: { key: "KeyO" },
ó: { key: "KeyO", accentKey: keyAcute },
ò: { key: "KeyO", accentKey: keyGrave },
ö: { key: "KeyO", accentKey: keyTrema },
õ: { key: "KeyO", accentKey: keyTilde },
ô: { key: "KeyO", accentKey: keyHat },
p: { key: "KeyP" },
q: { key: "KeyQ" },
r: { key: "KeyR" },
s: { key: "KeyS" },
t: { key: "KeyT" },
u: { key: "KeyU" },
ú: { key: "KeyU", accentKey: keyAcute },
ù: { key: "KeyU", accentKey: keyGrave },
ü: { key: "KeyU", accentKey: keyTrema },
û: { key: "KeyU", accentKey: keyHat },
v: { key: "KeyV" },
w: { key: "KeyW" },
x: { key: "KeyX" },
y: { key: "KeyY" },
ý: { key: "KeyY", accentKey: keyAcute },
ÿ: { key: "KeyY", accentKey: keyTrema },
z: { key: "KeyZ" },
// SC 29 (OEM_5) → Backquote: \ |
"\\": { key: "Backquote" },
"|": { key: "Backquote", shift: true },
// Number row
1: { key: "Digit1" },
"!": { key: "Digit1", shift: true },
2: { key: "Digit2" },
'"': { key: "Digit2", shift: true },
"@": { key: "Digit2", altRight: true },
3: { key: "Digit3" },
"#": { key: "Digit3", shift: true },
"£": { key: "Digit3", altRight: true },
4: { key: "Digit4" },
$: { key: "Digit4", shift: true },
"§": { key: "Digit4", altRight: true },
5: { key: "Digit5" },
"%": { key: "Digit5", shift: true },
6: { key: "Digit6" },
"&": { key: "Digit6", shift: true },
7: { key: "Digit7" },
"/": { key: "Digit7", shift: true },
"{": { key: "Digit7", altRight: true },
8: { key: "Digit8" },
"(": { key: "Digit8", shift: true },
"[": { key: "Digit8", altRight: true },
9: { key: "Digit9" },
")": { key: "Digit9", shift: true },
"]": { key: "Digit9", altRight: true },
0: { key: "Digit0" },
"=": { key: "Digit0", shift: true },
"}": { key: "Digit0", altRight: true },
// SC 0C (OEM_4) → Minus: ' ?
"'": { key: "Minus" },
"?": { key: "Minus", shift: true },
// SC 0D (OEM_6) → Equal: « »
"«": { key: "Equal" },
"»": { key: "Equal", shift: true },
// SC 1A (OEM_PLUS) → BracketLeft: + * ¨(dead)
"+": { key: "BracketLeft" },
"*": { key: "BracketLeft", shift: true },
"¨": { key: "BracketLeft", altRight: true, deadKey: true },
// SC 1B (OEM_1) → BracketRight: ´(dead) `(dead)
"´": { key: "BracketRight", deadKey: true },
"`": { key: "BracketRight", shift: true, deadKey: true },
// SC 27 (OEM_3) → Semicolon: ç Ç
ç: { key: "Semicolon" },
Ç: { key: "Semicolon", shift: true },
// SC 28 (OEM_7) → Quote: º ª
º: { key: "Quote" },
ª: { key: "Quote", shift: true },
// SC 2B (OEM_2) → Backslash: ~(dead) ^(dead)
"~": { key: "Backslash", deadKey: true },
"^": { key: "Backslash", shift: true, deadKey: true },
// SC 33-35: Comma, Period, Slash
",": { key: "Comma" },
";": { key: "Comma", shift: true },
".": { key: "Period" },
":": { key: "Period", shift: true },
"-": { key: "Slash" },
_: { key: "Slash", shift: true },
// SC 56 (OEM_102) → IntlBackslash: < >
"<": { key: "IntlBackslash" },
">": { key: "IntlBackslash", shift: true },
" ": { key: "Space" },
"\n": { key: "Enter" },
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const pt_PT: KeyboardLayout = {
isoCode: isoCode,
name: name,
chars: chars,
keyDisplayMap: en_US.keyDisplayMap,
modifierDisplayMap: en_US.modifierDisplayMap,
virtualKeyboard: en_US.virtualKeyboard,
};

View File

@@ -0,0 +1,171 @@
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts";
import { en_US } from "./en_US";
const name = "Русская";
const isoCode = "ru-RU";
export const chars = {
...en_US.chars,
А: { key: "KeyF", shift: true },
Б: { key: "Comma", shift: true },
В: { key: "KeyD", shift: true },
Г: { key: "KeyU", shift: true },
Д: { key: "KeyL", shift: true },
Е: { key: "KeyT", shift: true },
Ё: { key: "Backquote", shift: true },
Ж: { key: "Semicolon", shift: true },
З: { key: "KeyP", shift: true },
И: { key: "KeyB", shift: true },
Й: { key: "KeyQ", shift: true },
К: { key: "KeyR", shift: true },
Л: { key: "KeyK", shift: true },
М: { key: "KeyV", shift: true },
Н: { key: "KeyY", shift: true },
О: { key: "KeyJ", shift: true },
П: { key: "KeyG", shift: true },
Р: { key: "KeyH", shift: true },
С: { key: "KeyC", shift: true },
Т: { key: "KeyN", shift: true },
У: { key: "KeyE", shift: true },
Ф: { key: "KeyA", shift: true },
Х: { key: "BracketLeft", shift: true },
Ц: { key: "KeyW", shift: true },
Ч: { key: "KeyX", shift: true },
Ш: { key: "KeyI", shift: true },
Щ: { key: "KeyO", shift: true },
Ъ: { key: "BracketRight", shift: true },
Ы: { key: "KeyS", shift: true },
Ь: { key: "KeyM", shift: true },
Э: { key: "Quote", shift: true },
Ю: { key: "Period", shift: true },
Я: { key: "KeyZ", shift: true },
а: { key: "KeyF" },
б: { key: "Comma" },
в: { key: "KeyD" },
г: { key: "KeyU" },
д: { key: "KeyL" },
е: { key: "KeyT" },
ё: { key: "Backquote" },
ж: { key: "Semicolon" },
з: { key: "KeyP" },
и: { key: "KeyB" },
й: { key: "KeyQ" },
к: { key: "KeyR" },
л: { key: "KeyK" },
м: { key: "KeyV" },
н: { key: "KeyY" },
о: { key: "KeyJ" },
п: { key: "KeyG" },
р: { key: "KeyH" },
с: { key: "KeyC" },
т: { key: "KeyN" },
у: { key: "KeyE" },
ф: { key: "KeyA" },
х: { key: "BracketLeft" },
ц: { key: "KeyW" },
ч: { key: "KeyX" },
ш: { key: "KeyI" },
щ: { key: "KeyO" },
ъ: { key: "BracketRight" },
ы: { key: "KeyS" },
ь: { key: "KeyM" },
э: { key: "Quote" },
ю: { key: "Period" },
я: { key: "KeyZ" },
'"': { key: "Digit2", shift: true },
"№": { key: "Digit3", shift: true },
";": { key: "Digit4", shift: true },
":": { key: "Digit6", shift: true },
"?": { key: "Digit7", shift: true },
".": { key: "Slash" },
",": { key: "Slash", shift: true },
} as Record<string, KeyCombo>;
export const keyDisplayMap = {
...en_US.keyDisplayMap,
KeyF: "а",
Comma: "б",
KeyD: "в",
KeyU: "г",
KeyL: "д",
KeyT: "е",
Backquote: "ё",
Semicolon: "ж",
KeyP: "з",
KeyB: "и",
KeyQ: "й",
KeyR: "к",
KeyK: "л",
KeyV: "м",
KeyY: "н",
KeyJ: "о",
KeyG: "п",
KeyH: "р",
KeyC: "с",
KeyN: "т",
KeyE: "у",
KeyA: "ф",
BracketLeft: "х",
KeyW: "ц",
KeyX: "ч",
KeyI: "ш",
KeyO: "щ",
BracketRight: "ъ",
KeyS: "ы",
KeyM: "ь",
Quote: "э",
Period: "ю",
KeyZ: "я",
Slash: ".",
"(KeyF)": "А",
"(Comma)": "Б",
"(KeyD)": "В",
"(KeyU)": "Г",
"(KeyL)": "Д",
"(KeyT)": "Е",
"(Backquote)": "Ё",
"(Semicolon)": "Ж",
"(KeyP)": "З",
"(KeyB)": "И",
"(KeyQ)": "Й",
"(KeyR)": "К",
"(KeyK)": "Л",
"(KeyV)": "М",
"(KeyY)": "Н",
"(KeyJ)": "О",
"(KeyG)": "П",
"(KeyH)": "Р",
"(KeyC)": "С",
"(KeyN)": "Т",
"(KeyE)": "У",
"(KeyA)": "Ф",
"(BracketLeft)": "Х",
"(KeyW)": "Ц",
"(KeyX)": "Ч",
"(KeyI)": "Ш",
"(KeyO)": "Щ",
"(BracketRight)": "Ъ",
"(KeyS)": "Ы",
"(KeyM)": "Ь",
"(Quote)": "Э",
"(Period)": "Ю",
"(KeyZ)": "Я",
"(Digit2)": '"',
"(Digit3)": "№",
"(Digit4)": ";",
"(Digit6)": ":",
"(Digit7)": "?",
"(Slash)": ",",
};
export const modifierDisplayMap = en_US.modifierDisplayMap;
export const virtualKeyboard = en_US.virtualKeyboard;
export const ru_RU: KeyboardLayout = {
isoCode,
name,
chars,
keyDisplayMap,
modifierDisplayMap,
virtualKeyboard,
};

View File

@@ -0,0 +1,157 @@
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts";
import { en_US } from "./en_US"; // for fallback of keyDisplayMap, modifierDisplayMap, and virtualKeyboard
const name = "Slovenian";
const isoCode = "sl-SI";
export const chars = {
A: { key: "KeyA", shift: true },
B: { key: "KeyB", shift: true },
C: { key: "KeyC", shift: true },
Č: { key: "Semicolon", shift: true },
Ć: { key: "Quote", shift: true },
D: { key: "KeyD", shift: true },
Đ: { key: "BracketRight", 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 },
Š: { key: "BracketLeft", 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: "KeyZ", shift: true },
Z: { key: "KeyY", shift: true },
Ž: { key: "Backslash", shift: true },
a: { key: "KeyA" },
b: { key: "KeyB" },
c: { key: "KeyC" },
č: { key: "Semicolon" },
ć: { key: "Quote" },
d: { key: "KeyD" },
đ: { key: "BracketRight" },
e: { key: "KeyE" },
f: { key: "KeyF" },
g: { key: "KeyG" },
h: { key: "KeyH" },
i: { key: "KeyI" },
j: { key: "KeyJ" },
k: { key: "KeyK" },
l: { key: "KeyL" },
m: { key: "KeyM" },
n: { key: "KeyN" },
o: { key: "KeyO" },
p: { key: "KeyP" },
q: { key: "KeyQ" },
r: { key: "KeyR" },
s: { key: "KeyS" },
š: { key: "BracketLeft" },
t: { key: "KeyT" },
u: { key: "KeyU" },
v: { key: "KeyV" },
w: { key: "KeyW" },
x: { key: "KeyX" },
y: { key: "KeyZ" },
z: { key: "KeyY" },
ž: { key: "Backslash" },
1: { key: "Digit1" },
"!": { key: "Digit1", shift: true },
2: { key: "Digit2" },
'"': { key: "Digit2", shift: true },
3: { key: "Digit3" },
"#": { key: "Digit3", shift: true },
4: { key: "Digit4" },
$: { key: "Digit4", shift: true },
5: { key: "Digit5" },
"%": { key: "Digit5", shift: true },
6: { key: "Digit6" },
"&": { key: "Digit6", shift: true },
7: { key: "Digit7" },
"/": { key: "Digit7", shift: true },
8: { key: "Digit8" },
"(": { key: "Digit8", shift: true },
9: { key: "Digit9" },
")": { key: "Digit9", shift: true },
0: { key: "Digit0" },
"=": { key: "Digit0", shift: true },
"'": { key: "Minus" },
"?": { key: "Minus", shift: true },
"+": { key: "Equal" },
"*": { key: "Equal", shift: true },
"<": { key: "IntlBackslash" },
">": { key: "IntlBackslash", shift: true },
",": { key: "Comma" },
";": { key: "Comma", shift: true },
".": { key: "Period" },
":": { key: "Period", shift: true },
"-": { key: "Slash" },
_: { key: "Slash", shift: true },
"~": { key: "Digit1", shift: true },
ˇ: { key: "Digit2", shift: true },
"^": { key: "Digit3", shift: true },
"˘": { key: "Digit4", shift: true },
"°": { key: "Digit5", shift: true },
"˛": { key: "Digit6", shift: true },
"`": { key: "Digit7", shift: true },
"˙": { key: "Digit8", shift: true },
"´": { key: "Digit9", shift: true },
"˝": { key: "Digit0", shift: true },
"¨": { key: "Minus", shift: true },
"¸": { key: "Equal", shift: true },
"\\": { key: "KeyQ", AltGr: true },
"|": { key: "KeyW", AltGr: true },
"€": { key: "KeyE", AltGr: true },
"÷": { key: "BracketLeft", AltGr: true },
"×": { key: "BracketRight", AltGr: true },
"[": { key: "KeyF", AltGr: true },
"]": { key: "KeyG", AltGr: true },
ł: { key: "KeyK", AltGr: true },
Ł: { key: "KeyL", AltGr: true },
ß: { key: "Quote", AltGr: true },
"¤": { key: "Backslash", AltGr: true },
"@": { key: "KeyV", AltGr: true },
"{": { key: "KeyB", AltGr: true },
"}": { key: "KeyN", AltGr: true },
"§": { key: "KeyM", AltGr: true },
// "<": { key: "Comma", AltGr: true }, // Can be typed in two different locations (`IntlBackslash`)
// ">": { key: "Period", AltGr: true }, // Can be typed in two different locations (`IntlBackslash+Shift`)
" ": { key: "Space" },
"\n": { key: "Enter" },
Enter: { key: "Enter" },
Escape: { key: "Escape" },
Tab: { key: "Tab" },
PrintScreen: { key: "Prt Sc" },
SystemRequest: { key: "Prt Sc", shift: true },
ScrollLock: { key: "ScrollLock" },
Pause: { key: "Pause" },
Break: { key: "Pause", shift: true },
Insert: { key: "Insert" },
Delete: { key: "Delete" },
} as Record<string, KeyCombo>;
export const sl_SI: KeyboardLayout = {
isoCode: isoCode,
name: name,
chars: chars,
// TODO need to localize these maps and layouts
keyDisplayMap: en_US.keyDisplayMap,
modifierDisplayMap: en_US.modifierDisplayMap,
virtualKeyboard: en_US.virtualKeyboard,
};

View File

@@ -1,6 +1,8 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
import { modifierDisplayMap, keyDisplayMap, virtualKeyboard } from "./en_US"
export const name = "Svenska";
const name = "Svenska";
const isoCode = "sv-SE";
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
@@ -162,3 +164,12 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const sv_SE: KeyboardLayout = {
isoCode,
name,
chars,
keyDisplayMap,
modifierDisplayMap,
virtualKeyboard,
};

View File

@@ -105,6 +105,11 @@ export const keys = {
Space: 0x2c,
SystemRequest: 0x9a,
Tab: 0x2b,
Yen: 0x89,
KeyRO: 0x87,
Henkan: 0x8a,
Muhenkan: 0x8b,
KatakanaHiragana: 0x88,
} as Record<string, number>;
export const modifiers = {

View File

@@ -8,7 +8,7 @@ export const useKeyboardEvents = (
pasteCaptureRef?: React.RefObject<HTMLTextAreaElement>,
isReinitializingGadget?: boolean
) => {
const { sendKeyboardEvent, resetKeyboardState } = useKeyboard();
const { sendKeyboardEvent, sendKeypress, resetKeyboardState } = useKeyboard();
const { setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive } = useHidStore();
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
@@ -66,8 +66,15 @@ export const useKeyboardEvents = (
}, 10);
}
// Send per-key press event
const hidKey = keys[code];
if (hidKey !== undefined) {
sendKeypress(hidKey, true);
}
// Still update the full state for legacy compatibility and UI display
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
}, [handleModifierKeys, sendKeyboardEvent, isKeyboardLedManagedByHost, setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive, overrideCtrlV, pasteCaptureRef, isReinitializingGadget]);
}, [handleModifierKeys, sendKeyboardEvent, sendKeypress, isKeyboardLedManagedByHost, setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive, overrideCtrlV, pasteCaptureRef, isReinitializingGadget]);
const keyUpHandler = useCallback((e: KeyboardEvent) => {
if (isReinitializingGadget) return;
@@ -86,8 +93,15 @@ export const useKeyboardEvents = (
prev.activeModifiers.filter(k => k !== modifiers[e.code]),
);
// Send per-key release event
const hidKey = keys[e.code];
if (hidKey !== undefined) {
sendKeypress(hidKey, false);
}
// Still update the full state for legacy compatibility and UI display
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
}, [handleModifierKeys, sendKeyboardEvent, isKeyboardLedManagedByHost, setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive]);
}, [handleModifierKeys, sendKeyboardEvent, sendKeypress, isKeyboardLedManagedByHost, setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive]);
const setupKeyboardEvents = useCallback(() => {
const abortController = new AbortController();

View File

@@ -462,6 +462,11 @@ export default function PCHome() {
setDiskChannel(diskDataChannel);
};
const hidDataChannel = pc.createDataChannel("hid");
hidDataChannel.onopen = () => {
useRTCStore.getState().setHidChannel(hidDataChannel);
};
setPeerConnection(pc);
}, [
forceHttp,

90
usb.go
View File

@@ -6,6 +6,9 @@ import (
"sync"
"time"
"github.com/pion/webrtc/v4"
"kvm/internal/hidrpc"
"kvm/internal/usbgadget"
)
@@ -44,6 +47,28 @@ func initUsbGadget() {
gadget.SetOnKeyboardStateChange(func(state usbgadget.KeyboardState) {
if currentSession != nil {
writeJSONRPCEvent("keyboardLedState", state, currentSession)
// Send HID-RPC LED state message
if currentSession.HidChannel != nil {
var raw byte
if state.NumLock {
raw |= usbgadget.KeyboardLedMaskNumLock
}
if state.CapsLock {
raw |= usbgadget.KeyboardLedMaskCapsLock
}
if state.ScrollLock {
raw |= usbgadget.KeyboardLedMaskScrollLock
}
if state.Compose {
raw |= usbgadget.KeyboardLedMaskCompose
}
if state.Kana {
raw |= usbgadget.KeyboardLedMaskKana
}
ledData := hidrpc.MarshalKeyboardLedState(raw)
currentSession.HidChannel.Send(ledData)
}
}
})
@@ -111,6 +136,49 @@ func rpcKeyboardReport(modifier uint8, keys []uint8) error {
return gadget.KeyboardReport(modifier, keys)
}
func rpcKeypressReport(key uint8, press bool) error {
return gadget.KeypressReport(key, press)
}
func rpcKeypressKeepAlive() error {
return gadget.KeypressKeepAlive()
}
func handleHidChannel(d *webrtc.DataChannel, session *Session) {
hidServer := hidrpc.NewServer(&hidRpcHandler{session: session})
d.OnMessage(func(msg webrtc.DataChannelMessage) {
if err := hidServer.HandleMessage(msg.Data); err != nil {
usbLogger.Warn().Err(err).Msg("HID-RPC message handling error")
}
})
}
type hidRpcHandler struct {
session *Session
}
func (h *hidRpcHandler) HandleKeyboardReport(modifier byte, keys []byte) error {
return rpcKeyboardReport(modifier, keys)
}
func (h *hidRpcHandler) HandleKeypressReport(key byte, press bool) error {
return rpcKeypressReport(key, press)
}
func (h *hidRpcHandler) HandleKeypressKeepAlive() error {
return rpcKeypressKeepAlive()
}
func (h *hidRpcHandler) HandleKeyboardMacroReport(data []byte) error {
// TODO: Implement macro handling
return nil
}
func (h *hidRpcHandler) HandleCancelKeyboardMacro() error {
// TODO: Implement macro cancellation
return nil
}
func rpcAbsMouseReport(x, y int, buttons uint8) error {
return gadget.AbsMouseReport(x, y, buttons)
}
@@ -206,6 +274,28 @@ func rpcReinitializeUsbGadget() error {
gadget.SetOnKeyboardStateChange(func(state usbgadget.KeyboardState) {
if currentSession != nil {
writeJSONRPCEvent("keyboardLedState", state, currentSession)
// Send HID-RPC LED state message
if currentSession.HidChannel != nil {
var raw byte
if state.NumLock {
raw |= usbgadget.KeyboardLedMaskNumLock
}
if state.CapsLock {
raw |= usbgadget.KeyboardLedMaskCapsLock
}
if state.ScrollLock {
raw |= usbgadget.KeyboardLedMaskScrollLock
}
if state.Compose {
raw |= usbgadget.KeyboardLedMaskCompose
}
if state.Kana {
raw |= usbgadget.KeyboardLedMaskKana
}
ledData := hidrpc.MarshalKeyboardLedState(raw)
currentSession.HidChannel.Send(ledData)
}
}
})
gadget.SetOnHidDeviceMissing(func(device string, err error) {

View File

@@ -142,6 +142,9 @@ func newSession(sessionConfig SessionConfig) (*Session, error) {
handleTerminalChannel(d)
case "serial":
handleSerialChannel(d)
case "hid":
session.HidChannel = d
go handleHidChannel(d, session)
default:
if strings.HasPrefix(d.Label(), uploadIdPrefix) {
go handleUploadChannel(d)