This commit is contained in:
mofeng-git
2025-12-28 18:19:16 +08:00
commit d143d158e4
771 changed files with 220548 additions and 0 deletions

176
web/src/lib/charToHid.ts Normal file
View File

@@ -0,0 +1,176 @@
// Character to JavaScript keyCode mapping for text paste functionality
// Maps printable ASCII characters to JavaScript keyCodes that the backend expects
// The backend (keymap.rs) will convert these JS keyCodes to USB HID keycodes
export interface CharKeyMapping {
keyCode: number // JavaScript keyCode (same as KeyboardEvent.keyCode)
shift: boolean // Whether Shift modifier is needed
}
// US QWERTY keyboard layout mapping
// Maps characters to their JavaScript keyCode and whether Shift is required
const charToKeyMap: Record<string, CharKeyMapping> = {
// Lowercase letters (no shift) - JS keyCodes 65-90
a: { keyCode: 65, shift: false },
b: { keyCode: 66, shift: false },
c: { keyCode: 67, shift: false },
d: { keyCode: 68, shift: false },
e: { keyCode: 69, shift: false },
f: { keyCode: 70, shift: false },
g: { keyCode: 71, shift: false },
h: { keyCode: 72, shift: false },
i: { keyCode: 73, shift: false },
j: { keyCode: 74, shift: false },
k: { keyCode: 75, shift: false },
l: { keyCode: 76, shift: false },
m: { keyCode: 77, shift: false },
n: { keyCode: 78, shift: false },
o: { keyCode: 79, shift: false },
p: { keyCode: 80, shift: false },
q: { keyCode: 81, shift: false },
r: { keyCode: 82, shift: false },
s: { keyCode: 83, shift: false },
t: { keyCode: 84, shift: false },
u: { keyCode: 85, shift: false },
v: { keyCode: 86, shift: false },
w: { keyCode: 87, shift: false },
x: { keyCode: 88, shift: false },
y: { keyCode: 89, shift: false },
z: { keyCode: 90, shift: false },
// Uppercase letters (with shift) - same keyCodes, just with Shift
A: { keyCode: 65, shift: true },
B: { keyCode: 66, shift: true },
C: { keyCode: 67, shift: true },
D: { keyCode: 68, shift: true },
E: { keyCode: 69, shift: true },
F: { keyCode: 70, shift: true },
G: { keyCode: 71, shift: true },
H: { keyCode: 72, shift: true },
I: { keyCode: 73, shift: true },
J: { keyCode: 74, shift: true },
K: { keyCode: 75, shift: true },
L: { keyCode: 76, shift: true },
M: { keyCode: 77, shift: true },
N: { keyCode: 78, shift: true },
O: { keyCode: 79, shift: true },
P: { keyCode: 80, shift: true },
Q: { keyCode: 81, shift: true },
R: { keyCode: 82, shift: true },
S: { keyCode: 83, shift: true },
T: { keyCode: 84, shift: true },
U: { keyCode: 85, shift: true },
V: { keyCode: 86, shift: true },
W: { keyCode: 87, shift: true },
X: { keyCode: 88, shift: true },
Y: { keyCode: 89, shift: true },
Z: { keyCode: 90, shift: true },
// Numbers (no shift) - JS keyCodes 48-57
'0': { keyCode: 48, shift: false },
'1': { keyCode: 49, shift: false },
'2': { keyCode: 50, shift: false },
'3': { keyCode: 51, shift: false },
'4': { keyCode: 52, shift: false },
'5': { keyCode: 53, shift: false },
'6': { keyCode: 54, shift: false },
'7': { keyCode: 55, shift: false },
'8': { keyCode: 56, shift: false },
'9': { keyCode: 57, shift: false },
// Shifted number row symbols (US layout)
')': { keyCode: 48, shift: true }, // Shift + 0
'!': { keyCode: 49, shift: true }, // Shift + 1
'@': { keyCode: 50, shift: true }, // Shift + 2
'#': { keyCode: 51, shift: true }, // Shift + 3
$: { keyCode: 52, shift: true }, // Shift + 4
'%': { keyCode: 53, shift: true }, // Shift + 5
'^': { keyCode: 54, shift: true }, // Shift + 6
'&': { keyCode: 55, shift: true }, // Shift + 7
'*': { keyCode: 56, shift: true }, // Shift + 8
'(': { keyCode: 57, shift: true }, // Shift + 9
// Punctuation and symbols (no shift) - US layout JS keyCodes
'-': { keyCode: 189, shift: false }, // Minus
'=': { keyCode: 187, shift: false }, // Equal
'[': { keyCode: 219, shift: false }, // Left bracket
']': { keyCode: 221, shift: false }, // Right bracket
'\\': { keyCode: 220, shift: false }, // Backslash
';': { keyCode: 186, shift: false }, // Semicolon
"'": { keyCode: 222, shift: false }, // Apostrophe/Quote
'`': { keyCode: 192, shift: false }, // Grave/Backtick
',': { keyCode: 188, shift: false }, // Comma
'.': { keyCode: 190, shift: false }, // Period
'/': { keyCode: 191, shift: false }, // Slash
// Shifted punctuation and symbols (US layout)
_: { keyCode: 189, shift: true }, // Shift + Minus = Underscore
'+': { keyCode: 187, shift: true }, // Shift + Equal = Plus
'{': { keyCode: 219, shift: true }, // Shift + [ = {
'}': { keyCode: 221, shift: true }, // Shift + ] = }
'|': { keyCode: 220, shift: true }, // Shift + \ = |
':': { keyCode: 186, shift: true }, // Shift + ; = :
'"': { keyCode: 222, shift: true }, // Shift + ' = "
'~': { keyCode: 192, shift: true }, // Shift + ` = ~
'<': { keyCode: 188, shift: true }, // Shift + , = <
'>': { keyCode: 190, shift: true }, // Shift + . = >
'?': { keyCode: 191, shift: true }, // Shift + / = ?
// Whitespace and control characters
' ': { keyCode: 32, shift: false }, // Space
'\t': { keyCode: 9, shift: false }, // Tab
'\n': { keyCode: 13, shift: false }, // Enter (LF)
'\r': { keyCode: 13, shift: false }, // Enter (CR)
}
/**
* Get the JavaScript keyCode and modifier state for a character
* @param char - Single character to convert
* @returns CharKeyMapping or null if character is not mappable
*/
export function charToKey(char: string): CharKeyMapping | null {
if (char.length !== 1) return null
return charToKeyMap[char] || null
}
/**
* Check if a character can be typed via HID
* @param char - Single character to check
*/
export function isTypableChar(char: string): boolean {
return char.length === 1 && char in charToKeyMap
}
/**
* Get statistics about text typeability
* @param text - Text to analyze
* @returns Object with total, typable, and untypable character counts
*/
export function analyzeText(text: string): {
total: number
typable: number
untypable: number
untypableChars: string[]
} {
const untypableChars: string[] = []
let typable = 0
let untypable = 0
for (const char of text) {
if (isTypableChar(char)) {
typable++
} else {
untypable++
if (!untypableChars.includes(char)) {
untypableChars.push(char)
}
}
}
return {
total: text.length,
typable,
untypable,
untypableChars,
}
}

View File

@@ -0,0 +1,175 @@
// Keyboard layout definitions for virtual keyboard
export interface KeyboardLayout {
id: string
name: string
// Key display labels
keyLabels: Record<string, string>
// Shift variant labels (key in parentheses)
shiftLabels: Record<string, string>
// Virtual keyboard layout rows
layout: {
main: {
macros: string[]
functionRow: string[]
default: string[][]
shift: string[][]
}
control: string[][]
arrows: string[][]
}
}
// English US Layout
export const enUSLayout: KeyboardLayout = {
id: 'en-US',
name: 'English (US)',
keyLabels: {
// Macros
CtrlAltDelete: 'Ctrl+Alt+Del',
AltMetaEscape: 'Alt+Meta+Esc',
CtrlAltBackspace: 'Ctrl+Alt+Back',
// Modifiers
ControlLeft: 'Ctrl',
ControlRight: 'Ctrl',
ShiftLeft: 'Shift',
ShiftRight: 'Shift',
AltLeft: 'Alt',
AltRight: 'Alt',
AltGr: 'AltGr',
MetaLeft: 'Meta',
MetaRight: 'Meta',
// Special keys
Escape: 'Esc',
Backspace: 'Back',
Tab: 'Tab',
CapsLock: 'Caps',
Enter: 'Enter',
Space: ' ',
Menu: 'Menu',
// Navigation
Insert: 'Ins',
Delete: 'Del',
Home: 'Home',
End: 'End',
PageUp: 'PgUp',
PageDown: 'PgDn',
// Arrows
ArrowUp: '\u2191',
ArrowDown: '\u2193',
ArrowLeft: '\u2190',
ArrowRight: '\u2192',
// Control cluster
PrintScreen: 'PrtSc',
ScrollLock: 'ScrLk',
Pause: 'Pause',
// 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',
// 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',
// Symbols
Minus: '-',
Equal: '=',
BracketLeft: '[',
BracketRight: ']',
Backslash: '\\',
Semicolon: ';',
Quote: "'",
Backquote: '`',
Comma: ',',
Period: '.',
Slash: '/',
},
shiftLabels: {
// 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',
// Shifted numbers
Digit1: '!', Digit2: '@', Digit3: '#', Digit4: '$', Digit5: '%',
Digit6: '^', Digit7: '&', Digit8: '*', Digit9: '(', Digit0: ')',
// Shifted symbols
Minus: '_',
Equal: '+',
BracketLeft: '{',
BracketRight: '}',
Backslash: '|',
Semicolon: ':',
Quote: '"',
Backquote: '~',
Comma: '<',
Period: '>',
Slash: '?',
},
layout: {
main: {
macros: ['CtrlAltDelete', 'AltMetaEscape', 'CtrlAltBackspace'],
functionRow: ['Escape', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12'],
default: [
['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', 'AltRight', 'MetaRight', 'Menu', 'ControlRight'],
],
shift: [
['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', 'AltRight', 'MetaRight', 'Menu', 'ControlRight'],
],
},
control: [
['PrintScreen', 'ScrollLock', 'Pause'],
['Insert', 'Home', 'PageUp'],
['Delete', 'End', 'PageDown'],
],
arrows: [
['ArrowUp'],
['ArrowLeft', 'ArrowDown', 'ArrowRight'],
],
},
}
// All available layouts
export const keyboardLayouts: Record<string, KeyboardLayout> = {
'en-US': enUSLayout,
}
// Get layout by ID or return default
export function getKeyboardLayout(id: string): KeyboardLayout {
return keyboardLayouts[id] || enUSLayout
}
// Get key label for display
export function getKeyLabel(layout: KeyboardLayout, keyName: string, isShift: boolean): string {
if (isShift && layout.shiftLabels[keyName]) {
return layout.shiftLabels[keyName]
}
return layout.keyLabels[keyName] || keyName
}

View File

@@ -0,0 +1,223 @@
// Key codes and modifiers correspond to definitions in the
// [Linux USB HID gadget driver](https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt)
// [Universal Serial Bus HID Usage Tables: Section 10](https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf)
export const keys = {
// Letters
KeyA: 0x04,
KeyB: 0x05,
KeyC: 0x06,
KeyD: 0x07,
KeyE: 0x08,
KeyF: 0x09,
KeyG: 0x0a,
KeyH: 0x0b,
KeyI: 0x0c,
KeyJ: 0x0d,
KeyK: 0x0e,
KeyL: 0x0f,
KeyM: 0x10,
KeyN: 0x11,
KeyO: 0x12,
KeyP: 0x13,
KeyQ: 0x14,
KeyR: 0x15,
KeyS: 0x16,
KeyT: 0x17,
KeyU: 0x18,
KeyV: 0x19,
KeyW: 0x1a,
KeyX: 0x1b,
KeyY: 0x1c,
KeyZ: 0x1d,
// Numbers
Digit1: 0x1e,
Digit2: 0x1f,
Digit3: 0x20,
Digit4: 0x21,
Digit5: 0x22,
Digit6: 0x23,
Digit7: 0x24,
Digit8: 0x25,
Digit9: 0x26,
Digit0: 0x27,
// Control keys
Enter: 0x28,
Escape: 0x29,
Backspace: 0x2a,
Tab: 0x2b,
Space: 0x2c,
// Symbols
Minus: 0x2d,
Equal: 0x2e,
BracketLeft: 0x2f,
BracketRight: 0x30,
Backslash: 0x31,
Semicolon: 0x33,
Quote: 0x34,
Backquote: 0x35,
Comma: 0x36,
Period: 0x37,
Slash: 0x38,
// Lock keys
CapsLock: 0x39,
// Function keys
F1: 0x3a,
F2: 0x3b,
F3: 0x3c,
F4: 0x3d,
F5: 0x3e,
F6: 0x3f,
F7: 0x40,
F8: 0x41,
F9: 0x42,
F10: 0x43,
F11: 0x44,
F12: 0x45,
// Control cluster
PrintScreen: 0x46,
ScrollLock: 0x47,
Pause: 0x48,
Insert: 0x49,
Home: 0x4a,
PageUp: 0x4b,
Delete: 0x4c,
End: 0x4d,
PageDown: 0x4e,
// Arrow keys
ArrowRight: 0x4f,
ArrowLeft: 0x50,
ArrowDown: 0x51,
ArrowUp: 0x52,
// Numpad
NumLock: 0x53,
NumpadDivide: 0x54,
NumpadMultiply: 0x55,
NumpadSubtract: 0x56,
NumpadAdd: 0x57,
NumpadEnter: 0x58,
Numpad1: 0x59,
Numpad2: 0x5a,
Numpad3: 0x5b,
Numpad4: 0x5c,
Numpad5: 0x5d,
Numpad6: 0x5e,
Numpad7: 0x5f,
Numpad8: 0x60,
Numpad9: 0x61,
Numpad0: 0x62,
NumpadDecimal: 0x63,
// Non-US keys
IntlBackslash: 0x64,
ContextMenu: 0x65,
Menu: 0x65,
Application: 0x65,
// Extended function keys
F13: 0x68,
F14: 0x69,
F15: 0x6a,
F16: 0x6b,
F17: 0x6c,
F18: 0x6d,
F19: 0x6e,
F20: 0x6f,
F21: 0x70,
F22: 0x71,
F23: 0x72,
F24: 0x73,
// Media/System keys (Consumer Control)
// Note: These are Consumer Control keys, may need special handling
Mute: 0x7f,
VolumeUp: 0x80,
VolumeDown: 0x81,
// Modifiers (these are special - HID codes 0xE0-0xE7)
ControlLeft: 0xe0,
ShiftLeft: 0xe1,
AltLeft: 0xe2,
MetaLeft: 0xe3,
ControlRight: 0xe4,
ShiftRight: 0xe5,
AltRight: 0xe6,
AltGr: 0xe6,
MetaRight: 0xe7,
} as const
export type KeyName = keyof typeof keys
// Modifier bitmasks for HID report byte 0
export const modifiers = {
ControlLeft: 0x01,
ShiftLeft: 0x02,
AltLeft: 0x04,
MetaLeft: 0x08,
ControlRight: 0x10,
ShiftRight: 0x20,
AltRight: 0x40,
AltGr: 0x40,
MetaRight: 0x80,
} as const
export type ModifierName = keyof typeof modifiers
// Map HID key codes to modifier bitmasks
export const hidKeyToModifierMask: Record<number, number> = {
0xe0: 0x01, // ControlLeft
0xe1: 0x02, // ShiftLeft
0xe2: 0x04, // AltLeft
0xe3: 0x08, // MetaLeft
0xe4: 0x10, // ControlRight
0xe5: 0x20, // ShiftRight
0xe6: 0x40, // AltRight
0xe7: 0x80, // MetaRight
}
// Keys that latch (toggle state) instead of being held
export const latchingKeys = ['CapsLock', 'ScrollLock', 'NumLock'] as const
// Modifier key names
export const modifierKeyNames = [
'ControlLeft',
'ControlRight',
'ShiftLeft',
'ShiftRight',
'AltLeft',
'AltRight',
'AltGr',
'MetaLeft',
'MetaRight',
] as const
// Check if a key is a modifier
export function isModifierKey(keyName: string): keyName is ModifierName {
return keyName in modifiers
}
// Get modifier bitmask for a key name
export function getModifierMask(keyName: string): number {
if (keyName in modifiers) {
return modifiers[keyName as ModifierName]
}
return 0
}
// Decode modifier byte into individual states
export function decodeModifiers(modifier: number) {
return {
isShiftActive: (modifier & 0x22) !== 0, // ShiftLeft | ShiftRight
isControlActive: (modifier & 0x11) !== 0, // ControlLeft | ControlRight
isAltActive: (modifier & 0x44) !== 0, // AltLeft | AltRight
isMetaActive: (modifier & 0x88) !== 0, // MetaLeft | MetaRight
}
}

25
web/src/lib/utils.ts Normal file
View File

@@ -0,0 +1,25 @@
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/**
* Generate a UUID v4 with fallback for older browsers
* Uses crypto.randomUUID() if available, otherwise falls back to manual generation
*/
export function generateUUID(): string {
// Use native API if available (modern browsers)
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID()
}
// Fallback: generate UUID v4 manually
// Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}