feat: restore Ctrl+Shift+V via synthetic paste event on xterm textarea
Ctrl+Shift+V does not trigger a native browser paste event (unlike Cmd+V on macOS). Fix: read clipboard via navigator.clipboard.readText(), then dispatch a synthetic ClipboardEvent on xterm.js's hidden textarea. This triggers xterm.js's built-in handlePasteEvent which handles CR/LF normalization, bracketed paste mode, and correct UTF-8 encoding — the same code path as Cmd+V. Also replaces the old regression test that asserted Ctrl+Shift+V was NOT intercepted, replacing it with the correct test for the synthetic paste approach.
This commit is contained in:
@@ -365,11 +365,12 @@ function openTerminal(sessionName, remoteId) {
|
||||
|
||||
// --- Clipboard integration ---
|
||||
// Ctrl+Shift+C: copy selection to system clipboard (Ctrl+C still sends SIGINT)
|
||||
// Ctrl+Shift+V: handled natively by xterm.js — browser fires paste event on xterm's hidden
|
||||
// textarea → xterm.js handlePasteEvent → prepareTextForTerminal → bracketTextForPaste
|
||||
// → triggerDataEvent → onData → WebSocket. This path correctly handles both Unicode
|
||||
// encoding (TextEncoder) and bracketed paste mode (multiline protection). Do NOT
|
||||
// intercept Ctrl+Shift+V here — every custom handler we tried broke one or the other.
|
||||
// Ctrl+Shift+V: Ctrl+Shift+V does NOT trigger a native browser paste event (unlike Cmd+V
|
||||
// on macOS). We read the clipboard via navigator.clipboard.readText() and dispatch a
|
||||
// synthetic ClipboardEvent on xterm.js's hidden textarea. xterm.js catches this via its
|
||||
// built-in handlePasteEvent listener → prepareTextForTerminal → bracketTextForPaste
|
||||
// → triggerDataEvent → onData → WebSocket. This is the same path as Cmd+V and correctly
|
||||
// handles CR/LF normalization, bracketed paste mode, and UTF-8 encoding.
|
||||
_term.attachCustomKeyEventHandler(function(e) {
|
||||
if (e.type !== 'keydown') return true;
|
||||
|
||||
@@ -380,6 +381,24 @@ function openTerminal(sessionName, remoteId) {
|
||||
return false; // prevent xterm from processing
|
||||
}
|
||||
|
||||
// Ctrl+Shift+V → paste from system clipboard via synthetic ClipboardEvent on xterm textarea.
|
||||
// Dispatching on _term.textarea triggers xterm.js's built-in handlePasteEvent, which
|
||||
// handles CR/LF normalization, bracketed paste mode, and correct UTF-8 encoding —
|
||||
// the exact same code path as a native Cmd+V paste on macOS.
|
||||
if (e.ctrlKey && e.shiftKey && (e.key === 'V' || e.code === 'KeyV')) {
|
||||
if (navigator.clipboard && navigator.clipboard.readText && _term && _term.textarea) {
|
||||
navigator.clipboard.readText().then(function(text) {
|
||||
if (!text || !_term || !_term.textarea) return;
|
||||
var pasteEvent = new ClipboardEvent('paste', {
|
||||
clipboardData: new DataTransfer(),
|
||||
});
|
||||
pasteEvent.clipboardData.setData('text/plain', text);
|
||||
_term.textarea.dispatchEvent(pasteEvent);
|
||||
}).catch(function() {});
|
||||
}
|
||||
return false; // prevent xterm from processing the raw key
|
||||
}
|
||||
|
||||
// Ctrl+F → open search bar
|
||||
if (e.ctrlKey && !e.shiftKey && (e.key === 'f' || e.key === 'F' || e.code === 'KeyF')) {
|
||||
_openSearch();
|
||||
|
||||
@@ -993,21 +993,25 @@ test('terminal.js loads xterm-addon-image for inline graphics', () => {
|
||||
assert.ok(source.includes('ImageAddon'), 'must reference ImageAddon');
|
||||
});
|
||||
|
||||
// --- Native paste: xterm.js handles Ctrl+Shift+V ---
|
||||
// --- Ctrl+Shift+V: synthetic paste event on xterm textarea ---
|
||||
|
||||
test('terminal.js does NOT intercept Ctrl+Shift+V — lets xterm.js handle paste natively', () => {
|
||||
// Regression: every custom paste handler we tried broke either multiline or Unicode.
|
||||
// xterm.js's native paste pipeline (browser paste event → handlePasteEvent →
|
||||
// prepareTextForTerminal → bracketTextForPaste → triggerDataEvent → onData → WS)
|
||||
// handles both correctly. Fix: stop intercepting Ctrl+Shift+V entirely.
|
||||
test('terminal.js Ctrl+Shift+V dispatches synthetic paste event on textarea', () => {
|
||||
// Ctrl+Shift+V does NOT trigger a native browser paste event (unlike Cmd+V on macOS).
|
||||
// Fix: read clipboard via navigator.clipboard.readText(), then dispatch a synthetic
|
||||
// ClipboardEvent on xterm.js's hidden textarea. This goes through xterm.js's built-in
|
||||
// handlePasteEvent which handles CR/LF normalization, bracketed paste, and correct encoding.
|
||||
const source = fs.readFileSync(new URL('../terminal.js', import.meta.url), 'utf8');
|
||||
const handlerStart = source.indexOf('attachCustomKeyEventHandler');
|
||||
const handlerEnd = source.indexOf('// Auto-copy:', handlerStart);
|
||||
const handlerBlock = source.substring(handlerStart, handlerEnd);
|
||||
const hasVIntercept = handlerBlock.includes("e.key === 'V'") || handlerBlock.includes("e.code === 'KeyV'");
|
||||
assert.ok(!hasVIntercept,
|
||||
'attachCustomKeyEventHandler must NOT intercept Ctrl+Shift+V — ' +
|
||||
'xterm.js handles paste natively with correct encoding + bracketed paste');
|
||||
assert.ok(
|
||||
handlerBlock.includes("e.key === 'V'") || handlerBlock.includes("e.code === 'KeyV'"),
|
||||
'attachCustomKeyEventHandler must intercept Ctrl+Shift+V to trigger paste',
|
||||
);
|
||||
assert.ok(
|
||||
handlerBlock.includes('ClipboardEvent') || handlerBlock.includes('dispatchEvent'),
|
||||
'must dispatch synthetic paste event so xterm.js handlePasteEvent handles encoding correctly',
|
||||
);
|
||||
});
|
||||
|
||||
// --- UTF-8 output decoding via TextDecoder ---
|
||||
|
||||
Reference in New Issue
Block a user