diff --git a/crates/ericrfb-frontend/src/hotkey.ts b/crates/ericrfb-frontend/src/hotkey.ts new file mode 100644 index 0000000..c24ca0d --- /dev/null +++ b/crates/ericrfb-frontend/src/hotkey.ts @@ -0,0 +1,92 @@ +// Parse Belkin hotkey syntax and convert to scancode sequences. +// +// Syntax: [confirm] ((+|->|>))* +// + = press simultaneously (hold first, press second) +// > = press sequentially (release first, then press second) +// -> = same as > +// +// Examples: +// PrintScreen>0>1 → tap PrintScreen, tap 0, tap 1 +// Ctrl+Alt+Del → hold Ctrl+Alt, tap Del, release all +// PrintScreen>1>7 → tap PrintScreen, tap 1, tap 7 + +import { makeKeyPress, makeKeyRelease } from './protocol' + +// Belkin key names → e-RIC scancodes (from KeyTranslator.java) +const KEY_NAMES: Record = { + // Letters + A: 29, B: 47, C: 45, D: 31, E: 17, F: 32, G: 33, H: 34, I: 22, + J: 35, K: 36, L: 37, M: 49, N: 48, O: 23, P: 24, Q: 15, R: 18, + S: 30, T: 19, U: 21, V: 46, W: 16, X: 44, Y: 20, Z: 43, + + // Digits + '0': 10, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, + + // Modifiers + Ctrl: 54, Alt: 55, Shift: 41, Win: 105, + CtrlLeft: 54, CtrlRight: 58, AltLeft: 55, AltRight: 57, + ShiftLeft: 41, ShiftRight: 53, + + // Special keys + Esc: 59, Escape: 59, Tab: 14, CapsLock: 28, Space: 56, + Enter: 27, Return: 27, Backspace: 13, Del: 78, Delete: 78, + Insert: 75, Home: 76, End: 79, PageUp: 77, PageDown: 80, + PrintScreen: 72, Print: 72, SysRq: 72, + ScrollLock: 73, Pause: 74, Break: 74, + NumLock: 85, + + // Arrow keys + Up: 81, Down: 83, Left: 82, Right: 84, + + // F-keys + F1: 60, F2: 61, F3: 62, F4: 63, F5: 64, F6: 65, + F7: 66, F8: 67, F9: 68, F10: 69, F11: 70, F12: 71, + + // Numpad + NumPlus: 89, 'Num+': 89, NumMinus: 99, 'Num-': 99, + NumMultiply: 94, 'Num*': 94, NumDivide: 90, 'Num/': 90, + NumEnter: 98, +} + +/** + * Parse a Belkin hotkey string into a sequence of WS messages to send. + * Returns an array of ArrayBuffer messages (press/release pairs). + */ +export function parseHotkey(hotkey: string): ArrayBuffer[] { + if (!hotkey.trim()) return [] + + const messages: ArrayBuffer[] = [] + // Split on > (sequential) first, preserving + groups + const groups = hotkey.split(/->|>/).map(g => g.trim()).filter(Boolean) + + for (const group of groups) { + // Each group may contain + (simultaneous keys) + const keys = group.split('+').map(k => k.trim()).filter(Boolean) + const scancodes: number[] = [] + + for (const key of keys) { + const sc = KEY_NAMES[key] + if (sc !== undefined) { + scancodes.push(sc) + } + } + + if (scancodes.length === 0) continue + + if (scancodes.length === 1) { + // Single key: press then release + messages.push(makeKeyPress(scancodes[0])) + messages.push(makeKeyRelease(scancodes[0])) + } else { + // Combo: press all in order, release in reverse + for (const sc of scancodes) { + messages.push(makeKeyPress(sc)) + } + for (const sc of scancodes.reverse()) { + messages.push(makeKeyRelease(sc)) + } + } + } + + return messages +} diff --git a/crates/ericrfb-frontend/src/pages/console.ts b/crates/ericrfb-frontend/src/pages/console.ts index 870d924..cc28a18 100644 --- a/crates/ericrfb-frontend/src/pages/console.ts +++ b/crates/ericrfb-frontend/src/pages/console.ts @@ -1,4 +1,5 @@ import { codeToScancode } from '../input' +import { parseHotkey } from '../hotkey' import { TAG_BLIT, TAG_RESIZE, makeKeyPress, makeKeyRelease, makePointer, makeCtrlAltDel, @@ -153,6 +154,8 @@ function handleMessage(ev: MessageEvent) { // Port switcher // --------------------------------------------------------------------------- +let portHotkeys: Record = {} + async function loadPortList() { try { const resp = await fetch('/api/kvm/ports') @@ -161,26 +164,39 @@ async function loadPortList() { const select = document.getElementById('port-select') as HTMLSelectElement if (!select) return select.innerHTML = '' + portHotkeys = {} for (const p of data.ports) { const opt = document.createElement('option') opt.value = String(p.index) opt.textContent = p.name || `Port ${p.index + 1}` opt.selected = p.index === data.active_port select.appendChild(opt) + if (p.hotkey) portHotkeys[p.index] = p.hotkey } - select.addEventListener('change', async () => { + select.addEventListener('change', () => { const port = parseInt(select.value) - await fetch('/api/kvm/switch', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ port }), - }) - // Reconnect to pick up new video stream - ws?.close() + switchToPort(port) }) } catch { /* port list optional */ } } +function switchToPort(port: number) { + const hotkey = portHotkeys[port] + if (hotkey && ws?.readyState === WebSocket.OPEN) { + // Send the hotkey sequence as key events over WS to the KVM + const messages = parseHotkey(hotkey) + for (const msg of messages) { + ws.send(msg) + } + } + // Also update the Belkin's active port tracking + fetch('/api/kvm/switch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ port }), + }).catch(() => {}) +} + // --------------------------------------------------------------------------- // Input handlers // --------------------------------------------------------------------------- diff --git a/crates/ericrfb-proxy/src/kvm.rs b/crates/ericrfb-proxy/src/kvm.rs index 05d886a..9313fdb 100644 --- a/crates/ericrfb-proxy/src/kvm.rs +++ b/crates/ericrfb-proxy/src/kvm.rs @@ -83,7 +83,14 @@ fn extract_input_value(html: &str, name: &str) -> Option { let val_needle = "value=\""; let val_pos = after.find(val_needle)? + val_needle.len(); let end = after[val_pos..].find('"')? + val_pos; - Some(after[val_pos..end].to_string()) + Some(html_unescape(&after[val_pos..end])) +} + +fn html_unescape(s: &str) -> String { + s.replace(">", ">") + .replace("<", "<") + .replace("&", "&") + .replace(""", "\"") } fn extract_selected_option(html: &str, name: &str) -> Option {