mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-29 22:56:45 +08:00
refactor(hid): 统一 HID 键盘 CanonicalKey 语义并清理前端布局与输入链路冗余代码
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// API client for One-KVM backend
|
||||
|
||||
import { request, ApiError } from './request'
|
||||
import type { CanonicalKey } from '@/types/generated'
|
||||
|
||||
const API_BASE = '/api'
|
||||
|
||||
@@ -357,7 +358,7 @@ export const hidApi = {
|
||||
}>
|
||||
}>('/hid/otg/self-check'),
|
||||
|
||||
keyboard: async (type: 'down' | 'up', key: number, modifier?: number) => {
|
||||
keyboard: async (type: 'down' | 'up', key: CanonicalKey, modifier?: number) => {
|
||||
await ensureHidConnection()
|
||||
const event: HidKeyboardEvent = {
|
||||
type: type === 'down' ? 'keydown' : 'keyup',
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { CanonicalKey } from '@/types/generated'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
pressedKeys?: string[]
|
||||
pressedKeys?: CanonicalKey[]
|
||||
capsLock?: boolean
|
||||
mousePosition?: { x: number; y: number }
|
||||
debugMode?: boolean
|
||||
@@ -18,13 +19,14 @@ const keyNameMap: Record<string, string> = {
|
||||
MetaLeft: 'Win', MetaRight: 'Win',
|
||||
ControlLeft: 'Ctrl', ControlRight: 'Ctrl',
|
||||
ShiftLeft: 'Shift', ShiftRight: 'Shift',
|
||||
AltLeft: 'Alt', AltRight: 'Alt',
|
||||
AltLeft: 'Alt', AltRight: 'AltGr',
|
||||
CapsLock: 'Caps', NumLock: 'Num', ScrollLock: 'Scroll',
|
||||
Backspace: 'Back', Delete: 'Del',
|
||||
ArrowUp: '↑', ArrowDown: '↓', ArrowLeft: '←', ArrowRight: '→',
|
||||
Escape: 'Esc', Enter: 'Enter', Tab: 'Tab', Space: 'Space',
|
||||
PageUp: 'PgUp', PageDown: 'PgDn',
|
||||
Insert: 'Ins', Home: 'Home', End: 'End',
|
||||
ContextMenu: 'Menu',
|
||||
}
|
||||
|
||||
const keysDisplay = computed(() => {
|
||||
|
||||
@@ -69,24 +69,24 @@ async function typeChar(char: string, signal: AbortSignal): Promise<boolean> {
|
||||
return true
|
||||
}
|
||||
|
||||
const { hidCode, shift } = mapping
|
||||
const { key, shift } = mapping
|
||||
const modifier = shift ? 0x02 : 0
|
||||
|
||||
try {
|
||||
// Send keydown
|
||||
await hidApi.keyboard('down', hidCode, modifier)
|
||||
await hidApi.keyboard('down', key, modifier)
|
||||
|
||||
// Small delay between down and up to ensure key is registered
|
||||
await sleep(5)
|
||||
|
||||
if (signal.aborted) {
|
||||
// Even if aborted, still send keyup to release the key
|
||||
await hidApi.keyboard('up', hidCode, modifier)
|
||||
await hidApi.keyboard('up', key, modifier)
|
||||
return false
|
||||
}
|
||||
|
||||
// Send keyup
|
||||
await hidApi.keyboard('up', hidCode, modifier)
|
||||
await hidApi.keyboard('up', key, modifier)
|
||||
|
||||
// Additional small delay after keyup to ensure it's processed
|
||||
await sleep(2)
|
||||
@@ -96,7 +96,7 @@ async function typeChar(char: string, signal: AbortSignal): Promise<boolean> {
|
||||
console.error('[Paste] Failed to type character:', char, error)
|
||||
// Try to release the key even on error
|
||||
try {
|
||||
await hidApi.keyboard('up', hidCode, modifier)
|
||||
await hidApi.keyboard('up', key, modifier)
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ import { useI18n } from 'vue-i18n'
|
||||
import Keyboard from 'simple-keyboard'
|
||||
import 'simple-keyboard/build/css/index.css'
|
||||
import { hidApi } from '@/api'
|
||||
import { CanonicalKey } from '@/types/generated'
|
||||
import {
|
||||
keys,
|
||||
consumerKeys,
|
||||
latchingKeys,
|
||||
modifiers,
|
||||
updateModifierMaskForHidKey,
|
||||
updateModifierMaskForKey,
|
||||
type KeyName,
|
||||
type ConsumerKeyName,
|
||||
} from '@/lib/keyboardMappings'
|
||||
@@ -23,13 +24,15 @@ import {
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
attached?: boolean
|
||||
capsLock?: boolean
|
||||
pressedKeys?: CanonicalKey[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'update:attached', value: boolean): void
|
||||
(e: 'keyDown', key: string): void
|
||||
(e: 'keyUp', key: string): void
|
||||
(e: 'keyDown', key: CanonicalKey): void
|
||||
(e: 'keyUp', key: CanonicalKey): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -45,13 +48,17 @@ const arrowsKeyboard = ref<Keyboard | null>(null)
|
||||
|
||||
// Pressed keys tracking
|
||||
const pressedModifiers = ref<number>(0)
|
||||
const keysDown = ref<string[]>([])
|
||||
const keysDown = ref<CanonicalKey[]>([])
|
||||
|
||||
// Shift state for display
|
||||
const isShiftActive = computed(() => {
|
||||
return (pressedModifiers.value & 0x22) !== 0
|
||||
})
|
||||
|
||||
const areLettersUppercase = computed(() => {
|
||||
return Boolean(props.capsLock) !== isShiftActive.value
|
||||
})
|
||||
|
||||
const layoutName = computed(() => {
|
||||
return isShiftActive.value ? 'shift' : 'default'
|
||||
})
|
||||
@@ -63,7 +70,12 @@ const keyNamesForDownKeys = computed(() => {
|
||||
.filter(([_, mask]) => (activeModifierMask & mask) !== 0)
|
||||
.map(([name]) => name)
|
||||
|
||||
return [...modifierNames, ...keysDown.value, ' ']
|
||||
return Array.from(new Set([
|
||||
...modifierNames,
|
||||
...(props.pressedKeys ?? []),
|
||||
...keysDown.value,
|
||||
...(props.capsLock ? ['CapsLock'] : []),
|
||||
]))
|
||||
})
|
||||
|
||||
// Dragging state (for floating mode)
|
||||
@@ -88,7 +100,7 @@ const keyboardLayout = {
|
||||
'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',
|
||||
'ControlLeft MetaLeft AltLeft Space AltRight MetaRight ContextMenu ControlRight',
|
||||
],
|
||||
shift: [
|
||||
'CtrlAltDelete AltMetaEscape CtrlAltBackspace',
|
||||
@@ -97,7 +109,7 @@ const keyboardLayout = {
|
||||
'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',
|
||||
'ControlLeft MetaLeft AltLeft Space AltRight MetaRight ContextMenu ControlRight',
|
||||
],
|
||||
},
|
||||
control: {
|
||||
@@ -148,11 +160,10 @@ const keyDisplayMap = computed<Record<string, string>>(() => {
|
||||
ShiftLeft: 'Shift',
|
||||
ShiftRight: 'Shift',
|
||||
AltLeft: 'Alt',
|
||||
AltRight: 'Alt',
|
||||
AltGr: 'AltGr',
|
||||
AltRight: 'AltGr',
|
||||
MetaLeft: metaLabel,
|
||||
MetaRight: metaLabel,
|
||||
Menu: 'Menu',
|
||||
ContextMenu: 'Menu',
|
||||
|
||||
// Special keys
|
||||
Escape: 'Esc',
|
||||
@@ -187,20 +198,60 @@ const keyDisplayMap = computed<Record<string, string>>(() => {
|
||||
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',
|
||||
KeyA: areLettersUppercase.value ? 'A' : 'a',
|
||||
KeyB: areLettersUppercase.value ? 'B' : 'b',
|
||||
KeyC: areLettersUppercase.value ? 'C' : 'c',
|
||||
KeyD: areLettersUppercase.value ? 'D' : 'd',
|
||||
KeyE: areLettersUppercase.value ? 'E' : 'e',
|
||||
KeyF: areLettersUppercase.value ? 'F' : 'f',
|
||||
KeyG: areLettersUppercase.value ? 'G' : 'g',
|
||||
KeyH: areLettersUppercase.value ? 'H' : 'h',
|
||||
KeyI: areLettersUppercase.value ? 'I' : 'i',
|
||||
KeyJ: areLettersUppercase.value ? 'J' : 'j',
|
||||
KeyK: areLettersUppercase.value ? 'K' : 'k',
|
||||
KeyL: areLettersUppercase.value ? 'L' : 'l',
|
||||
KeyM: areLettersUppercase.value ? 'M' : 'm',
|
||||
KeyN: areLettersUppercase.value ? 'N' : 'n',
|
||||
KeyO: areLettersUppercase.value ? 'O' : 'o',
|
||||
KeyP: areLettersUppercase.value ? 'P' : 'p',
|
||||
KeyQ: areLettersUppercase.value ? 'Q' : 'q',
|
||||
KeyR: areLettersUppercase.value ? 'R' : 'r',
|
||||
KeyS: areLettersUppercase.value ? 'S' : 's',
|
||||
KeyT: areLettersUppercase.value ? 'T' : 't',
|
||||
KeyU: areLettersUppercase.value ? 'U' : 'u',
|
||||
KeyV: areLettersUppercase.value ? 'V' : 'v',
|
||||
KeyW: areLettersUppercase.value ? 'W' : 'w',
|
||||
KeyX: areLettersUppercase.value ? 'X' : 'x',
|
||||
KeyY: areLettersUppercase.value ? 'Y' : 'y',
|
||||
KeyZ: areLettersUppercase.value ? 'Z' : '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',
|
||||
// Letter labels in the shifted layout follow CapsLock xor Shift too
|
||||
'(KeyA)': areLettersUppercase.value ? 'A' : 'a',
|
||||
'(KeyB)': areLettersUppercase.value ? 'B' : 'b',
|
||||
'(KeyC)': areLettersUppercase.value ? 'C' : 'c',
|
||||
'(KeyD)': areLettersUppercase.value ? 'D' : 'd',
|
||||
'(KeyE)': areLettersUppercase.value ? 'E' : 'e',
|
||||
'(KeyF)': areLettersUppercase.value ? 'F' : 'f',
|
||||
'(KeyG)': areLettersUppercase.value ? 'G' : 'g',
|
||||
'(KeyH)': areLettersUppercase.value ? 'H' : 'h',
|
||||
'(KeyI)': areLettersUppercase.value ? 'I' : 'i',
|
||||
'(KeyJ)': areLettersUppercase.value ? 'J' : 'j',
|
||||
'(KeyK)': areLettersUppercase.value ? 'K' : 'k',
|
||||
'(KeyL)': areLettersUppercase.value ? 'L' : 'l',
|
||||
'(KeyM)': areLettersUppercase.value ? 'M' : 'm',
|
||||
'(KeyN)': areLettersUppercase.value ? 'N' : 'n',
|
||||
'(KeyO)': areLettersUppercase.value ? 'O' : 'o',
|
||||
'(KeyP)': areLettersUppercase.value ? 'P' : 'p',
|
||||
'(KeyQ)': areLettersUppercase.value ? 'Q' : 'q',
|
||||
'(KeyR)': areLettersUppercase.value ? 'R' : 'r',
|
||||
'(KeyS)': areLettersUppercase.value ? 'S' : 's',
|
||||
'(KeyT)': areLettersUppercase.value ? 'T' : 't',
|
||||
'(KeyU)': areLettersUppercase.value ? 'U' : 'u',
|
||||
'(KeyV)': areLettersUppercase.value ? 'V' : 'v',
|
||||
'(KeyW)': areLettersUppercase.value ? 'W' : 'w',
|
||||
'(KeyX)': areLettersUppercase.value ? 'X' : 'x',
|
||||
'(KeyY)': areLettersUppercase.value ? 'Y' : 'y',
|
||||
'(KeyZ)': areLettersUppercase.value ? 'Z' : 'z',
|
||||
|
||||
// Numbers
|
||||
Digit1: '1', Digit2: '2', Digit3: '3', Digit4: '4', Digit5: '5',
|
||||
@@ -303,47 +354,47 @@ async function onKeyDown(key: string) {
|
||||
const keyCode = keys[cleanKey as KeyName]
|
||||
|
||||
// Handle latching keys (Caps Lock, etc.)
|
||||
if ((latchingKeys as readonly string[]).includes(cleanKey)) {
|
||||
emit('keyDown', cleanKey)
|
||||
if (latchingKeys.some(latchingKey => latchingKey === keyCode)) {
|
||||
emit('keyDown', keyCode)
|
||||
const currentMask = pressedModifiers.value & 0xff
|
||||
await sendKeyPress(keyCode, true, currentMask)
|
||||
setTimeout(() => {
|
||||
sendKeyPress(keyCode, false, currentMask)
|
||||
emit('keyUp', cleanKey)
|
||||
emit('keyUp', keyCode)
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle modifier keys (toggle)
|
||||
if (cleanKey in modifiers) {
|
||||
const mask = modifiers[cleanKey as keyof typeof modifiers]
|
||||
const mask = modifiers[keyCode] ?? 0
|
||||
if (mask !== 0) {
|
||||
const isCurrentlyDown = (pressedModifiers.value & mask) !== 0
|
||||
|
||||
if (isCurrentlyDown) {
|
||||
const nextMask = pressedModifiers.value & ~mask
|
||||
pressedModifiers.value = nextMask
|
||||
await sendKeyPress(keyCode, false, nextMask)
|
||||
emit('keyUp', cleanKey)
|
||||
emit('keyUp', keyCode)
|
||||
} else {
|
||||
const nextMask = pressedModifiers.value | mask
|
||||
pressedModifiers.value = nextMask
|
||||
await sendKeyPress(keyCode, true, nextMask)
|
||||
emit('keyDown', cleanKey)
|
||||
emit('keyDown', keyCode)
|
||||
}
|
||||
updateKeyboardButtonTheme()
|
||||
return
|
||||
}
|
||||
|
||||
// Regular key: press and release
|
||||
keysDown.value.push(cleanKey)
|
||||
emit('keyDown', cleanKey)
|
||||
keysDown.value.push(keyCode)
|
||||
emit('keyDown', keyCode)
|
||||
const currentMask = pressedModifiers.value & 0xff
|
||||
await sendKeyPress(keyCode, true, currentMask)
|
||||
updateKeyboardButtonTheme()
|
||||
setTimeout(async () => {
|
||||
keysDown.value = keysDown.value.filter(k => k !== cleanKey)
|
||||
keysDown.value = keysDown.value.filter(k => k !== keyCode)
|
||||
await sendKeyPress(keyCode, false, currentMask)
|
||||
emit('keyUp', cleanKey)
|
||||
emit('keyUp', keyCode)
|
||||
updateKeyboardButtonTheme()
|
||||
}, 50)
|
||||
}
|
||||
@@ -352,7 +403,7 @@ async function onKeyUp() {
|
||||
// Not used for now - we handle up in onKeyDown with setTimeout
|
||||
}
|
||||
|
||||
async function sendKeyPress(keyCode: number, press: boolean, modifierMask: number) {
|
||||
async function sendKeyPress(keyCode: CanonicalKey, press: boolean, modifierMask: number) {
|
||||
try {
|
||||
await hidApi.keyboard(press ? 'down' : 'up', keyCode, modifierMask & 0xff)
|
||||
} catch (err) {
|
||||
@@ -372,7 +423,7 @@ async function executeMacro(steps: MacroStep[]) {
|
||||
for (const mod of step.modifiers) {
|
||||
if (mod in keys) {
|
||||
const modHid = keys[mod as KeyName]
|
||||
macroModifierMask = updateModifierMaskForHidKey(macroModifierMask, modHid, true)
|
||||
macroModifierMask = updateModifierMaskForKey(macroModifierMask, modHid, true)
|
||||
await sendKeyPress(modHid, true, macroModifierMask)
|
||||
}
|
||||
}
|
||||
@@ -394,7 +445,7 @@ async function executeMacro(steps: MacroStep[]) {
|
||||
for (const mod of step.modifiers) {
|
||||
if (mod in keys) {
|
||||
const modHid = keys[mod as KeyName]
|
||||
macroModifierMask = updateModifierMaskForHidKey(macroModifierMask, modHid, false)
|
||||
macroModifierMask = updateModifierMaskForKey(macroModifierMask, modHid, false)
|
||||
await sendKeyPress(modHid, false, macroModifierMask)
|
||||
}
|
||||
}
|
||||
@@ -421,8 +472,12 @@ function updateKeyboardButtonTheme() {
|
||||
}
|
||||
|
||||
// Update layout when shift state changes
|
||||
watch(layoutName, (name) => {
|
||||
mainKeyboard.value?.setOptions({ layoutName: name })
|
||||
watch([layoutName, () => props.capsLock], ([name]) => {
|
||||
mainKeyboard.value?.setOptions({
|
||||
layoutName: name,
|
||||
display: keyDisplayMap.value,
|
||||
})
|
||||
updateKeyboardButtonTheme()
|
||||
})
|
||||
|
||||
// Initialize keyboards with unique selectors
|
||||
@@ -835,12 +890,12 @@ onUnmounted(() => {
|
||||
min-width: 55px;
|
||||
}
|
||||
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="AltGr"] {
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="AltRight"] {
|
||||
flex-grow: 1.25;
|
||||
min-width: 55px;
|
||||
}
|
||||
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="Menu"] {
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="ContextMenu"] {
|
||||
flex-grow: 1.25;
|
||||
min-width: 55px;
|
||||
}
|
||||
@@ -1194,8 +1249,8 @@ html.dark .hg-theme-default .hg-button.down-key,
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="MetaLeft"],
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="MetaRight"],
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="AltLeft"],
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="AltGr"],
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="Menu"] {
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="AltRight"],
|
||||
.vkb .simple-keyboard .hg-button[data-skbtn="ContextMenu"] {
|
||||
min-width: 46px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
// HID input composable - manages keyboard and mouse input
|
||||
// Extracted from ConsoleView.vue for better separation of concerns
|
||||
|
||||
import { ref, type Ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { hidApi } from '@/api'
|
||||
import { keyboardEventToHidCode, updateModifierMaskForHidKey } from '@/lib/keyboardMappings'
|
||||
|
||||
export interface HidInputState {
|
||||
mouseMode: Ref<'absolute' | 'relative'>
|
||||
pressedKeys: Ref<string[]>
|
||||
keyboardLed: Ref<{ capsLock: boolean; numLock: boolean; scrollLock: boolean }>
|
||||
mousePosition: Ref<{ x: number; y: number }>
|
||||
isPointerLocked: Ref<boolean>
|
||||
cursorVisible: Ref<boolean>
|
||||
}
|
||||
|
||||
export interface UseHidInputOptions {
|
||||
videoContainerRef: Ref<HTMLDivElement | null>
|
||||
getVideoElement: () => HTMLElement | null
|
||||
isFullscreen: Ref<boolean>
|
||||
}
|
||||
|
||||
export function useHidInput(options: UseHidInputOptions) {
|
||||
const { t } = useI18n()
|
||||
|
||||
// State
|
||||
const mouseMode = ref<'absolute' | 'relative'>('absolute')
|
||||
const pressedKeys = ref<string[]>([])
|
||||
const keyboardLed = ref({
|
||||
capsLock: false,
|
||||
numLock: false,
|
||||
scrollLock: false,
|
||||
})
|
||||
const activeModifierMask = ref(0)
|
||||
const mousePosition = ref({ x: 0, y: 0 })
|
||||
const lastMousePosition = ref({ x: 0, y: 0 })
|
||||
const isPointerLocked = ref(false)
|
||||
const cursorVisible = ref(localStorage.getItem('hidShowCursor') !== 'false')
|
||||
const pressedMouseButton = ref<'left' | 'right' | 'middle' | null>(null)
|
||||
|
||||
// Error handling - silently handle all HID errors
|
||||
function handleHidError(_error: unknown, _operation: string) {
|
||||
// All HID errors are silently ignored
|
||||
}
|
||||
|
||||
// Check if a key should be blocked
|
||||
function shouldBlockKey(e: KeyboardEvent): boolean {
|
||||
if (options.isFullscreen.value) return true
|
||||
|
||||
const key = e.key.toUpperCase()
|
||||
if (e.ctrlKey && ['W', 'T', 'N'].includes(key)) return false
|
||||
if (key === 'F11') return false
|
||||
if (e.altKey && key === 'TAB') return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Keyboard handlers
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
const container = options.videoContainerRef.value
|
||||
if (!container) return
|
||||
|
||||
if (!options.isFullscreen.value && !container.contains(document.activeElement)) return
|
||||
|
||||
if (shouldBlockKey(e)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
if (!options.isFullscreen.value && (e.metaKey || e.key === 'Meta')) {
|
||||
toast.info(t('console.metaKeyHint'), {
|
||||
description: t('console.metaKeyHintDesc'),
|
||||
duration: 3000,
|
||||
})
|
||||
}
|
||||
|
||||
const keyName = e.key === ' ' ? 'Space' : e.key
|
||||
if (!pressedKeys.value.includes(keyName)) {
|
||||
pressedKeys.value = [...pressedKeys.value, keyName]
|
||||
}
|
||||
|
||||
keyboardLed.value.capsLock = e.getModifierState('CapsLock')
|
||||
keyboardLed.value.numLock = e.getModifierState('NumLock')
|
||||
keyboardLed.value.scrollLock = e.getModifierState('ScrollLock')
|
||||
|
||||
const hidKey = keyboardEventToHidCode(e.code, e.key)
|
||||
if (hidKey === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const modifierMask = updateModifierMaskForHidKey(activeModifierMask.value, hidKey, true)
|
||||
activeModifierMask.value = modifierMask
|
||||
hidApi.keyboard('down', hidKey, modifierMask).catch(err => handleHidError(err, 'keyboard down'))
|
||||
}
|
||||
|
||||
function handleKeyUp(e: KeyboardEvent) {
|
||||
const container = options.videoContainerRef.value
|
||||
if (!container) return
|
||||
|
||||
if (!options.isFullscreen.value && !container.contains(document.activeElement)) return
|
||||
|
||||
if (shouldBlockKey(e)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const keyName = e.key === ' ' ? 'Space' : e.key
|
||||
pressedKeys.value = pressedKeys.value.filter(k => k !== keyName)
|
||||
|
||||
const hidKey = keyboardEventToHidCode(e.code, e.key)
|
||||
if (hidKey === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const modifierMask = updateModifierMaskForHidKey(activeModifierMask.value, hidKey, false)
|
||||
activeModifierMask.value = modifierMask
|
||||
hidApi.keyboard('up', hidKey, modifierMask).catch(err => handleHidError(err, 'keyboard up'))
|
||||
}
|
||||
|
||||
// Mouse handlers
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
const videoElement = options.getVideoElement()
|
||||
if (!videoElement) return
|
||||
|
||||
if (mouseMode.value === 'absolute') {
|
||||
const rect = videoElement.getBoundingClientRect()
|
||||
const x = Math.round((e.clientX - rect.left) / rect.width * 32767)
|
||||
const y = Math.round((e.clientY - rect.top) / rect.height * 32767)
|
||||
|
||||
mousePosition.value = { x, y }
|
||||
hidApi.mouse({ type: 'move_abs', x, y }).catch(err => handleHidError(err, 'mouse move'))
|
||||
} else {
|
||||
if (isPointerLocked.value) {
|
||||
const dx = e.movementX
|
||||
const dy = e.movementY
|
||||
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
const clampedDx = Math.max(-127, Math.min(127, dx))
|
||||
const clampedDy = Math.max(-127, Math.min(127, dy))
|
||||
hidApi.mouse({ type: 'move', x: clampedDx, y: clampedDy }).catch(err => handleHidError(err, 'mouse move'))
|
||||
}
|
||||
|
||||
mousePosition.value = {
|
||||
x: mousePosition.value.x + dx,
|
||||
y: mousePosition.value.y + dy,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
|
||||
const container = options.videoContainerRef.value
|
||||
if (container && document.activeElement !== container) {
|
||||
if (typeof container.focus === 'function') {
|
||||
container.focus()
|
||||
}
|
||||
}
|
||||
|
||||
if (mouseMode.value === 'relative' && !isPointerLocked.value) {
|
||||
requestPointerLock()
|
||||
return
|
||||
}
|
||||
|
||||
const button = e.button === 0 ? 'left' : e.button === 2 ? 'right' : 'middle'
|
||||
pressedMouseButton.value = button
|
||||
hidApi.mouse({ type: 'down', button }).catch(err => handleHidError(err, 'mouse down'))
|
||||
}
|
||||
|
||||
function handleMouseUp(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
handleMouseUpInternal(e.button)
|
||||
}
|
||||
|
||||
function handleWindowMouseUp(e: MouseEvent) {
|
||||
if (pressedMouseButton.value !== null) {
|
||||
handleMouseUpInternal(e.button)
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseUpInternal(rawButton: number) {
|
||||
if (mouseMode.value === 'relative' && !isPointerLocked.value) {
|
||||
pressedMouseButton.value = null
|
||||
return
|
||||
}
|
||||
|
||||
const button = rawButton === 0 ? 'left' : rawButton === 2 ? 'right' : 'middle'
|
||||
|
||||
if (pressedMouseButton.value !== button) return
|
||||
|
||||
pressedMouseButton.value = null
|
||||
hidApi.mouse({ type: 'up', button }).catch(err => handleHidError(err, 'mouse up'))
|
||||
}
|
||||
|
||||
function handleWheel(e: WheelEvent) {
|
||||
e.preventDefault()
|
||||
const scroll = e.deltaY > 0 ? -1 : 1
|
||||
hidApi.mouse({ type: 'scroll', scroll }).catch(err => handleHidError(err, 'mouse scroll'))
|
||||
}
|
||||
|
||||
function handleContextMenu(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
// Pointer lock
|
||||
function requestPointerLock() {
|
||||
const container = options.videoContainerRef.value
|
||||
if (!container) return
|
||||
|
||||
container.requestPointerLock().catch((err: Error) => {
|
||||
toast.error(t('console.pointerLockFailed'), {
|
||||
description: err.message,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function exitPointerLock() {
|
||||
if (document.pointerLockElement) {
|
||||
document.exitPointerLock()
|
||||
}
|
||||
}
|
||||
|
||||
function handlePointerLockChange() {
|
||||
const container = options.videoContainerRef.value
|
||||
isPointerLocked.value = document.pointerLockElement === container
|
||||
|
||||
if (isPointerLocked.value) {
|
||||
mousePosition.value = { x: 0, y: 0 }
|
||||
toast.info(t('console.pointerLocked'), {
|
||||
description: t('console.pointerLockedDesc'),
|
||||
duration: 3000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handlePointerLockError() {
|
||||
isPointerLocked.value = false
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
pressedKeys.value = []
|
||||
activeModifierMask.value = 0
|
||||
if (pressedMouseButton.value !== null) {
|
||||
const button = pressedMouseButton.value
|
||||
pressedMouseButton.value = null
|
||||
hidApi.mouse({ type: 'up', button }).catch(err => handleHidError(err, 'mouse up (blur)'))
|
||||
}
|
||||
}
|
||||
|
||||
// Mode toggle
|
||||
function toggleMouseMode() {
|
||||
if (mouseMode.value === 'relative' && isPointerLocked.value) {
|
||||
exitPointerLock()
|
||||
}
|
||||
|
||||
mouseMode.value = mouseMode.value === 'absolute' ? 'relative' : 'absolute'
|
||||
lastMousePosition.value = { x: 0, y: 0 }
|
||||
mousePosition.value = { x: 0, y: 0 }
|
||||
|
||||
if (mouseMode.value === 'relative') {
|
||||
toast.info(t('console.relativeModeHint'), {
|
||||
description: t('console.relativeModeHintDesc'),
|
||||
duration: 5000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Virtual keyboard handlers
|
||||
function handleVirtualKeyDown(key: string) {
|
||||
if (!pressedKeys.value.includes(key)) {
|
||||
pressedKeys.value = [...pressedKeys.value, key]
|
||||
}
|
||||
}
|
||||
|
||||
function handleVirtualKeyUp(key: string) {
|
||||
pressedKeys.value = pressedKeys.value.filter(k => k !== key)
|
||||
}
|
||||
|
||||
// Cursor visibility
|
||||
function handleCursorVisibilityChange(e: Event) {
|
||||
const customEvent = e as CustomEvent<{ visible: boolean }>
|
||||
cursorVisible.value = customEvent.detail.visible
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
function setupEventListeners() {
|
||||
document.addEventListener('pointerlockchange', handlePointerLockChange)
|
||||
document.addEventListener('pointerlockerror', handlePointerLockError)
|
||||
window.addEventListener('blur', handleBlur)
|
||||
window.addEventListener('mouseup', handleWindowMouseUp)
|
||||
window.addEventListener('cursor-visibility-change', handleCursorVisibilityChange)
|
||||
}
|
||||
|
||||
function cleanupEventListeners() {
|
||||
document.removeEventListener('pointerlockchange', handlePointerLockChange)
|
||||
document.removeEventListener('pointerlockerror', handlePointerLockError)
|
||||
window.removeEventListener('blur', handleBlur)
|
||||
window.removeEventListener('mouseup', handleWindowMouseUp)
|
||||
window.removeEventListener('cursor-visibility-change', handleCursorVisibilityChange)
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
mouseMode,
|
||||
pressedKeys,
|
||||
keyboardLed,
|
||||
mousePosition,
|
||||
isPointerLocked,
|
||||
cursorVisible,
|
||||
|
||||
// Keyboard handlers
|
||||
handleKeyDown,
|
||||
handleKeyUp,
|
||||
|
||||
// Mouse handlers
|
||||
handleMouseMove,
|
||||
handleMouseDown,
|
||||
handleMouseUp,
|
||||
handleWheel,
|
||||
handleContextMenu,
|
||||
|
||||
// Pointer lock
|
||||
requestPointerLock,
|
||||
exitPointerLock,
|
||||
|
||||
// Mode toggle
|
||||
toggleMouseMode,
|
||||
|
||||
// Virtual keyboard
|
||||
handleVirtualKeyDown,
|
||||
handleVirtualKeyUp,
|
||||
|
||||
// Cursor visibility
|
||||
handleCursorVisibilityChange,
|
||||
|
||||
// Lifecycle
|
||||
setupEventListeners,
|
||||
cleanupEventListeners,
|
||||
}
|
||||
}
|
||||
@@ -1,129 +1,130 @@
|
||||
// Character to HID usage mapping for text paste functionality.
|
||||
// The table follows US QWERTY layout semantics.
|
||||
|
||||
import { type CanonicalKey } from '@/types/generated'
|
||||
import { keys } from '@/lib/keyboardMappings'
|
||||
|
||||
export interface CharKeyMapping {
|
||||
hidCode: number // USB HID usage code
|
||||
key: CanonicalKey
|
||||
shift: boolean // Whether Shift modifier is needed
|
||||
}
|
||||
|
||||
const charToKeyMap: Record<string, CharKeyMapping> = {
|
||||
// Lowercase letters
|
||||
a: { hidCode: keys.KeyA, shift: false },
|
||||
b: { hidCode: keys.KeyB, shift: false },
|
||||
c: { hidCode: keys.KeyC, shift: false },
|
||||
d: { hidCode: keys.KeyD, shift: false },
|
||||
e: { hidCode: keys.KeyE, shift: false },
|
||||
f: { hidCode: keys.KeyF, shift: false },
|
||||
g: { hidCode: keys.KeyG, shift: false },
|
||||
h: { hidCode: keys.KeyH, shift: false },
|
||||
i: { hidCode: keys.KeyI, shift: false },
|
||||
j: { hidCode: keys.KeyJ, shift: false },
|
||||
k: { hidCode: keys.KeyK, shift: false },
|
||||
l: { hidCode: keys.KeyL, shift: false },
|
||||
m: { hidCode: keys.KeyM, shift: false },
|
||||
n: { hidCode: keys.KeyN, shift: false },
|
||||
o: { hidCode: keys.KeyO, shift: false },
|
||||
p: { hidCode: keys.KeyP, shift: false },
|
||||
q: { hidCode: keys.KeyQ, shift: false },
|
||||
r: { hidCode: keys.KeyR, shift: false },
|
||||
s: { hidCode: keys.KeyS, shift: false },
|
||||
t: { hidCode: keys.KeyT, shift: false },
|
||||
u: { hidCode: keys.KeyU, shift: false },
|
||||
v: { hidCode: keys.KeyV, shift: false },
|
||||
w: { hidCode: keys.KeyW, shift: false },
|
||||
x: { hidCode: keys.KeyX, shift: false },
|
||||
y: { hidCode: keys.KeyY, shift: false },
|
||||
z: { hidCode: keys.KeyZ, shift: false },
|
||||
a: { key: keys.KeyA, shift: false },
|
||||
b: { key: keys.KeyB, shift: false },
|
||||
c: { key: keys.KeyC, shift: false },
|
||||
d: { key: keys.KeyD, shift: false },
|
||||
e: { key: keys.KeyE, shift: false },
|
||||
f: { key: keys.KeyF, shift: false },
|
||||
g: { key: keys.KeyG, shift: false },
|
||||
h: { key: keys.KeyH, shift: false },
|
||||
i: { key: keys.KeyI, shift: false },
|
||||
j: { key: keys.KeyJ, shift: false },
|
||||
k: { key: keys.KeyK, shift: false },
|
||||
l: { key: keys.KeyL, shift: false },
|
||||
m: { key: keys.KeyM, shift: false },
|
||||
n: { key: keys.KeyN, shift: false },
|
||||
o: { key: keys.KeyO, shift: false },
|
||||
p: { key: keys.KeyP, shift: false },
|
||||
q: { key: keys.KeyQ, shift: false },
|
||||
r: { key: keys.KeyR, shift: false },
|
||||
s: { key: keys.KeyS, shift: false },
|
||||
t: { key: keys.KeyT, shift: false },
|
||||
u: { key: keys.KeyU, shift: false },
|
||||
v: { key: keys.KeyV, shift: false },
|
||||
w: { key: keys.KeyW, shift: false },
|
||||
x: { key: keys.KeyX, shift: false },
|
||||
y: { key: keys.KeyY, shift: false },
|
||||
z: { key: keys.KeyZ, shift: false },
|
||||
|
||||
// Uppercase letters
|
||||
A: { hidCode: keys.KeyA, shift: true },
|
||||
B: { hidCode: keys.KeyB, shift: true },
|
||||
C: { hidCode: keys.KeyC, shift: true },
|
||||
D: { hidCode: keys.KeyD, shift: true },
|
||||
E: { hidCode: keys.KeyE, shift: true },
|
||||
F: { hidCode: keys.KeyF, shift: true },
|
||||
G: { hidCode: keys.KeyG, shift: true },
|
||||
H: { hidCode: keys.KeyH, shift: true },
|
||||
I: { hidCode: keys.KeyI, shift: true },
|
||||
J: { hidCode: keys.KeyJ, shift: true },
|
||||
K: { hidCode: keys.KeyK, shift: true },
|
||||
L: { hidCode: keys.KeyL, shift: true },
|
||||
M: { hidCode: keys.KeyM, shift: true },
|
||||
N: { hidCode: keys.KeyN, shift: true },
|
||||
O: { hidCode: keys.KeyO, shift: true },
|
||||
P: { hidCode: keys.KeyP, shift: true },
|
||||
Q: { hidCode: keys.KeyQ, shift: true },
|
||||
R: { hidCode: keys.KeyR, shift: true },
|
||||
S: { hidCode: keys.KeyS, shift: true },
|
||||
T: { hidCode: keys.KeyT, shift: true },
|
||||
U: { hidCode: keys.KeyU, shift: true },
|
||||
V: { hidCode: keys.KeyV, shift: true },
|
||||
W: { hidCode: keys.KeyW, shift: true },
|
||||
X: { hidCode: keys.KeyX, shift: true },
|
||||
Y: { hidCode: keys.KeyY, shift: true },
|
||||
Z: { hidCode: keys.KeyZ, shift: true },
|
||||
A: { key: keys.KeyA, shift: true },
|
||||
B: { key: keys.KeyB, shift: true },
|
||||
C: { key: keys.KeyC, shift: true },
|
||||
D: { key: keys.KeyD, shift: true },
|
||||
E: { key: keys.KeyE, shift: true },
|
||||
F: { key: keys.KeyF, shift: true },
|
||||
G: { key: keys.KeyG, shift: true },
|
||||
H: { key: keys.KeyH, shift: true },
|
||||
I: { key: keys.KeyI, shift: true },
|
||||
J: { key: keys.KeyJ, shift: true },
|
||||
K: { key: keys.KeyK, shift: true },
|
||||
L: { key: keys.KeyL, shift: true },
|
||||
M: { key: keys.KeyM, shift: true },
|
||||
N: { key: keys.KeyN, shift: true },
|
||||
O: { key: keys.KeyO, shift: true },
|
||||
P: { key: keys.KeyP, shift: true },
|
||||
Q: { key: keys.KeyQ, shift: true },
|
||||
R: { key: keys.KeyR, shift: true },
|
||||
S: { key: keys.KeyS, shift: true },
|
||||
T: { key: keys.KeyT, shift: true },
|
||||
U: { key: keys.KeyU, shift: true },
|
||||
V: { key: keys.KeyV, shift: true },
|
||||
W: { key: keys.KeyW, shift: true },
|
||||
X: { key: keys.KeyX, shift: true },
|
||||
Y: { key: keys.KeyY, shift: true },
|
||||
Z: { key: keys.KeyZ, shift: true },
|
||||
|
||||
// Number row
|
||||
'0': { hidCode: keys.Digit0, shift: false },
|
||||
'1': { hidCode: keys.Digit1, shift: false },
|
||||
'2': { hidCode: keys.Digit2, shift: false },
|
||||
'3': { hidCode: keys.Digit3, shift: false },
|
||||
'4': { hidCode: keys.Digit4, shift: false },
|
||||
'5': { hidCode: keys.Digit5, shift: false },
|
||||
'6': { hidCode: keys.Digit6, shift: false },
|
||||
'7': { hidCode: keys.Digit7, shift: false },
|
||||
'8': { hidCode: keys.Digit8, shift: false },
|
||||
'9': { hidCode: keys.Digit9, shift: false },
|
||||
'0': { key: keys.Digit0, shift: false },
|
||||
'1': { key: keys.Digit1, shift: false },
|
||||
'2': { key: keys.Digit2, shift: false },
|
||||
'3': { key: keys.Digit3, shift: false },
|
||||
'4': { key: keys.Digit4, shift: false },
|
||||
'5': { key: keys.Digit5, shift: false },
|
||||
'6': { key: keys.Digit6, shift: false },
|
||||
'7': { key: keys.Digit7, shift: false },
|
||||
'8': { key: keys.Digit8, shift: false },
|
||||
'9': { key: keys.Digit9, shift: false },
|
||||
|
||||
// Shifted number row symbols
|
||||
')': { hidCode: keys.Digit0, shift: true },
|
||||
'!': { hidCode: keys.Digit1, shift: true },
|
||||
'@': { hidCode: keys.Digit2, shift: true },
|
||||
'#': { hidCode: keys.Digit3, shift: true },
|
||||
'$': { hidCode: keys.Digit4, shift: true },
|
||||
'%': { hidCode: keys.Digit5, shift: true },
|
||||
'^': { hidCode: keys.Digit6, shift: true },
|
||||
'&': { hidCode: keys.Digit7, shift: true },
|
||||
'*': { hidCode: keys.Digit8, shift: true },
|
||||
'(': { hidCode: keys.Digit9, shift: true },
|
||||
')': { key: keys.Digit0, shift: true },
|
||||
'!': { key: keys.Digit1, shift: true },
|
||||
'@': { key: keys.Digit2, shift: true },
|
||||
'#': { key: keys.Digit3, shift: true },
|
||||
'$': { key: keys.Digit4, shift: true },
|
||||
'%': { key: keys.Digit5, shift: true },
|
||||
'^': { key: keys.Digit6, shift: true },
|
||||
'&': { key: keys.Digit7, shift: true },
|
||||
'*': { key: keys.Digit8, shift: true },
|
||||
'(': { key: keys.Digit9, shift: true },
|
||||
|
||||
// Punctuation and symbols
|
||||
'-': { hidCode: keys.Minus, shift: false },
|
||||
'=': { hidCode: keys.Equal, shift: false },
|
||||
'[': { hidCode: keys.BracketLeft, shift: false },
|
||||
']': { hidCode: keys.BracketRight, shift: false },
|
||||
'\\': { hidCode: keys.Backslash, shift: false },
|
||||
';': { hidCode: keys.Semicolon, shift: false },
|
||||
"'": { hidCode: keys.Quote, shift: false },
|
||||
'`': { hidCode: keys.Backquote, shift: false },
|
||||
',': { hidCode: keys.Comma, shift: false },
|
||||
'.': { hidCode: keys.Period, shift: false },
|
||||
'/': { hidCode: keys.Slash, shift: false },
|
||||
'-': { key: keys.Minus, shift: false },
|
||||
'=': { key: keys.Equal, shift: false },
|
||||
'[': { key: keys.BracketLeft, shift: false },
|
||||
']': { key: keys.BracketRight, shift: false },
|
||||
'\\': { key: keys.Backslash, shift: false },
|
||||
';': { key: keys.Semicolon, shift: false },
|
||||
"'": { key: keys.Quote, shift: false },
|
||||
'`': { key: keys.Backquote, shift: false },
|
||||
',': { key: keys.Comma, shift: false },
|
||||
'.': { key: keys.Period, shift: false },
|
||||
'/': { key: keys.Slash, shift: false },
|
||||
|
||||
// Shifted punctuation and symbols
|
||||
_: { hidCode: keys.Minus, shift: true },
|
||||
'+': { hidCode: keys.Equal, shift: true },
|
||||
'{': { hidCode: keys.BracketLeft, shift: true },
|
||||
'}': { hidCode: keys.BracketRight, shift: true },
|
||||
'|': { hidCode: keys.Backslash, shift: true },
|
||||
':': { hidCode: keys.Semicolon, shift: true },
|
||||
'"': { hidCode: keys.Quote, shift: true },
|
||||
'~': { hidCode: keys.Backquote, shift: true },
|
||||
'<': { hidCode: keys.Comma, shift: true },
|
||||
'>': { hidCode: keys.Period, shift: true },
|
||||
'?': { hidCode: keys.Slash, shift: true },
|
||||
_: { key: keys.Minus, shift: true },
|
||||
'+': { key: keys.Equal, shift: true },
|
||||
'{': { key: keys.BracketLeft, shift: true },
|
||||
'}': { key: keys.BracketRight, shift: true },
|
||||
'|': { key: keys.Backslash, shift: true },
|
||||
':': { key: keys.Semicolon, shift: true },
|
||||
'"': { key: keys.Quote, shift: true },
|
||||
'~': { key: keys.Backquote, shift: true },
|
||||
'<': { key: keys.Comma, shift: true },
|
||||
'>': { key: keys.Period, shift: true },
|
||||
'?': { key: keys.Slash, shift: true },
|
||||
|
||||
// Whitespace and control
|
||||
' ': { hidCode: keys.Space, shift: false },
|
||||
'\t': { hidCode: keys.Tab, shift: false },
|
||||
'\n': { hidCode: keys.Enter, shift: false },
|
||||
'\r': { hidCode: keys.Enter, shift: false },
|
||||
' ': { key: keys.Space, shift: false },
|
||||
'\t': { key: keys.Tab, shift: false },
|
||||
'\n': { key: keys.Enter, shift: false },
|
||||
'\r': { key: keys.Enter, shift: false },
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HID usage code and modifier state for a character
|
||||
* Get canonical key and modifier state for a character
|
||||
* @param char - Single character to convert
|
||||
* @returns CharKeyMapping or null if character is not mappable
|
||||
*/
|
||||
|
||||
@@ -1,77 +1,15 @@
|
||||
// Keyboard layout definitions for virtual keyboard
|
||||
// Virtual keyboard layout data shared by the on-screen 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[][]
|
||||
media: string[] // Media keys row
|
||||
}
|
||||
}
|
||||
|
||||
// OS-specific keyboard layout type
|
||||
export type KeyboardOsType = 'windows' | 'mac' | 'android'
|
||||
|
||||
// Bottom row layouts for different OS
|
||||
export const osBottomRows: Record<KeyboardOsType, string[]> = {
|
||||
// Windows: Ctrl - Win - Alt - Space - Alt - Win - Menu - Ctrl
|
||||
windows: ['ControlLeft', 'MetaLeft', 'AltLeft', 'Space', 'AltRight', 'MetaRight', 'Menu', 'ControlRight'],
|
||||
// Mac: Ctrl - Option - Cmd - Space - Cmd - Option - Ctrl
|
||||
windows: ['ControlLeft', 'MetaLeft', 'AltLeft', 'Space', 'AltRight', 'MetaRight', 'ContextMenu', 'ControlRight'],
|
||||
mac: ['ControlLeft', 'AltLeft', 'MetaLeft', 'Space', 'MetaRight', 'AltRight', 'ControlRight'],
|
||||
// Android: simplified layout
|
||||
android: ['ControlLeft', 'AltLeft', 'Space', 'AltRight', 'ControlRight'],
|
||||
}
|
||||
|
||||
// OS-specific modifier display names
|
||||
export const osModifierLabels: Record<KeyboardOsType, Record<string, string>> = {
|
||||
windows: {
|
||||
ControlLeft: '^Ctrl',
|
||||
ControlRight: 'Ctrl^',
|
||||
MetaLeft: '⊞Win',
|
||||
MetaRight: 'Win⊞',
|
||||
AltLeft: 'Alt',
|
||||
AltRight: 'Alt',
|
||||
AltGr: 'AltGr',
|
||||
Menu: 'Menu',
|
||||
},
|
||||
mac: {
|
||||
ControlLeft: '^Ctrl',
|
||||
ControlRight: 'Ctrl^',
|
||||
MetaLeft: '⌘Cmd',
|
||||
MetaRight: 'Cmd⌘',
|
||||
AltLeft: '⌥Opt',
|
||||
AltRight: 'Opt⌥',
|
||||
AltGr: '⌥Opt',
|
||||
Menu: 'Menu',
|
||||
},
|
||||
android: {
|
||||
ControlLeft: 'Ctrl',
|
||||
ControlRight: 'Ctrl',
|
||||
MetaLeft: 'Meta',
|
||||
MetaRight: 'Meta',
|
||||
AltLeft: 'Alt',
|
||||
AltRight: 'Alt',
|
||||
AltGr: 'Alt',
|
||||
Menu: 'Menu',
|
||||
},
|
||||
}
|
||||
|
||||
// Media keys (Consumer Control)
|
||||
export const mediaKeys = ['PrevTrack', 'PlayPause', 'NextTrack', 'Stop', 'Mute', 'VolumeDown', 'VolumeUp']
|
||||
|
||||
// Media key display names
|
||||
export const mediaKeyLabels: Record<string, string> = {
|
||||
PlayPause: '⏯',
|
||||
Stop: '⏹',
|
||||
@@ -81,158 +19,3 @@ export const mediaKeyLabels: Record<string, string> = {
|
||||
VolumeUp: '🔊',
|
||||
VolumeDown: '🔉',
|
||||
}
|
||||
|
||||
// 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'],
|
||||
],
|
||||
media: ['PrevTrack', 'PlayPause', 'NextTrack', 'Stop', 'Mute', 'VolumeDown', 'VolumeUp'],
|
||||
},
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -1,157 +1,128 @@
|
||||
// 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)
|
||||
import { CanonicalKey } from '@/types/generated'
|
||||
|
||||
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,
|
||||
|
||||
// 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,
|
||||
KeyA: CanonicalKey.KeyA,
|
||||
KeyB: CanonicalKey.KeyB,
|
||||
KeyC: CanonicalKey.KeyC,
|
||||
KeyD: CanonicalKey.KeyD,
|
||||
KeyE: CanonicalKey.KeyE,
|
||||
KeyF: CanonicalKey.KeyF,
|
||||
KeyG: CanonicalKey.KeyG,
|
||||
KeyH: CanonicalKey.KeyH,
|
||||
KeyI: CanonicalKey.KeyI,
|
||||
KeyJ: CanonicalKey.KeyJ,
|
||||
KeyK: CanonicalKey.KeyK,
|
||||
KeyL: CanonicalKey.KeyL,
|
||||
KeyM: CanonicalKey.KeyM,
|
||||
KeyN: CanonicalKey.KeyN,
|
||||
KeyO: CanonicalKey.KeyO,
|
||||
KeyP: CanonicalKey.KeyP,
|
||||
KeyQ: CanonicalKey.KeyQ,
|
||||
KeyR: CanonicalKey.KeyR,
|
||||
KeyS: CanonicalKey.KeyS,
|
||||
KeyT: CanonicalKey.KeyT,
|
||||
KeyU: CanonicalKey.KeyU,
|
||||
KeyV: CanonicalKey.KeyV,
|
||||
KeyW: CanonicalKey.KeyW,
|
||||
KeyX: CanonicalKey.KeyX,
|
||||
KeyY: CanonicalKey.KeyY,
|
||||
KeyZ: CanonicalKey.KeyZ,
|
||||
Digit1: CanonicalKey.Digit1,
|
||||
Digit2: CanonicalKey.Digit2,
|
||||
Digit3: CanonicalKey.Digit3,
|
||||
Digit4: CanonicalKey.Digit4,
|
||||
Digit5: CanonicalKey.Digit5,
|
||||
Digit6: CanonicalKey.Digit6,
|
||||
Digit7: CanonicalKey.Digit7,
|
||||
Digit8: CanonicalKey.Digit8,
|
||||
Digit9: CanonicalKey.Digit9,
|
||||
Digit0: CanonicalKey.Digit0,
|
||||
Enter: CanonicalKey.Enter,
|
||||
Escape: CanonicalKey.Escape,
|
||||
Backspace: CanonicalKey.Backspace,
|
||||
Tab: CanonicalKey.Tab,
|
||||
Space: CanonicalKey.Space,
|
||||
Minus: CanonicalKey.Minus,
|
||||
Equal: CanonicalKey.Equal,
|
||||
BracketLeft: CanonicalKey.BracketLeft,
|
||||
BracketRight: CanonicalKey.BracketRight,
|
||||
Backslash: CanonicalKey.Backslash,
|
||||
Semicolon: CanonicalKey.Semicolon,
|
||||
Quote: CanonicalKey.Quote,
|
||||
Backquote: CanonicalKey.Backquote,
|
||||
Comma: CanonicalKey.Comma,
|
||||
Period: CanonicalKey.Period,
|
||||
Slash: CanonicalKey.Slash,
|
||||
CapsLock: CanonicalKey.CapsLock,
|
||||
F1: CanonicalKey.F1,
|
||||
F2: CanonicalKey.F2,
|
||||
F3: CanonicalKey.F3,
|
||||
F4: CanonicalKey.F4,
|
||||
F5: CanonicalKey.F5,
|
||||
F6: CanonicalKey.F6,
|
||||
F7: CanonicalKey.F7,
|
||||
F8: CanonicalKey.F8,
|
||||
F9: CanonicalKey.F9,
|
||||
F10: CanonicalKey.F10,
|
||||
F11: CanonicalKey.F11,
|
||||
F12: CanonicalKey.F12,
|
||||
PrintScreen: CanonicalKey.PrintScreen,
|
||||
ScrollLock: CanonicalKey.ScrollLock,
|
||||
Pause: CanonicalKey.Pause,
|
||||
Insert: CanonicalKey.Insert,
|
||||
Home: CanonicalKey.Home,
|
||||
PageUp: CanonicalKey.PageUp,
|
||||
Delete: CanonicalKey.Delete,
|
||||
End: CanonicalKey.End,
|
||||
PageDown: CanonicalKey.PageDown,
|
||||
ArrowRight: CanonicalKey.ArrowRight,
|
||||
ArrowLeft: CanonicalKey.ArrowLeft,
|
||||
ArrowDown: CanonicalKey.ArrowDown,
|
||||
ArrowUp: CanonicalKey.ArrowUp,
|
||||
NumLock: CanonicalKey.NumLock,
|
||||
NumpadDivide: CanonicalKey.NumpadDivide,
|
||||
NumpadMultiply: CanonicalKey.NumpadMultiply,
|
||||
NumpadSubtract: CanonicalKey.NumpadSubtract,
|
||||
NumpadAdd: CanonicalKey.NumpadAdd,
|
||||
NumpadEnter: CanonicalKey.NumpadEnter,
|
||||
Numpad1: CanonicalKey.Numpad1,
|
||||
Numpad2: CanonicalKey.Numpad2,
|
||||
Numpad3: CanonicalKey.Numpad3,
|
||||
Numpad4: CanonicalKey.Numpad4,
|
||||
Numpad5: CanonicalKey.Numpad5,
|
||||
Numpad6: CanonicalKey.Numpad6,
|
||||
Numpad7: CanonicalKey.Numpad7,
|
||||
Numpad8: CanonicalKey.Numpad8,
|
||||
Numpad9: CanonicalKey.Numpad9,
|
||||
Numpad0: CanonicalKey.Numpad0,
|
||||
NumpadDecimal: CanonicalKey.NumpadDecimal,
|
||||
IntlBackslash: CanonicalKey.IntlBackslash,
|
||||
ContextMenu: CanonicalKey.ContextMenu,
|
||||
F13: CanonicalKey.F13,
|
||||
F14: CanonicalKey.F14,
|
||||
F15: CanonicalKey.F15,
|
||||
F16: CanonicalKey.F16,
|
||||
F17: CanonicalKey.F17,
|
||||
F18: CanonicalKey.F18,
|
||||
F19: CanonicalKey.F19,
|
||||
F20: CanonicalKey.F20,
|
||||
F21: CanonicalKey.F21,
|
||||
F22: CanonicalKey.F22,
|
||||
F23: CanonicalKey.F23,
|
||||
F24: CanonicalKey.F24,
|
||||
ControlLeft: CanonicalKey.ControlLeft,
|
||||
ShiftLeft: CanonicalKey.ShiftLeft,
|
||||
AltLeft: CanonicalKey.AltLeft,
|
||||
MetaLeft: CanonicalKey.MetaLeft,
|
||||
ControlRight: CanonicalKey.ControlRight,
|
||||
ShiftRight: CanonicalKey.ShiftRight,
|
||||
AltRight: CanonicalKey.AltRight,
|
||||
MetaRight: CanonicalKey.MetaRight,
|
||||
} as const
|
||||
|
||||
export type KeyName = keyof typeof keys
|
||||
|
||||
// Consumer Control Usage codes (for multimedia keys)
|
||||
// These are sent via a separate Consumer Control HID report
|
||||
export const consumerKeys = {
|
||||
PlayPause: 0x00cd,
|
||||
Stop: 0x00b7,
|
||||
@@ -164,69 +135,153 @@ export const consumerKeys = {
|
||||
|
||||
export type ConsumerKeyName = keyof typeof consumerKeys
|
||||
|
||||
// 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
|
||||
export const modifiers: Partial<Record<CanonicalKey, number>> = {
|
||||
[CanonicalKey.ControlLeft]: 0x01,
|
||||
[CanonicalKey.ShiftLeft]: 0x02,
|
||||
[CanonicalKey.AltLeft]: 0x04,
|
||||
[CanonicalKey.MetaLeft]: 0x08,
|
||||
[CanonicalKey.ControlRight]: 0x10,
|
||||
[CanonicalKey.ShiftRight]: 0x20,
|
||||
[CanonicalKey.AltRight]: 0x40,
|
||||
[CanonicalKey.MetaRight]: 0x80,
|
||||
}
|
||||
|
||||
// Update modifier mask when a HID modifier key is pressed/released.
|
||||
export function updateModifierMaskForHidKey(mask: number, hidKey: number, press: boolean): number {
|
||||
const bit = hidKeyToModifierMask[hidKey] ?? 0
|
||||
export const keyToHidUsage = {
|
||||
[CanonicalKey.KeyA]: 0x04,
|
||||
[CanonicalKey.KeyB]: 0x05,
|
||||
[CanonicalKey.KeyC]: 0x06,
|
||||
[CanonicalKey.KeyD]: 0x07,
|
||||
[CanonicalKey.KeyE]: 0x08,
|
||||
[CanonicalKey.KeyF]: 0x09,
|
||||
[CanonicalKey.KeyG]: 0x0a,
|
||||
[CanonicalKey.KeyH]: 0x0b,
|
||||
[CanonicalKey.KeyI]: 0x0c,
|
||||
[CanonicalKey.KeyJ]: 0x0d,
|
||||
[CanonicalKey.KeyK]: 0x0e,
|
||||
[CanonicalKey.KeyL]: 0x0f,
|
||||
[CanonicalKey.KeyM]: 0x10,
|
||||
[CanonicalKey.KeyN]: 0x11,
|
||||
[CanonicalKey.KeyO]: 0x12,
|
||||
[CanonicalKey.KeyP]: 0x13,
|
||||
[CanonicalKey.KeyQ]: 0x14,
|
||||
[CanonicalKey.KeyR]: 0x15,
|
||||
[CanonicalKey.KeyS]: 0x16,
|
||||
[CanonicalKey.KeyT]: 0x17,
|
||||
[CanonicalKey.KeyU]: 0x18,
|
||||
[CanonicalKey.KeyV]: 0x19,
|
||||
[CanonicalKey.KeyW]: 0x1a,
|
||||
[CanonicalKey.KeyX]: 0x1b,
|
||||
[CanonicalKey.KeyY]: 0x1c,
|
||||
[CanonicalKey.KeyZ]: 0x1d,
|
||||
[CanonicalKey.Digit1]: 0x1e,
|
||||
[CanonicalKey.Digit2]: 0x1f,
|
||||
[CanonicalKey.Digit3]: 0x20,
|
||||
[CanonicalKey.Digit4]: 0x21,
|
||||
[CanonicalKey.Digit5]: 0x22,
|
||||
[CanonicalKey.Digit6]: 0x23,
|
||||
[CanonicalKey.Digit7]: 0x24,
|
||||
[CanonicalKey.Digit8]: 0x25,
|
||||
[CanonicalKey.Digit9]: 0x26,
|
||||
[CanonicalKey.Digit0]: 0x27,
|
||||
[CanonicalKey.Enter]: 0x28,
|
||||
[CanonicalKey.Escape]: 0x29,
|
||||
[CanonicalKey.Backspace]: 0x2a,
|
||||
[CanonicalKey.Tab]: 0x2b,
|
||||
[CanonicalKey.Space]: 0x2c,
|
||||
[CanonicalKey.Minus]: 0x2d,
|
||||
[CanonicalKey.Equal]: 0x2e,
|
||||
[CanonicalKey.BracketLeft]: 0x2f,
|
||||
[CanonicalKey.BracketRight]: 0x30,
|
||||
[CanonicalKey.Backslash]: 0x31,
|
||||
[CanonicalKey.Semicolon]: 0x33,
|
||||
[CanonicalKey.Quote]: 0x34,
|
||||
[CanonicalKey.Backquote]: 0x35,
|
||||
[CanonicalKey.Comma]: 0x36,
|
||||
[CanonicalKey.Period]: 0x37,
|
||||
[CanonicalKey.Slash]: 0x38,
|
||||
[CanonicalKey.CapsLock]: 0x39,
|
||||
[CanonicalKey.F1]: 0x3a,
|
||||
[CanonicalKey.F2]: 0x3b,
|
||||
[CanonicalKey.F3]: 0x3c,
|
||||
[CanonicalKey.F4]: 0x3d,
|
||||
[CanonicalKey.F5]: 0x3e,
|
||||
[CanonicalKey.F6]: 0x3f,
|
||||
[CanonicalKey.F7]: 0x40,
|
||||
[CanonicalKey.F8]: 0x41,
|
||||
[CanonicalKey.F9]: 0x42,
|
||||
[CanonicalKey.F10]: 0x43,
|
||||
[CanonicalKey.F11]: 0x44,
|
||||
[CanonicalKey.F12]: 0x45,
|
||||
[CanonicalKey.PrintScreen]: 0x46,
|
||||
[CanonicalKey.ScrollLock]: 0x47,
|
||||
[CanonicalKey.Pause]: 0x48,
|
||||
[CanonicalKey.Insert]: 0x49,
|
||||
[CanonicalKey.Home]: 0x4a,
|
||||
[CanonicalKey.PageUp]: 0x4b,
|
||||
[CanonicalKey.Delete]: 0x4c,
|
||||
[CanonicalKey.End]: 0x4d,
|
||||
[CanonicalKey.PageDown]: 0x4e,
|
||||
[CanonicalKey.ArrowRight]: 0x4f,
|
||||
[CanonicalKey.ArrowLeft]: 0x50,
|
||||
[CanonicalKey.ArrowDown]: 0x51,
|
||||
[CanonicalKey.ArrowUp]: 0x52,
|
||||
[CanonicalKey.NumLock]: 0x53,
|
||||
[CanonicalKey.NumpadDivide]: 0x54,
|
||||
[CanonicalKey.NumpadMultiply]: 0x55,
|
||||
[CanonicalKey.NumpadSubtract]: 0x56,
|
||||
[CanonicalKey.NumpadAdd]: 0x57,
|
||||
[CanonicalKey.NumpadEnter]: 0x58,
|
||||
[CanonicalKey.Numpad1]: 0x59,
|
||||
[CanonicalKey.Numpad2]: 0x5a,
|
||||
[CanonicalKey.Numpad3]: 0x5b,
|
||||
[CanonicalKey.Numpad4]: 0x5c,
|
||||
[CanonicalKey.Numpad5]: 0x5d,
|
||||
[CanonicalKey.Numpad6]: 0x5e,
|
||||
[CanonicalKey.Numpad7]: 0x5f,
|
||||
[CanonicalKey.Numpad8]: 0x60,
|
||||
[CanonicalKey.Numpad9]: 0x61,
|
||||
[CanonicalKey.Numpad0]: 0x62,
|
||||
[CanonicalKey.NumpadDecimal]: 0x63,
|
||||
[CanonicalKey.IntlBackslash]: 0x64,
|
||||
[CanonicalKey.ContextMenu]: 0x65,
|
||||
[CanonicalKey.F13]: 0x68,
|
||||
[CanonicalKey.F14]: 0x69,
|
||||
[CanonicalKey.F15]: 0x6a,
|
||||
[CanonicalKey.F16]: 0x6b,
|
||||
[CanonicalKey.F17]: 0x6c,
|
||||
[CanonicalKey.F18]: 0x6d,
|
||||
[CanonicalKey.F19]: 0x6e,
|
||||
[CanonicalKey.F20]: 0x6f,
|
||||
[CanonicalKey.F21]: 0x70,
|
||||
[CanonicalKey.F22]: 0x71,
|
||||
[CanonicalKey.F23]: 0x72,
|
||||
[CanonicalKey.F24]: 0x73,
|
||||
[CanonicalKey.ControlLeft]: 0xe0,
|
||||
[CanonicalKey.ShiftLeft]: 0xe1,
|
||||
[CanonicalKey.AltLeft]: 0xe2,
|
||||
[CanonicalKey.MetaLeft]: 0xe3,
|
||||
[CanonicalKey.ControlRight]: 0xe4,
|
||||
[CanonicalKey.ShiftRight]: 0xe5,
|
||||
[CanonicalKey.AltRight]: 0xe6,
|
||||
[CanonicalKey.MetaRight]: 0xe7,
|
||||
} as const satisfies Record<CanonicalKey, number>
|
||||
|
||||
export function canonicalKeyToHidUsage(key: CanonicalKey): number {
|
||||
return keyToHidUsage[key]
|
||||
}
|
||||
|
||||
export function updateModifierMaskForKey(mask: number, key: CanonicalKey, press: boolean): number {
|
||||
const bit = modifiers[key] ?? 0
|
||||
if (bit === 0) return mask
|
||||
return press ? (mask | bit) : (mask & ~bit)
|
||||
}
|
||||
|
||||
// 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',
|
||||
export const latchingKeys = [
|
||||
CanonicalKey.CapsLock,
|
||||
CanonicalKey.ScrollLock,
|
||||
CanonicalKey.NumLock,
|
||||
] 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
|
||||
}
|
||||
|
||||
// Normalize browser-specific KeyboardEvent.code variants.
|
||||
export function normalizeKeyboardCode(code: string, key: string): string {
|
||||
if (code === 'IntlBackslash' && (key === '`' || key === '~')) return 'Backquote'
|
||||
@@ -238,18 +293,10 @@ export function normalizeKeyboardCode(code: string, key: string): string {
|
||||
return code
|
||||
}
|
||||
|
||||
// Convert KeyboardEvent.code/key to USB HID usage code.
|
||||
export function keyboardEventToHidCode(code: string, key: string): number | undefined {
|
||||
export function keyboardEventToCanonicalKey(code: string, key: string): CanonicalKey | undefined {
|
||||
const normalizedCode = normalizeKeyboardCode(code, key)
|
||||
return keys[normalizedCode as KeyName]
|
||||
}
|
||||
|
||||
// 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
|
||||
if (normalizedCode in keys) {
|
||||
return keys[normalizedCode as KeyName]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ export enum AtxDriverType {
|
||||
Gpio = "gpio",
|
||||
/** USB HID relay module */
|
||||
UsbRelay = "usbrelay",
|
||||
/** Serial/COM port relay (LCUS type) */
|
||||
/** Serial/COM port relay (taobao LCUS type) */
|
||||
Serial = "serial",
|
||||
/** Disabled / Not configured */
|
||||
None = "none",
|
||||
@@ -149,6 +149,7 @@ export interface AtxKeyConfig {
|
||||
* Pin or channel number:
|
||||
* - For GPIO: GPIO pin number
|
||||
* - For USB Relay: relay channel (0-based)
|
||||
* - For Serial Relay (LCUS): relay channel (1-based)
|
||||
*/
|
||||
pin: number;
|
||||
/** Active level (only applicable to GPIO, ignored for USB Relay) */
|
||||
@@ -444,11 +445,11 @@ export interface AtxConfigUpdate {
|
||||
/** Available ATX devices for discovery */
|
||||
export interface AtxDevices {
|
||||
/** Available GPIO chips (/dev/gpiochip*) */
|
||||
/** Available Serial ports (/dev/ttyUSB*) */
|
||||
serial_ports: string[];
|
||||
gpio_chips: string[];
|
||||
/** Available USB HID relay devices (/dev/hidraw*) */
|
||||
usb_relays: string[];
|
||||
/** Available Serial ports (/dev/ttyUSB*) */
|
||||
serial_ports: string[];
|
||||
}
|
||||
|
||||
export interface AudioConfigUpdate {
|
||||
@@ -623,19 +624,19 @@ export interface RustDeskConfigUpdate {
|
||||
device_password?: string;
|
||||
}
|
||||
|
||||
/** Stream 配置响应(包含 has_turn_password 字段) */
|
||||
/** Stream configuration response (includes has_turn_password) */
|
||||
export interface StreamConfigResponse {
|
||||
mode: StreamMode;
|
||||
encoder: EncoderType;
|
||||
bitrate_preset: BitratePreset;
|
||||
/** 是否有公共 ICE 服务器可用(编译时确定) */
|
||||
/** Whether public ICE servers are available (compile-time decision) */
|
||||
has_public_ice_servers: boolean;
|
||||
/** 当前是否正在使用公共 ICE 服务器(STUN/TURN 都为空时) */
|
||||
/** Whether public ICE servers are currently in use (when STUN/TURN are unset) */
|
||||
using_public_ice_servers: boolean;
|
||||
stun_server?: string;
|
||||
turn_server?: string;
|
||||
turn_username?: string;
|
||||
/** 指示是否已设置 TURN 密码(实际密码不返回) */
|
||||
/** Indicates whether TURN password has been configured (password is not returned) */
|
||||
has_turn_password: boolean;
|
||||
}
|
||||
|
||||
@@ -688,3 +689,130 @@ export interface WebConfigUpdate {
|
||||
bind_address?: string;
|
||||
https_enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared canonical keyboard key identifiers used across frontend and backend.
|
||||
*
|
||||
* The enum names intentionally mirror `KeyboardEvent.code` style values so the
|
||||
* browser, virtual keyboard, and HID backend can all speak the same language.
|
||||
*/
|
||||
export enum CanonicalKey {
|
||||
KeyA = "KeyA",
|
||||
KeyB = "KeyB",
|
||||
KeyC = "KeyC",
|
||||
KeyD = "KeyD",
|
||||
KeyE = "KeyE",
|
||||
KeyF = "KeyF",
|
||||
KeyG = "KeyG",
|
||||
KeyH = "KeyH",
|
||||
KeyI = "KeyI",
|
||||
KeyJ = "KeyJ",
|
||||
KeyK = "KeyK",
|
||||
KeyL = "KeyL",
|
||||
KeyM = "KeyM",
|
||||
KeyN = "KeyN",
|
||||
KeyO = "KeyO",
|
||||
KeyP = "KeyP",
|
||||
KeyQ = "KeyQ",
|
||||
KeyR = "KeyR",
|
||||
KeyS = "KeyS",
|
||||
KeyT = "KeyT",
|
||||
KeyU = "KeyU",
|
||||
KeyV = "KeyV",
|
||||
KeyW = "KeyW",
|
||||
KeyX = "KeyX",
|
||||
KeyY = "KeyY",
|
||||
KeyZ = "KeyZ",
|
||||
Digit1 = "Digit1",
|
||||
Digit2 = "Digit2",
|
||||
Digit3 = "Digit3",
|
||||
Digit4 = "Digit4",
|
||||
Digit5 = "Digit5",
|
||||
Digit6 = "Digit6",
|
||||
Digit7 = "Digit7",
|
||||
Digit8 = "Digit8",
|
||||
Digit9 = "Digit9",
|
||||
Digit0 = "Digit0",
|
||||
Enter = "Enter",
|
||||
Escape = "Escape",
|
||||
Backspace = "Backspace",
|
||||
Tab = "Tab",
|
||||
Space = "Space",
|
||||
Minus = "Minus",
|
||||
Equal = "Equal",
|
||||
BracketLeft = "BracketLeft",
|
||||
BracketRight = "BracketRight",
|
||||
Backslash = "Backslash",
|
||||
Semicolon = "Semicolon",
|
||||
Quote = "Quote",
|
||||
Backquote = "Backquote",
|
||||
Comma = "Comma",
|
||||
Period = "Period",
|
||||
Slash = "Slash",
|
||||
CapsLock = "CapsLock",
|
||||
F1 = "F1",
|
||||
F2 = "F2",
|
||||
F3 = "F3",
|
||||
F4 = "F4",
|
||||
F5 = "F5",
|
||||
F6 = "F6",
|
||||
F7 = "F7",
|
||||
F8 = "F8",
|
||||
F9 = "F9",
|
||||
F10 = "F10",
|
||||
F11 = "F11",
|
||||
F12 = "F12",
|
||||
PrintScreen = "PrintScreen",
|
||||
ScrollLock = "ScrollLock",
|
||||
Pause = "Pause",
|
||||
Insert = "Insert",
|
||||
Home = "Home",
|
||||
PageUp = "PageUp",
|
||||
Delete = "Delete",
|
||||
End = "End",
|
||||
PageDown = "PageDown",
|
||||
ArrowRight = "ArrowRight",
|
||||
ArrowLeft = "ArrowLeft",
|
||||
ArrowDown = "ArrowDown",
|
||||
ArrowUp = "ArrowUp",
|
||||
NumLock = "NumLock",
|
||||
NumpadDivide = "NumpadDivide",
|
||||
NumpadMultiply = "NumpadMultiply",
|
||||
NumpadSubtract = "NumpadSubtract",
|
||||
NumpadAdd = "NumpadAdd",
|
||||
NumpadEnter = "NumpadEnter",
|
||||
Numpad1 = "Numpad1",
|
||||
Numpad2 = "Numpad2",
|
||||
Numpad3 = "Numpad3",
|
||||
Numpad4 = "Numpad4",
|
||||
Numpad5 = "Numpad5",
|
||||
Numpad6 = "Numpad6",
|
||||
Numpad7 = "Numpad7",
|
||||
Numpad8 = "Numpad8",
|
||||
Numpad9 = "Numpad9",
|
||||
Numpad0 = "Numpad0",
|
||||
NumpadDecimal = "NumpadDecimal",
|
||||
IntlBackslash = "IntlBackslash",
|
||||
ContextMenu = "ContextMenu",
|
||||
F13 = "F13",
|
||||
F14 = "F14",
|
||||
F15 = "F15",
|
||||
F16 = "F16",
|
||||
F17 = "F17",
|
||||
F18 = "F18",
|
||||
F19 = "F19",
|
||||
F20 = "F20",
|
||||
F21 = "F21",
|
||||
F22 = "F22",
|
||||
F23 = "F23",
|
||||
F24 = "F24",
|
||||
ControlLeft = "ControlLeft",
|
||||
ShiftLeft = "ShiftLeft",
|
||||
AltLeft = "AltLeft",
|
||||
MetaLeft = "MetaLeft",
|
||||
ControlRight = "ControlRight",
|
||||
ShiftRight = "ShiftRight",
|
||||
AltRight = "AltRight",
|
||||
MetaRight = "MetaRight",
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// HID (Human Interface Device) type definitions
|
||||
// Shared between WebRTC DataChannel and WebSocket HID channels
|
||||
|
||||
import { type CanonicalKey } from '@/types/generated'
|
||||
import { canonicalKeyToHidUsage } from '@/lib/keyboardMappings'
|
||||
|
||||
/** Keyboard event for HID input */
|
||||
export interface HidKeyboardEvent {
|
||||
type: 'keydown' | 'keyup'
|
||||
key: number
|
||||
key: CanonicalKey
|
||||
/** Raw HID modifier byte (bit0..bit7 = LCtrl..RMeta) */
|
||||
modifier?: number
|
||||
}
|
||||
@@ -51,7 +54,7 @@ export function encodeKeyboardEvent(event: HidKeyboardEvent): ArrayBuffer {
|
||||
|
||||
view.setUint8(0, MSG_KEYBOARD)
|
||||
view.setUint8(1, event.type === 'keydown' ? KB_EVENT_DOWN : KB_EVENT_UP)
|
||||
view.setUint8(2, event.key & 0xff)
|
||||
view.setUint8(2, canonicalKeyToHidUsage(event.key) & 0xff)
|
||||
|
||||
view.setUint8(3, (event.modifier ?? 0) & 0xff)
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ import { useWebRTC } from '@/composables/useWebRTC'
|
||||
import { useVideoSession } from '@/composables/useVideoSession'
|
||||
import { getUnifiedAudio } from '@/composables/useUnifiedAudio'
|
||||
import { streamApi, hidApi, atxApi, extensionsApi, atxConfigApi, authApi } from '@/api'
|
||||
import { CanonicalKey } from '@/types/generated'
|
||||
import type { HidKeyboardEvent, HidMouseEvent } from '@/types/hid'
|
||||
import { keyboardEventToHidCode, updateModifierMaskForHidKey } from '@/lib/keyboardMappings'
|
||||
import { keyboardEventToCanonicalKey, updateModifierMaskForKey } from '@/lib/keyboardMappings'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { generateUUID } from '@/lib/utils'
|
||||
import type { VideoMode } from '@/components/VideoConfigPopover.vue'
|
||||
@@ -117,7 +118,7 @@ const myClientId = generateUUID()
|
||||
|
||||
// HID state
|
||||
const mouseMode = ref<'absolute' | 'relative'>('absolute')
|
||||
const pressedKeys = ref<string[]>([])
|
||||
const pressedKeys = ref<CanonicalKey[]>([])
|
||||
const keyboardLed = ref({
|
||||
capsLock: false,
|
||||
})
|
||||
@@ -1539,7 +1540,7 @@ function handleHidError(_error: any, _operation: string) {
|
||||
}
|
||||
|
||||
// HID channel selection: use WebRTC DataChannel when available, fallback to WebSocket
|
||||
function sendKeyboardEvent(type: 'down' | 'up', key: number, modifier?: number) {
|
||||
function sendKeyboardEvent(type: 'down' | 'up', key: CanonicalKey, modifier?: number) {
|
||||
// In WebRTC mode with DataChannel ready, use DataChannel for lower latency
|
||||
if (videoMode.value !== 'mjpeg' && webrtc.dataChannelReady.value) {
|
||||
const event: HidKeyboardEvent = {
|
||||
@@ -1618,22 +1619,21 @@ function handleKeyDown(e: KeyboardEvent) {
|
||||
})
|
||||
}
|
||||
|
||||
const keyName = e.key === ' ' ? 'Space' : e.key
|
||||
if (!pressedKeys.value.includes(keyName)) {
|
||||
pressedKeys.value = [...pressedKeys.value, keyName]
|
||||
}
|
||||
|
||||
keyboardLed.value.capsLock = e.getModifierState('CapsLock')
|
||||
|
||||
const hidKey = keyboardEventToHidCode(e.code, e.key)
|
||||
if (hidKey === undefined) {
|
||||
const canonicalKey = keyboardEventToCanonicalKey(e.code, e.key)
|
||||
if (canonicalKey === undefined) {
|
||||
console.warn(`[HID] Unmapped key down: code=${e.code}, key=${e.key}`)
|
||||
return
|
||||
}
|
||||
|
||||
const modifierMask = updateModifierMaskForHidKey(activeModifierMask.value, hidKey, true)
|
||||
if (!pressedKeys.value.includes(canonicalKey)) {
|
||||
pressedKeys.value = [...pressedKeys.value, canonicalKey]
|
||||
}
|
||||
|
||||
const modifierMask = updateModifierMaskForKey(activeModifierMask.value, canonicalKey, true)
|
||||
activeModifierMask.value = modifierMask
|
||||
sendKeyboardEvent('down', hidKey, modifierMask)
|
||||
sendKeyboardEvent('down', canonicalKey, modifierMask)
|
||||
}
|
||||
|
||||
function handleKeyUp(e: KeyboardEvent) {
|
||||
@@ -1649,18 +1649,17 @@ function handleKeyUp(e: KeyboardEvent) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const keyName = e.key === ' ' ? 'Space' : e.key
|
||||
pressedKeys.value = pressedKeys.value.filter(k => k !== keyName)
|
||||
|
||||
const hidKey = keyboardEventToHidCode(e.code, e.key)
|
||||
if (hidKey === undefined) {
|
||||
const canonicalKey = keyboardEventToCanonicalKey(e.code, e.key)
|
||||
if (canonicalKey === undefined) {
|
||||
console.warn(`[HID] Unmapped key up: code=${e.code}, key=${e.key}`)
|
||||
return
|
||||
}
|
||||
|
||||
const modifierMask = updateModifierMaskForHidKey(activeModifierMask.value, hidKey, false)
|
||||
pressedKeys.value = pressedKeys.value.filter(k => k !== canonicalKey)
|
||||
|
||||
const modifierMask = updateModifierMaskForKey(activeModifierMask.value, canonicalKey, false)
|
||||
activeModifierMask.value = modifierMask
|
||||
sendKeyboardEvent('up', hidKey, modifierMask)
|
||||
sendKeyboardEvent('up', canonicalKey, modifierMask)
|
||||
}
|
||||
|
||||
function getActiveVideoElement(): HTMLImageElement | HTMLVideoElement | null {
|
||||
@@ -2016,18 +2015,18 @@ function handleToggleVirtualKeyboard() {
|
||||
}
|
||||
|
||||
// Virtual keyboard key event handlers
|
||||
function handleVirtualKeyDown(key: string) {
|
||||
function handleVirtualKeyDown(key: CanonicalKey) {
|
||||
// Add to pressedKeys for InfoBar display
|
||||
if (!pressedKeys.value.includes(key)) {
|
||||
pressedKeys.value = [...pressedKeys.value, key]
|
||||
}
|
||||
// Toggle CapsLock state when virtual keyboard presses CapsLock
|
||||
if (key === 'CapsLock') {
|
||||
if (key === CanonicalKey.CapsLock) {
|
||||
keyboardLed.value.capsLock = !keyboardLed.value.capsLock
|
||||
}
|
||||
}
|
||||
|
||||
function handleVirtualKeyUp(key: string) {
|
||||
function handleVirtualKeyUp(key: CanonicalKey) {
|
||||
// Remove from pressedKeys
|
||||
pressedKeys.value = pressedKeys.value.filter(k => k !== key)
|
||||
}
|
||||
@@ -2485,6 +2484,8 @@ onUnmounted(() => {
|
||||
v-if="virtualKeyboardVisible"
|
||||
v-model:visible="virtualKeyboardVisible"
|
||||
v-model:attached="virtualKeyboardAttached"
|
||||
:caps-lock="keyboardLed.capsLock"
|
||||
:pressed-keys="pressedKeys"
|
||||
@key-down="handleVirtualKeyDown"
|
||||
@key-up="handleVirtualKeyUp"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user