From 76f0f1727c1adb953f907274edddf7ef8a8a3f6e Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sat, 31 Jan 2026 18:07:18 -0800 Subject: [PATCH] fix(desktop): trim trailing whitespace when copying text from terminal Adds a copy handler that trims trailing whitespace from each line when copying text from the terminal. This matches the default behavior of iTerm2 and other popular terminal emulators. Fixes #1018 --- .../TabsContent/Terminal/helpers.ts | 35 +++++++++++++++++++ .../Terminal/hooks/useTerminalLifecycle.ts | 3 ++ 2 files changed, 38 insertions(+) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts index a6dda7d0031..02548abe1c8 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts @@ -313,6 +313,41 @@ export interface PasteHandlerOptions { isBracketedPasteEnabled?: () => boolean; } +/** + * Setup copy handler for xterm to trim trailing whitespace from copied text. + * + * Terminal emulators fill lines with whitespace to pad to the terminal width. + * When copying text, this results in unwanted trailing spaces on each line. + * This handler intercepts copy events and trims trailing whitespace from each + * line before writing to the clipboard. + * + * Returns a cleanup function to remove the handler. + */ +export function setupCopyHandler(xterm: XTerm): () => void { + const element = xterm.element; + if (!element) return () => {}; + + const handleCopy = (event: ClipboardEvent) => { + const selection = xterm.getSelection(); + if (!selection) return; + + // Trim trailing whitespace from each line while preserving intentional newlines + const trimmedText = selection + .split("\n") + .map((line) => line.trimEnd()) + .join("\n"); + + event.preventDefault(); + event.clipboardData?.setData("text/plain", trimmedText); + }; + + element.addEventListener("copy", handleCopy); + + return () => { + element.removeEventListener("copy", handleCopy); + }; +} + /** * Setup paste handler for xterm to ensure bracketed paste mode works correctly. * diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalLifecycle.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalLifecycle.ts index 4fbcd575eb4..52c66e4f32e 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalLifecycle.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalLifecycle.ts @@ -14,6 +14,7 @@ import { DEBUG_TERMINAL, FIRST_RENDER_RESTORE_FALLBACK_MS } from "../config"; import { createTerminalInstance, setupClickToMoveCursor, + setupCopyHandler, setupFocusListener, setupKeyboardHandler, setupPasteHandler, @@ -518,6 +519,7 @@ export function useTerminalLifecycle({ onWrite: handleWrite, isBracketedPasteEnabled: () => isBracketedPasteRef.current, }); + const cleanupCopy = setupCopyHandler(xterm); const handleVisibilityChange = () => { if (document.hidden || isUnmounted) return; @@ -562,6 +564,7 @@ export function useTerminalLifecycle({ cleanupFocus?.(); cleanupResize(); cleanupPaste(); + cleanupCopy(); cleanupQuerySuppression(); unregisterClearCallbackRef.current(paneId); unregisterScrollToBottomCallbackRef.current(paneId);