BangSo/infra/sandboxes/computer/clipboard-bridge.js

115 lines
4.0 KiB
JavaScript

/**
* Host → sandbox paste for the chrome-less noVNC embed.
*
* Debian noVNC 1.3 only syncs clipboard from the full vnc.html panel.
* Without this bridge, Ctrl/Cmd+V only forwarded keys: on macOS Cmd+V became
* Super+V (no paste on Linux), and the remote CLIPBOARD stayed empty.
*/
export const KEYSYM = {
Control_L: 0xffe3,
Control_R: 0xffe4,
Alt_L: 0xffe9,
Alt_R: 0xffea,
Super_L: 0xffeb,
Super_R: 0xffec,
Mode_switch: 0xff7e,
ISO_Level3_Shift: 0xfe03,
v: 0x76,
};
/** @param {{ ctrlKey?: boolean, metaKey?: boolean, altKey?: boolean, code?: string, key?: string }} event */
export function isPasteChord(event) {
if (!event || event.altKey) return false;
if (!(event.ctrlKey || event.metaKey)) return false;
return event.code === "KeyV" || event.key === "v" || event.key === "V";
}
/** @param {{ clipboardData?: DataTransfer | null }} event */
export function clipboardTextFromPaste(event) {
const data = event?.clipboardData;
if (!data) return "";
return data.getData("text/plain") || data.getData("text") || "";
}
/** True when noVNC RFB can accept clipboard and key events. */
export function isRfbConnected(rfb) {
const state = rfb?._rfbConnectionState;
return !state || state === "connected";
}
/**
* Release modifiers that may still be down from the host paste chord.
* noVNC 1.3 remaps macOS MetaLeft→Alt_L and MetaRight→Super_L (and Alt→
* Mode_switch / ISO_Level3_Shift), so release those keysyms too.
* @param {(keysym: number, code: string, down?: boolean) => void} sendKey
*/
export function releaseModifierKeys(sendKey) {
sendKey(KEYSYM.Control_L, "ControlLeft", false);
sendKey(KEYSYM.Control_R, "ControlRight", false);
sendKey(KEYSYM.Alt_L, "AltLeft", false);
sendKey(KEYSYM.Alt_R, "AltRight", false);
sendKey(KEYSYM.Super_L, "MetaLeft", false);
sendKey(KEYSYM.Super_R, "MetaRight", false);
sendKey(KEYSYM.Mode_switch, "AltLeft", false);
sendKey(KEYSYM.ISO_Level3_Shift, "AltRight", false);
}
/** Send Linux Ctrl+V so the focused remote app pastes from CLIPBOARD. */
export function sendRemotePaste(sendKey) {
sendKey(KEYSYM.Control_L, "ControlLeft", true);
sendKey(KEYSYM.v, "KeyV", true);
sendKey(KEYSYM.v, "KeyV", false);
sendKey(KEYSYM.Control_L, "ControlLeft", false);
}
/**
* Sync host paste into the RFB session, then paste into the remote desktop.
* @param {{ viewOnly?: boolean, clipboardPasteFrom?: (text: string) => void, sendKey?: Function, _rfbConnectionState?: string }} rfb
* @param {string} text
*/
export function pasteHostText(rfb, text) {
if (!rfb || rfb.viewOnly || !text || !isRfbConnected(rfb)) return false;
const sendKey = rfb.sendKey?.bind(rfb);
if (!sendKey || typeof rfb.clipboardPasteFrom !== "function") return false;
releaseModifierKeys(sendKey);
rfb.clipboardPasteFrom(text);
sendRemotePaste(sendKey);
return true;
}
/**
* @param {{ viewOnly?: boolean, clipboardPasteFrom?: (text: string) => void, sendKey?: Function, _rfbConnectionState?: string }} rfb
* @param {{ target?: EventTarget }} [options]
* @returns {() => void} detach
*/
export function attachHostClipboardPaste(rfb, options = {}) {
const target = options.target ?? globalThis;
const onKeyDown = (event) => {
if (rfb.viewOnly || !isPasteChord(event)) return;
// Keep default so the browser still fires `paste` with clipboardData.
event.stopPropagation();
};
const onKeyUp = (event) => {
if (rfb.viewOnly || !isPasteChord(event)) return;
event.stopPropagation();
};
const onPaste = (event) => {
if (rfb.viewOnly) return;
const text = clipboardTextFromPaste(event);
if (!text) return;
if (!pasteHostText(rfb, text)) return;
event.preventDefault();
event.stopPropagation();
};
target.addEventListener("keydown", onKeyDown, true);
target.addEventListener("keyup", onKeyUp, true);
target.addEventListener("paste", onPaste, true);
return () => {
target.removeEventListener("keydown", onKeyDown, true);
target.removeEventListener("keyup", onKeyUp, true);
target.removeEventListener("paste", onPaste, true);
};
}