1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/static/js/console_keys.js
catborise 0532471759 feat(console): one VNC console page, driven from the panel's bar
The VM page offered a lite and a full VNC console. Lite lacked a clipboard,
display settings and a way to reconnect; full put noVNC's own side bar under
the panel's bar, with a Fullscreen item that called a function noVNC does
not have, status messages hidden behind the bar, saved noVNC settings that
overrode the Access tab's, connection settings that cannot apply, and a
Connect button that reused the page's two-minute token and failed.

console-vnc.html and static/js/console_vnc.js replace both, on noVNC's RFB
with Bootstrap controls, and keep what the full view did:
- Status in the bar (Connecting, Connected, Disconnected, Connection lost,
  Could not connect, Authentication failed with the server's reason on
  hover); Disconnect, then Reconnect, which loads the page again for a new
  token.
- The VNC password in a dialog (with a username when the server asks for
  one), not a browser prompt; without passwordless console it is not in
  the page.
- Send key(s) gains Hold Ctrl / Alt / Windows (noVNC's extra keys).
- Clipboard: what the VM copies, sending text to the VM's clipboard, and
  typing it key by key (new: works in firmware, installers and login
  prompts; Shift for the characters a US keyboard shifts, 25 ms a key as
  firmware drops faster keys, verified in a UEFI shell).
- View: scaling (none, fit, resize the VM's screen), clip, drag a clipped
  screen, view only, dot cursor, quality, compression; it starts as
  Settings > Console says (a touch screen starts fitted).
- A Keyboard item on touch devices (noVNC's on-screen keyboard input,
  key events and Android's text-only input), Fullscreen with keyboard
  lock, the bell.
Dropped with the full view: noVNC's websocket host/port/path, repeater ID
and log level (the panel fixes them), automatic reconnect (the token is
single use in time), XVP power buttons (QEMU has no XVP) and shared-mode off.

The bar is compact (about 45 px in the 850x600 window). The Access tab has
one Console button instead of the Lite/Full split and four switches, and
it and the VM list open one window per VM (opening it again reloads it).
2026-10-27 08:05:31 +03:00

99 lines
3.9 KiB
JavaScript

// The keys menu of the VNC console page: key combinations the browser or the
// operating system would otherwise catch, and modifiers a device lacks.
import KeyTable from './novnc/core/input/keysym.js';
const CTRL = { keysym: KeyTable.XK_Control_L, code: 'ControlLeft' };
const ALT = { keysym: KeyTable.XK_Alt_L, code: 'AltLeft' };
const SUPER = { keysym: KeyTable.XK_Super_L, code: 'MetaLeft' };
const HOLDS = [['holdctrl', CTRL], ['holdalt', ALT], ['holdsuper', SUPER]];
function hold(rfb, id, key, down) {
const item = document.getElementById(id);
rfb.sendKey(key.keysym, key.code, down);
item.setAttribute('aria-pressed', down);
item.classList.toggle('active', down);
}
// the held modifiers up: before a combination from the menu (which presses
// and releases its own), and before the page stops sending keys
export function releaseHeld(rfb) {
for (const [id, key] of HOLDS) {
if (document.getElementById(id).getAttribute('aria-pressed') === 'true') {
hold(rfb, id, key, false);
}
}
}
// press the keys in order, release them in reverse
function sendKeys(rfb, keys) {
releaseHeld(rfb);
keys.forEach((key) => rfb.sendKey(key.keysym, key.code, true));
setTimeout(() => [...keys].reverse().forEach((key) => rfb.sendKey(key.keysym, key.code, false)), 20);
}
export function bindKeyAction(id, handler) {
const el = document.getElementById(id);
if (el) {
el.addEventListener('click', (e) => {
e.preventDefault();
handler();
});
}
}
export function bindSendKeys(rfb) {
const send = (...keys) => () => sendKeys(rfb, keys);
bindKeyAction('ctrlaltdel', () => {
releaseHeld(rfb);
rfb.sendCtrlAltDel();
});
bindKeyAction('ctrlaltbackspace', send(CTRL, ALT, { keysym: KeyTable.XK_BackSpace, code: 'Backspace' }));
bindKeyAction('alttab', send(ALT, { keysym: KeyTable.XK_Tab, code: 'Tab' }));
bindKeyAction('superkey', send(SUPER));
bindKeyAction('sendesc', send({ keysym: KeyTable.XK_Escape, code: 'Escape' }));
bindKeyAction('sendtab', send({ keysym: KeyTable.XK_Tab, code: 'Tab' }));
bindKeyAction('ctrlc', send(CTRL, { keysym: KeyTable.XK_c, code: 'KeyC' }));
bindKeyAction('ctrlz', send(CTRL, { keysym: KeyTable.XK_z, code: 'KeyZ' }));
bindKeyAction('ctrld', send(CTRL, { keysym: KeyTable.XK_d, code: 'KeyD' }));
bindKeyAction('ctrlbackslash', send(CTRL, { keysym: KeyTable.XK_backslash, code: 'Backslash' }));
for (let i = 1; i <= 12; i++) {
bindKeyAction('ctrlaltf' + i, send(CTRL, ALT, { keysym: KeyTable['XK_F' + i], code: 'F' + i }));
}
// a held modifier stays down until it is chosen again: with the next key
// typed it makes a combination the menu does not list
for (const [id, key] of HOLDS) {
bindKeyAction(id, () => {
// a view-only page sends no keys: nothing to hold
if (!rfb.viewOnly) {
hold(rfb, id, key, document.getElementById(id).getAttribute('aria-pressed') !== 'true');
}
});
}
}
// the Fullscreen item; in full screen the page also takes the keys the
// browser would keep (Alt, Tab, Escape) where the browser lets it
export function bindFullscreen() {
const item = document.getElementById('fullscreen_button');
if (!document.fullscreenEnabled) {
item.hidden = true;
return;
}
bindKeyAction('fullscreen_button', () => {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
document.documentElement.requestFullscreen().catch(() => {});
}
});
document.addEventListener('fullscreenchange', () => {
if (!navigator.keyboard) {
return;
}
if (document.fullscreenElement) {
navigator.keyboard.lock(['AltLeft', 'AltRight', 'Tab', 'Escape']).catch(() => {});
} else {
navigator.keyboard.unlock();
}
});
}