feat: suspend input during port switch with overlay and countdown
When switching ports via the console dropdown: - Input (keyboard, mouse, wheel) is suspended immediately to prevent stray events from interfering with the OSCAR hotkey sequence - A semi-transparent overlay with spinner and countdown timer appears over the console canvas - Duration is calculated from the actual key_pause_duration setting multiplied by the number of * pause tokens in the port's hotkey, plus a 500ms buffer - Input resumes and overlay disappears when the timer expires Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -155,12 +155,15 @@ function handleMessage(ev: MessageEvent) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
let portHotkeys: Record<number, string> = {}
|
let portHotkeys: Record<number, string> = {}
|
||||||
|
let keyPauseDuration = 2000
|
||||||
|
let inputSuspended = false
|
||||||
|
|
||||||
async function loadPortList() {
|
async function loadPortList() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/kvm/ports')
|
const resp = await fetch('/api/kvm/ports')
|
||||||
if (!resp.ok) return
|
if (!resp.ok) return
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
|
keyPauseDuration = data.key_pause_duration || 2000
|
||||||
const select = document.getElementById('port-select') as HTMLSelectElement
|
const select = document.getElementById('port-select') as HTMLSelectElement
|
||||||
if (!select) return
|
if (!select) return
|
||||||
select.innerHTML = ''
|
select.innerHTML = ''
|
||||||
@@ -182,19 +185,69 @@ async function loadPortList() {
|
|||||||
|
|
||||||
function switchToPort(port: number) {
|
function switchToPort(port: number) {
|
||||||
const hotkey = portHotkeys[port]
|
const hotkey = portHotkeys[port]
|
||||||
if (hotkey && ws?.readyState === WebSocket.OPEN) {
|
if (!hotkey || ws?.readyState !== WebSocket.OPEN) return
|
||||||
// Send the hotkey sequence as key events over WS to the KVM
|
|
||||||
const messages = parseHotkey(hotkey)
|
// Count pause tokens to calculate total switch duration
|
||||||
for (const msg of messages) {
|
const pauseCount = (hotkey.match(/\*/g) || []).length
|
||||||
ws.send(msg)
|
const totalDelay = pauseCount * keyPauseDuration + 500 // +500ms buffer
|
||||||
}
|
|
||||||
|
// Suspend input and show overlay
|
||||||
|
inputSuspended = true
|
||||||
|
showSwitchOverlay(totalDelay)
|
||||||
|
|
||||||
|
// Send the hotkey sequence
|
||||||
|
const messages = parseHotkey(hotkey)
|
||||||
|
for (const msg of messages) {
|
||||||
|
ws.send(msg)
|
||||||
}
|
}
|
||||||
// Also update the Belkin's active port tracking
|
|
||||||
|
// Update Belkin's active port tracking
|
||||||
fetch('/api/kvm/switch', {
|
fetch('/api/kvm/switch', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ port }),
|
body: JSON.stringify({ port }),
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
|
|
||||||
|
// Resume input after the switch completes
|
||||||
|
setTimeout(() => {
|
||||||
|
inputSuspended = false
|
||||||
|
hideSwitchOverlay()
|
||||||
|
document.getElementById('canvas')?.focus()
|
||||||
|
}, totalDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSwitchOverlay(duration: number) {
|
||||||
|
let overlay = document.getElementById('switch-overlay')
|
||||||
|
if (!overlay) {
|
||||||
|
overlay = document.createElement('div')
|
||||||
|
overlay.id = 'switch-overlay'
|
||||||
|
overlay.className = 'switch-overlay'
|
||||||
|
document.querySelector('.console-wrap')?.appendChild(overlay)
|
||||||
|
}
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="switch-overlay-content">
|
||||||
|
<div class="switch-spinner"></div>
|
||||||
|
<div>Switching port...</div>
|
||||||
|
<div class="switch-timer" id="switch-timer"></div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
overlay.hidden = false
|
||||||
|
|
||||||
|
// Countdown timer
|
||||||
|
const start = Date.now()
|
||||||
|
const timerEl = document.getElementById('switch-timer')
|
||||||
|
const tick = () => {
|
||||||
|
if (!timerEl || overlay!.hidden) return
|
||||||
|
const remaining = Math.max(0, duration - (Date.now() - start))
|
||||||
|
timerEl.textContent = `${(remaining / 1000).toFixed(1)}s`
|
||||||
|
if (remaining > 0) requestAnimationFrame(tick)
|
||||||
|
}
|
||||||
|
tick()
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSwitchOverlay() {
|
||||||
|
const overlay = document.getElementById('switch-overlay')
|
||||||
|
if (overlay) overlay.hidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -206,18 +259,20 @@ function wireInputHandlers() {
|
|||||||
|
|
||||||
canvas.addEventListener('keydown', (e) => {
|
canvas.addEventListener('keydown', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
if (inputSuspended) return
|
||||||
const sc = codeToScancode(e.code)
|
const sc = codeToScancode(e.code)
|
||||||
if (sc !== undefined && ws?.readyState === WebSocket.OPEN) ws.send(makeKeyPress(sc))
|
if (sc !== undefined && ws?.readyState === WebSocket.OPEN) ws.send(makeKeyPress(sc))
|
||||||
})
|
})
|
||||||
|
|
||||||
canvas.addEventListener('keyup', (e) => {
|
canvas.addEventListener('keyup', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
if (inputSuspended) return
|
||||||
const sc = codeToScancode(e.code)
|
const sc = codeToScancode(e.code)
|
||||||
if (sc !== undefined && ws?.readyState === WebSocket.OPEN) ws.send(makeKeyRelease(sc))
|
if (sc !== undefined && ws?.readyState === WebSocket.OPEN) ws.send(makeKeyRelease(sc))
|
||||||
})
|
})
|
||||||
|
|
||||||
function sendPointer(e: MouseEvent) {
|
function sendPointer(e: MouseEvent) {
|
||||||
if (ws?.readyState !== WebSocket.OPEN) return
|
if (inputSuspended || ws?.readyState !== WebSocket.OPEN) return
|
||||||
const rect = canvas.getBoundingClientRect()
|
const rect = canvas.getBoundingClientRect()
|
||||||
const x = Math.round((e.clientX - rect.left) * canvas.width / rect.width)
|
const x = Math.round((e.clientX - rect.left) * canvas.width / rect.width)
|
||||||
const y = Math.round((e.clientY - rect.top) * canvas.height / rect.height)
|
const y = Math.round((e.clientY - rect.top) * canvas.height / rect.height)
|
||||||
@@ -247,7 +302,7 @@ function wireInputHandlers() {
|
|||||||
canvas.addEventListener('contextmenu', (e) => e.preventDefault())
|
canvas.addEventListener('contextmenu', (e) => e.preventDefault())
|
||||||
canvas.addEventListener('wheel', (e) => {
|
canvas.addEventListener('wheel', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (ws?.readyState !== WebSocket.OPEN) return
|
if (inputSuspended || ws?.readyState !== WebSocket.OPEN) return
|
||||||
const rect = canvas.getBoundingClientRect()
|
const rect = canvas.getBoundingClientRect()
|
||||||
const x = Math.round((e.clientX - rect.left) * canvas.width / rect.width)
|
const x = Math.round((e.clientX - rect.left) * canvas.width / rect.width)
|
||||||
const y = Math.round((e.clientY - rect.top) * canvas.height / rect.height)
|
const y = Math.round((e.clientY - rect.top) * canvas.height / rect.height)
|
||||||
|
|||||||
@@ -179,6 +179,48 @@ body {
|
|||||||
cursor: none;
|
cursor: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Port switch overlay */
|
||||||
|
.switch-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-overlay-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-spinner {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid #444;
|
||||||
|
border-top-color: #4a7c59;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-timer {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
/* Console wrap needs position for overlay */
|
||||||
|
.console-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
/* Ports config page */
|
/* Ports config page */
|
||||||
.ports-page {
|
.ports-page {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user