Skip to content

Commit b3cc6ae

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 ac0299e commit b3cc6ae

File tree

8 files changed

+32
-6
lines changed

8 files changed

+32
-6
lines changed

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { boundNumber, fireAndForget, stringToBase64 } from "@/util/util";
4040
import * as jotai from "jotai";
4141
import * as React from "react";
4242
import { getBlockingCommand } from "./shellblocking";
43-
import { computeTheme, DefaultTermTheme } from "./termutil";
43+
import { computeTheme, DefaultTermTheme, trimTerminalSelection } from "./termutil";
4444
import { TermWrap, WebGLSupported } from "./termwrap";
4545

4646
export class TermViewModel implements ViewModel {
@@ -750,10 +750,13 @@ export class TermViewModel implements ViewModel {
750750
} else if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:c")) {
751751
event.preventDefault();
752752
event.stopPropagation();
753-
const sel = this.termRef.current?.terminal.getSelection();
753+
let sel = this.termRef.current?.terminal.getSelection();
754754
if (!sel) {
755755
return false;
756756
}
757+
if (globalStore.get(getSettingsKeyAtom("term:trimtrailingwhitespace")) !== false) {
758+
sel = trimTerminalSelection(sel);
759+
}
757760
navigator.clipboard.writeText(sel);
758761
return false;
759762
} else if (keyutil.checkKeyPressed(waveEvent, "Cmd:k")) {
@@ -829,7 +832,11 @@ export class TermViewModel implements ViewModel {
829832
label: "Copy",
830833
click: () => {
831834
if (selection) {
832-
navigator.clipboard.writeText(selection);
835+
const text =
836+
globalStore.get(getSettingsKeyAtom("term:trimtrailingwhitespace")) !== false
837+
? trimTerminalSelection(selection)
838+
: selection;
839+
navigator.clipboard.writeText(text);
833840
}
834841
},
835842
});

frontend/app/view/term/termutil.ts

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

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

13+
export function trimTerminalSelection(text: string): string {
14+
return text
15+
.split("\n")
16+
.map((line) => line.trimEnd())
17+
.join("\n");
18+
}
19+
1320
export function normalizeCursorStyle(cursorStyle: string): TermTypes.Terminal["options"]["cursorStyle"] {
1421
if (cursorStyle === "underline" || cursorStyle === "bar") {
1522
return cursorStyle;

frontend/app/view/term/termwrap.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
extractAllClipboardData,
4343
normalizeCursorStyle,
4444
quoteForPosixShell,
45+
trimTerminalSelection,
4546
} from "./termutil";
4647

4748
const dlog = debug("wave:termwrap");
@@ -380,6 +381,7 @@ export class TermWrap {
380381

381382
async initTerminal() {
382383
const copyOnSelectAtom = getSettingsKeyAtom("term:copyonselect");
384+
const trimTrailingWhitespaceAtom = getSettingsKeyAtom("term:trimtrailingwhitespace");
383385
this.toDispose.push(this.terminal.onData(this.handleTermData.bind(this)));
384386
this.toDispose.push(
385387
this.terminal.onSelectionChange(
@@ -393,8 +395,11 @@ export class TermWrap {
393395
if (active != null && active.closest(".search-container") != null) {
394396
return;
395397
}
396-
const selectedText = this.terminal.getSelection();
398+
let selectedText = this.terminal.getSelection();
397399
if (selectedText.length > 0) {
400+
if (globalStore.get(trimTrailingWhitespaceAtom) !== false) {
401+
selectedText = trimTerminalSelection(selectedText);
402+
}
398403
navigator.clipboard.writeText(selectedText);
399404
}
400405
})

frontend/types/gotypes.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,6 +1423,7 @@ declare global {
14231423
"term:osc52"?: string;
14241424
"term:durable"?: boolean;
14251425
"term:showsplitbuttons"?: boolean;
1426+
"term:trimtrailingwhitespace"?: boolean;
14261427
"editor:minimapenabled"?: boolean;
14271428
"editor:stickyscrollenabled"?: boolean;
14281429
"editor:wordwrap"?: boolean;

pkg/wconfig/defaultconfig/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"term:cursor": "block",
3737
"term:cursorblink": false,
3838
"term:copyonselect": true,
39+
"term:trimtrailingwhitespace": true,
3940
"term:durable": false,
4041
"waveai:showcloudmodes": true,
4142
"waveai:defaultmode": "waveai@balanced",

pkg/wconfig/metaconsts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const (
6060
ConfigKey_TermOsc52 = "term:osc52"
6161
ConfigKey_TermDurable = "term:durable"
6262
ConfigKey_TermShowSplitButtons = "term:showsplitbuttons"
63+
ConfigKey_TermTrimTrailingWhitespace = "term:trimtrailingwhitespace"
6364

6465
ConfigKey_EditorMinimapEnabled = "editor:minimapenabled"
6566
ConfigKey_EditorStickyScrollEnabled = "editor:stickyscrollenabled"

pkg/wconfig/settingsconfig.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,9 @@ type SettingsType struct {
109109
TermBellSound *bool `json:"term:bellsound,omitempty"`
110110
TermBellIndicator *bool `json:"term:bellindicator,omitempty"`
111111
TermOsc52 string `json:"term:osc52,omitempty" jsonschema:"enum=focus,enum=always"`
112-
TermDurable *bool `json:"term:durable,omitempty"`
113-
TermShowSplitButtons bool `json:"term:showsplitbuttons,omitempty"`
112+
TermDurable *bool `json:"term:durable,omitempty"`
113+
TermShowSplitButtons bool `json:"term:showsplitbuttons,omitempty"`
114+
TermTrimTrailingWhitespace *bool `json:"term:trimtrailingwhitespace,omitempty"`
114115

115116
EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`
116117
EditorStickyScrollEnabled bool `json:"editor:stickyscrollenabled,omitempty"`

schema/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@
171171
"term:showsplitbuttons": {
172172
"type": "boolean"
173173
},
174+
"term:trimtrailingwhitespace": {
175+
"type": "boolean"
176+
},
174177
"editor:minimapenabled": {
175178
"type": "boolean"
176179
},

0 commit comments

Comments
 (0)