mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-28 16:30:48 +08:00
feat(hid): 添加 Linux 键盘与 SysRq 快捷键 (#307)
控制台增加独立快捷键菜单,提供常用组合键和 Linux SysRq 指令,虚拟键盘补充 Linux 布局。 通过可靠且有序的 WebSocket 发送组合键,取消或失败时逐一释放按键,防止修饰键残留。 执行期间避免物理键盘与虚拟键盘交错输入,断连、失焦和切换布局时停止执行。 修正焦点移动后的物理按键释放,并将原虚拟键盘组合键迁移到统一菜单。 验证:独立暂存快照前端构建通过;6 项快捷键单测及 5 组浏览器测试通过,覆盖组合键序列、失败清理、取消及键盘焦点。 Refs #307
This commit is contained in:
108
web/src/lib/keyboardShortcuts.ts
Normal file
108
web/src/lib/keyboardShortcuts.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { CanonicalKey as Key } from '@/types/generated'
|
||||
import { updateModifierMaskForKey } from '@/lib/keyboardMappings'
|
||||
|
||||
export interface KeyboardShortcut {
|
||||
id: string
|
||||
label: string
|
||||
description: string
|
||||
keys: Key[]
|
||||
destructive?: boolean
|
||||
}
|
||||
|
||||
function combination(id: string, label: string, keys: Key[]): KeyboardShortcut {
|
||||
return { id, label, description: id, keys }
|
||||
}
|
||||
|
||||
export const keyboardShortcuts: KeyboardShortcut[] = [
|
||||
combination('ctrl-alt-delete', 'Ctrl+Alt+Del', [Key.ControlLeft, Key.AltLeft, Key.Delete]),
|
||||
combination('task-manager', 'Ctrl+Shift+Esc', [Key.ControlLeft, Key.ShiftLeft, Key.Escape]),
|
||||
combination('switch-window', 'Alt+Tab', [Key.AltLeft, Key.Tab]),
|
||||
combination('close-window', 'Alt+F4', [Key.AltLeft, Key.F4]),
|
||||
combination('lock-windows', 'Win+L', [Key.MetaLeft, Key.KeyL]),
|
||||
combination('run', 'Win+R', [Key.MetaLeft, Key.KeyR]),
|
||||
combination('tty1', 'Ctrl+Alt+F1', [Key.ControlLeft, Key.AltLeft, Key.F1]),
|
||||
combination('tty2', 'Ctrl+Alt+F2', [Key.ControlLeft, Key.AltLeft, Key.F2]),
|
||||
combination('restart-x', 'Ctrl+Alt+Bksp', [Key.ControlLeft, Key.AltLeft, Key.Backspace]),
|
||||
combination('force-quit', 'Cmd+Opt+Esc', [Key.MetaLeft, Key.AltLeft, Key.Escape]),
|
||||
]
|
||||
|
||||
function sysrq(letter: string, key: Key, destructive = false): KeyboardShortcut {
|
||||
return {
|
||||
id: `sysrq-${letter.toLowerCase()}`, label: letter,
|
||||
description: `sysrq-${letter.toLowerCase()}`, destructive,
|
||||
keys: [Key.AltLeft, Key.PrintScreen, key],
|
||||
}
|
||||
}
|
||||
|
||||
export const sysrqDiagnostics = [
|
||||
sysrq('H', Key.KeyH), sysrq('F', Key.KeyF, true),
|
||||
sysrq('M', Key.KeyM), sysrq('T', Key.KeyT),
|
||||
]
|
||||
export const sysrqRecovery = [
|
||||
sysrq('R', Key.KeyR), sysrq('E', Key.KeyE, true), sysrq('I', Key.KeyI, true),
|
||||
sysrq('S', Key.KeyS), sysrq('U', Key.KeyU), sysrq('B', Key.KeyB, true),
|
||||
]
|
||||
|
||||
export type ShortcutSender = (type: 'down' | 'up', key: Key, modifiers: number) => Promise<void>
|
||||
|
||||
function checkCancelled(signal?: AbortSignal) {
|
||||
if (signal?.aborted) throw signal.reason ?? new DOMException('Cancelled', 'AbortError')
|
||||
}
|
||||
|
||||
function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
||||
checkCancelled(signal)
|
||||
return new Promise((resolve, reject) => {
|
||||
const abort = () => {
|
||||
clearTimeout(timer)
|
||||
reject(signal?.reason ?? new DOMException('Cancelled', 'AbortError'))
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
signal?.removeEventListener('abort', abort)
|
||||
resolve()
|
||||
}, ms)
|
||||
signal?.addEventListener('abort', abort, { once: true })
|
||||
})
|
||||
}
|
||||
|
||||
/** Send on one ordered transport; always attempt to release every attempted key. */
|
||||
export async function executeKeyboardShortcut(
|
||||
shortcut: KeyboardShortcut,
|
||||
send: ShortcutSender,
|
||||
options: {
|
||||
signal?: AbortSignal
|
||||
wait?: typeof delay
|
||||
} = {},
|
||||
) {
|
||||
const wait = options.wait ?? delay
|
||||
checkCancelled(options.signal)
|
||||
const held: Key[] = []
|
||||
let mask = 0
|
||||
let failure: unknown
|
||||
try {
|
||||
for (const key of shortcut.keys) {
|
||||
checkCancelled(options.signal)
|
||||
// A failed send may still have reached the device, so track it first.
|
||||
held.push(key)
|
||||
mask = updateModifierMaskForKey(mask, key, true)
|
||||
await send('down', key, mask)
|
||||
await wait(40, options.signal)
|
||||
}
|
||||
await wait(100, options.signal)
|
||||
} catch (error) {
|
||||
failure = error
|
||||
} finally {
|
||||
// Cleanup must not inherit cancellation, and one failed release must not
|
||||
// prevent the remaining modifiers from being released.
|
||||
for (const key of held.reverse()) {
|
||||
mask = updateModifierMaskForKey(mask, key, false)
|
||||
try {
|
||||
await send('up', key, mask)
|
||||
await wait(20)
|
||||
} catch (error) {
|
||||
failure ??= error
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failure) throw failure
|
||||
checkCancelled(options.signal)
|
||||
}
|
||||
Reference in New Issue
Block a user