From 553fcc41fc0b229d6a50493ec1864a2003c2cb34 Mon Sep 17 00:00:00 2001 From: Aditi Chauhan Date: Thu, 18 Jun 2026 13:53:58 +0530 Subject: [PATCH] fix(frontend): wire clipboard copy/paste in XtermTerminal zellij owns SGR mouse tracking and xterm's selection model is canvas-only, so neither the OS Copy command nor plain drag-to-select worked. Intercept Cmd/Ctrl+C to copy the active selection (falling through when nothing is selected so Ctrl+C still sends SIGINT) and Cmd/Ctrl+V to paste explicitly via the clipboard API rather than relying on xterm's native paste event. Fixes #305 Co-Authored-By: Claude Sonnet 4.6 --- .../src/renderer/components/XtermTerminal.tsx | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/frontend/src/renderer/components/XtermTerminal.tsx b/frontend/src/renderer/components/XtermTerminal.tsx index fe1e3cf0..6fc30650 100644 --- a/frontend/src/renderer/components/XtermTerminal.tsx +++ b/frontend/src/renderer/components/XtermTerminal.tsx @@ -71,6 +71,43 @@ const terminalThemes = buildTerminalThemes(); // events stop reaching zellij. The clear only wipes pixels; modes stay up. const CLEAR_SEQUENCE = "\x1b[3J\x1b[2J\x1b[H"; +// xterm's selection is an internal model rendered to canvas/WebGL, not a DOM +// selection, so the OS/Electron "Copy" command can never see it — we must +// intercept Cmd/Ctrl+C ourselves. When nothing is selected, Ctrl+C must keep +// reaching the PTY (it's SIGINT), so we only swallow the event when there's a +// selection to copy. Cmd/Ctrl+V is wired explicitly too: relying on xterm's +// native DOM 'paste' event is not reliable across platforms under Electron. +function isCopyShortcut(event: KeyboardEvent): boolean { + const modifier = event.metaKey || event.ctrlKey; + return modifier && event.key.toLowerCase() === "c"; +} + +function isPasteShortcut(event: KeyboardEvent): boolean { + const modifier = event.metaKey || event.ctrlKey; + return modifier && event.key.toLowerCase() === "v"; +} + +function attachClipboardHandling(term: Terminal): void { + term.attachCustomKeyEventHandler((event) => { + if (event.type !== "keydown") return true; + + if (isCopyShortcut(event)) { + if (!term.hasSelection()) return true; + void navigator.clipboard.writeText(term.getSelection()); + return false; + } + + if (isPasteShortcut(event)) { + void navigator.clipboard.readText().then((text) => { + if (text) term.paste(text); + }); + return false; + } + + return true; + }); +} + export function XtermTerminal(props: XtermTerminalProps) { const hostRef = useRef(null); const termRef = useRef(null); @@ -142,6 +179,7 @@ export function XtermTerminal(props: XtermTerminalProps) { term.open(host); loadRenderer(term); + attachClipboardHandling(term); const fitTerminal = () => { try {