feat: auto-copy mouse selection + OSC 52 tmux clipboard bridge
Mouse selection: onSelectionChange auto-copies to system clipboard on every selection change (last fire on mouse-up captures final text). Matches iTerm2/WezTerm/ttyd native select-to-copy behavior. OSC 52: Custom parser.registerOscHandler(52, ...) decodes base64 clipboard data from tmux. When tmux copies text (set-clipboard on in .tmux.conf), it sends OSC 52 to the terminal. We intercept and write to system clipboard via _copyToClipboard (with execCommand fallback for HTTP/LAN contexts). Together: mouse-select auto-copies, and tmux Ctrl+B [ -> select -> Enter copies to system clipboard via OSC 52.
This commit is contained in:
@@ -312,6 +312,36 @@ function openTerminal(sessionName, sourceUrl) {
|
|||||||
return true; // let xterm handle all other keys normally
|
return true; // let xterm handle all other keys normally
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto-copy: when mouse selection ends, copy to system clipboard.
|
||||||
|
// Matches terminal emulator conventions (iTerm2, WezTerm, ttyd native).
|
||||||
|
// onSelectionChange fires whenever selection changes — copy if text is selected.
|
||||||
|
// When selection is cleared (empty string), we skip the clipboard write.
|
||||||
|
_term.onSelectionChange(function() {
|
||||||
|
var sel = _term.getSelection();
|
||||||
|
if (sel) {
|
||||||
|
_copyToClipboard(sel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// OSC 52 clipboard integration — bridges tmux clipboard to the browser.
|
||||||
|
// When tmux copies text (with `set-clipboard on` in .tmux.conf), it sends
|
||||||
|
// an OSC 52 escape sequence to the terminal. xterm.js surfaces this via the
|
||||||
|
// parser API. We intercept and write the decoded text to the system clipboard
|
||||||
|
// so that: Ctrl+B [ → select → Enter (tmux copy) → system clipboard receives it.
|
||||||
|
_term.parser.registerOscHandler(52, function(data) {
|
||||||
|
// OSC 52 format: Pc ; Pd — Pc = selection target (c/p/q/s/0-7), Pd = base64 text
|
||||||
|
var parts = data.split(';');
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
try {
|
||||||
|
var text = atob(parts[1]);
|
||||||
|
_copyToClipboard(text);
|
||||||
|
} catch (e) {
|
||||||
|
// Invalid base64 or unsupported — silently ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true; // Handled — don't pass to xterm's default handler
|
||||||
|
});
|
||||||
|
|
||||||
if (_fitAddon) {
|
if (_fitAddon) {
|
||||||
// requestAnimationFrame guarantees one full browser layout pass after the flex
|
// requestAnimationFrame guarantees one full browser layout pass after the flex
|
||||||
// container becomes visible before fit() measures dimensions.
|
// container becomes visible before fit() measures dimensions.
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ function loadTerminal() {
|
|||||||
focus: () => { focusCallCount++; },
|
focus: () => { focusCallCount++; },
|
||||||
attachCustomKeyEventHandler: () => {},
|
attachCustomKeyEventHandler: () => {},
|
||||||
getSelection: () => '',
|
getSelection: () => '',
|
||||||
|
onSelectionChange: () => {},
|
||||||
|
parser: { registerOscHandler: () => {} },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Capture all messages sent via WebSocket.send()
|
// Capture all messages sent via WebSocket.send()
|
||||||
@@ -335,6 +337,8 @@ function createMultiSessionEnv() {
|
|||||||
focus: () => {},
|
focus: () => {},
|
||||||
attachCustomKeyEventHandler: () => {},
|
attachCustomKeyEventHandler: () => {},
|
||||||
getSelection: () => '',
|
getSelection: () => '',
|
||||||
|
onSelectionChange: () => {},
|
||||||
|
parser: { registerOscHandler: () => {} },
|
||||||
writeMessages: [],
|
writeMessages: [],
|
||||||
};
|
};
|
||||||
t.write = (data) => t.writeMessages.push(data);
|
t.write = (data) => t.writeMessages.push(data);
|
||||||
@@ -921,4 +925,28 @@ test('_setTerminalFontSize sets _term.options.fontSize and calls _fitAddon.fit()
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Clipboard Issue 1: auto-copy mouse selection via onSelectionChange ---
|
||||||
|
|
||||||
|
test('terminal.js auto-copies mouse selection to clipboard via onSelectionChange', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../terminal.js', import.meta.url), 'utf8');
|
||||||
|
assert.ok(
|
||||||
|
source.includes('onSelectionChange'),
|
||||||
|
'must register onSelectionChange handler to auto-copy mouse selection to clipboard',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Clipboard Issue 2: OSC 52 handler bridges tmux clipboard to browser ---
|
||||||
|
|
||||||
|
test('terminal.js registers OSC 52 handler for tmux clipboard bridge', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../terminal.js', import.meta.url), 'utf8');
|
||||||
|
assert.ok(
|
||||||
|
source.includes('registerOscHandler'),
|
||||||
|
'must call parser.registerOscHandler to intercept tmux OSC 52 clipboard sequences',
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
source.includes('atob'),
|
||||||
|
'must decode base64 OSC 52 clipboard payload with atob()',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user