Skip to content

Commit 4817f82

Browse files
Drew-Goddynclaude
andcommitted
fix: trim trailing whitespace from terminal clipboard copies
Fixes #2778. xterm.js getSelection() returns lines padded to the full terminal column width, resulting in hundreds of trailing spaces when pasting into external apps. Adds a term:trimtrailingwhitespace setting (default true) that strips trailing whitespace from each line before writing to the clipboard. Applies to all three copy paths: copy-on-select, Ctrl+Shift+C, and right-click Copy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2601e35 commit 4817f82

File tree

8 files changed

+32
-7
lines changed

8 files changed

+32
-7
lines changed

frontend/app/view/term/term-model.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { boundNumber, stringToBase64 } from "@/util/util";
3838
import * as jotai from "jotai";
3939
import * as React from "react";
4040
import { getBlockingCommand } from "./shellblocking";
41-
import { computeTheme, DefaultTermTheme } from "./termutil";
41+
import { computeTheme, DefaultTermTheme, trimTerminalSelection } from "./termutil";
4242
import { TermWrap } from "./termwrap";
4343

4444
export class TermViewModel implements ViewModel {
@@ -631,10 +631,13 @@ export class TermViewModel implements ViewModel {
631631
} else if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:c")) {
632632
event.preventDefault();
633633
event.stopPropagation();
634-
const sel = this.termRef.current?.terminal.getSelection();
634+
let sel = this.termRef.current?.terminal.getSelection();
635635
if (!sel) {
636636
return false;
637637
}
638+
if (globalStore.get(getSettingsKeyAtom("term:trimtrailingwhitespace")) !== false) {
639+
sel = trimTerminalSelection(sel);
640+
}
638641
navigator.clipboard.writeText(sel);
639642
return false;
640643
} else if (keyutil.checkKeyPressed(waveEvent, "Cmd:k")) {
@@ -692,7 +695,11 @@ export class TermViewModel implements ViewModel {
692695
label: "Copy",
693696
click: () => {
694697
if (selection) {
695-
navigator.clipboard.writeText(selection);
698+
const text =
699+
globalStore.get(getSettingsKeyAtom("term:trimtrailingwhitespace")) !== false
700+
? trimTerminalSelection(selection)
701+
: selection;
702+
navigator.clipboard.writeText(text);
696703
}
697704
},
698705
});

frontend/app/view/term/termutil.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ import { colord } from "colord";
99

1010
export type GenClipboardItem = { text?: string; image?: Blob };
1111

12+
export function trimTerminalSelection(text: string): string {
13+
return text
14+
.split("\n")
15+
.map((line) => line.trimEnd())
16+
.join("\n");
17+
}
18+
1219
function applyTransparencyToColor(hexColor: string, transparency: number): string {
1320
const alpha = 1 - transparency; // transparency is already 0-1
1421
return colord(hexColor).alpha(alpha).toHex();

frontend/app/view/term/termwrap.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import debug from "debug";
3030
import * as jotai from "jotai";
3131
import { debounce } from "throttle-debounce";
3232
import { FitAddon } from "./fitaddon";
33-
import { createTempFileFromBlob, extractAllClipboardData } from "./termutil";
33+
import { createTempFileFromBlob, extractAllClipboardData, trimTerminalSelection } from "./termutil";
3434

3535
const dlog = debug("wave:termwrap");
3636

@@ -555,6 +555,7 @@ export class TermWrap {
555555

556556
async initTerminal() {
557557
const copyOnSelectAtom = getSettingsKeyAtom("term:copyonselect");
558+
const trimTrailingWhitespaceAtom = getSettingsKeyAtom("term:trimtrailingwhitespace");
558559
this.toDispose.push(this.terminal.onData(this.handleTermData.bind(this)));
559560
this.toDispose.push(this.terminal.onKey(this.onKeyHandler.bind(this)));
560561
this.toDispose.push(
@@ -563,8 +564,11 @@ export class TermWrap {
563564
if (!globalStore.get(copyOnSelectAtom)) {
564565
return;
565566
}
566-
const selectedText = this.terminal.getSelection();
567+
let selectedText = this.terminal.getSelection();
567568
if (selectedText.length > 0) {
569+
if (globalStore.get(trimTrailingWhitespaceAtom) !== false) {
570+
selectedText = trimTerminalSelection(selectedText);
571+
}
568572
navigator.clipboard.writeText(selectedText);
569573
}
570574
})

frontend/types/gotypes.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,7 @@ declare global {
12501250
"term:macoptionismeta"?: boolean;
12511251
"term:bellsound"?: boolean;
12521252
"term:bellindicator"?: boolean;
1253+
"term:trimtrailingwhitespace"?: boolean;
12531254
"editor:minimapenabled"?: boolean;
12541255
"editor:stickyscrollenabled"?: boolean;
12551256
"editor:wordwrap"?: boolean;

pkg/wconfig/defaultconfig/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"term:bellsound": false,
2828
"term:bellindicator": true,
2929
"term:copyonselect": true,
30+
"term:trimtrailingwhitespace": true,
3031
"waveai:showcloudmodes": true,
3132
"waveai:defaultmode": "waveai@balanced"
3233
}

pkg/wconfig/metaconsts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const (
5050
ConfigKey_TermMacOptionIsMeta = "term:macoptionismeta"
5151
ConfigKey_TermBellSound = "term:bellsound"
5252
ConfigKey_TermBellIndicator = "term:bellindicator"
53+
ConfigKey_TermTrimTrailingWhitespace = "term:trimtrailingwhitespace"
5354

5455
ConfigKey_EditorMinimapEnabled = "editor:minimapenabled"
5556
ConfigKey_EditorStickyScrollEnabled = "editor:stickyscrollenabled"

pkg/wconfig/settingsconfig.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,9 @@ type SettingsType struct {
9595
TermAllowBracketedPaste *bool `json:"term:allowbracketedpaste,omitempty"`
9696
TermShiftEnterNewline *bool `json:"term:shiftenternewline,omitempty"`
9797
TermMacOptionIsMeta *bool `json:"term:macoptionismeta,omitempty"`
98-
TermBellSound *bool `json:"term:bellsound,omitempty"`
99-
TermBellIndicator *bool `json:"term:bellindicator,omitempty"`
98+
TermBellSound *bool `json:"term:bellsound,omitempty"`
99+
TermBellIndicator *bool `json:"term:bellindicator,omitempty"`
100+
TermTrimTrailingWhitespace *bool `json:"term:trimtrailingwhitespace,omitempty"`
100101

101102
EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`
102103
EditorStickyScrollEnabled bool `json:"editor:stickyscrollenabled,omitempty"`

schema/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@
128128
"term:bellindicator": {
129129
"type": "boolean"
130130
},
131+
"term:trimtrailingwhitespace": {
132+
"type": "boolean"
133+
},
131134
"editor:minimapenabled": {
132135
"type": "boolean"
133136
},

0 commit comments

Comments
 (0)