feat: Send Key dropdown for browser-intercepted keys
Some checks failed
CI / fmt (push) Failing after 40s
Publish / frontend (push) Successful in 44s
CI / check (push) Successful in 1m21s
CI / clippy (push) Successful in 1m45s
Publish / backend (push) Successful in 2m51s

Adds a "Send Key" dropdown menu to the console toolbar for keys that
browsers intercept before JavaScript can capture them (Print Screen,
Scroll Lock, Pause/Break). Also includes Escape, Tab, Caps Lock,
Num Lock, and Ctrl+Alt+Del.

Each menu item sends a press+release scancode pair directly over the
WebSocket, bypassing browser key capture entirely. This enables
activating downstream KVM switch menus (e.g., Avocent OSCAR via
Print Screen) from the blekin interface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 11:32:10 +03:00
parent ea18d97aa6
commit 63aa9a400f
2 changed files with 85 additions and 4 deletions

View File

@@ -20,7 +20,21 @@ export function mountConsole(el: HTMLElement, s: SessionInfo) {
<div class="toolbar">
<span>${s.boardName}</span>
<select id="port-select" title="Switch KVM port"></select>
<button id="btn-cad">Ctrl+Alt+Del</button>
<div class="sendkey-wrap">
<button id="btn-sendkey">Send Key &#x25BE;</button>
<div class="sendkey-menu" id="sendkey-menu" hidden>
<button data-sc="72">Print Screen</button>
<button data-sc="73">Scroll Lock</button>
<button data-sc="74">Pause / Break</button>
<hr />
<button data-sc="59">Escape</button>
<button data-sc="14">Tab</button>
<button data-sc="28">Caps Lock</button>
<button data-sc="85">Num Lock</button>
<hr />
<button data-cad="1">Ctrl + Alt + Del</button>
</div>
</div>
<button id="btn-fs">Fullscreen</button>
<span class="status" id="status">connecting...</span>
</div>
@@ -228,11 +242,40 @@ function wireInputHandlers() {
}
function wireToolbar() {
document.getElementById('btn-cad')?.addEventListener('click', () => {
if (ws?.readyState === WebSocket.OPEN) ws.send(makeCtrlAltDel())
document.getElementById('canvas')?.focus()
// Send Key dropdown
const sendKeyBtn = document.getElementById('btn-sendkey')!
const sendKeyMenu = document.getElementById('sendkey-menu')!
sendKeyBtn.addEventListener('click', (e) => {
e.stopPropagation()
sendKeyMenu.hidden = !sendKeyMenu.hidden
})
// Close menu on outside click
document.addEventListener('click', () => { sendKeyMenu.hidden = true })
sendKeyMenu.addEventListener('click', (e) => e.stopPropagation())
sendKeyMenu.querySelectorAll('button[data-sc]').forEach(btn => {
btn.addEventListener('click', () => {
const sc = parseInt((btn as HTMLElement).dataset.sc!)
if (ws?.readyState === WebSocket.OPEN) {
ws.send(makeKeyPress(sc))
ws.send(makeKeyRelease(sc))
}
sendKeyMenu.hidden = true
document.getElementById('canvas')?.focus()
})
})
sendKeyMenu.querySelectorAll('button[data-cad]').forEach(btn => {
btn.addEventListener('click', () => {
if (ws?.readyState === WebSocket.OPEN) ws.send(makeCtrlAltDel())
sendKeyMenu.hidden = true
document.getElementById('canvas')?.focus()
})
})
// Fullscreen
document.getElementById('btn-fs')?.addEventListener('click', () => {
const shell = document.querySelector('.shell')
if (document.fullscreenElement) document.exitFullscreen()

View File

@@ -126,6 +126,44 @@ body {
.toolbar .status { margin-left: auto; color: #888; }
.toolbar .status.connected { color: #4a7c59; }
/* Send Key dropdown */
.sendkey-wrap {
position: relative;
}
.sendkey-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 100;
background: #2a2a2a;
border: 1px solid #444;
border-radius: 4px;
padding: 0.25rem 0;
min-width: 160px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.sendkey-menu button {
display: block;
width: 100%;
padding: 0.35rem 0.75rem;
border: none;
background: none;
color: #ccc;
text-align: left;
cursor: pointer;
font-size: 0.8rem;
}
.sendkey-menu button:hover { background: #383838; }
.sendkey-menu hr {
border: none;
border-top: 1px solid #3a3a3a;
margin: 0.2rem 0;
}
/* Console canvas */
.console-wrap {
flex: 1;