mirror of
https://github.com/wavetermdev/waveterm
synced 2026-04-21 14:37:16 +00:00
fix: trim trailing whitespace from terminal clipboard copies (#3167)
Fixes #2778. ## Problem `xterm.js`'s `getSelection()` returns lines padded to the full terminal column width. Every copy path passed this directly to `navigator.clipboard.writeText()`, so pasting into Slack, editors, etc. included hundreds of trailing spaces. ## Solution Adds a `term:trimtrailingwhitespace` setting (default `true`) that strips trailing whitespace from each line before writing to the clipboard. Applied to all three copy paths: - copy-on-select (`termwrap.ts`) - `Ctrl+Shift+C` (`term-model.ts`) - right-click Copy context menu (`term-model.ts`) OSC 52 is intentionally excluded — that path copies program-provided text, not grid content. The trim itself uses the same per-line `trimEnd()` approach already present in `bufferLinesToText` via `translateToString(true)`. Setting to `false` restores the previous behaviour. ## Files changed - `pkg/wconfig/settingsconfig.go` — new `TermTrimTrailingWhitespace *bool` field - `pkg/wconfig/defaultconfig/settings.json` — default `true` - `frontend/app/view/term/termutil.ts` — `trimTerminalSelection` helper - `frontend/app/view/term/termwrap.ts` — copy-on-select path - `frontend/app/view/term/term-model.ts` — Ctrl+Shift+C and right-click paths - Generated: `pkg/wconfig/metaconsts.go`, `frontend/types/gotypes.d.ts`, `schema/settings.json` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
38e3c2e9b3
commit
1fe0fa236c
8 changed files with 32 additions and 6 deletions
|
|
@ -40,7 +40,7 @@ import { boundNumber, fireAndForget, stringToBase64 } from "@/util/util";
|
|||
import * as jotai from "jotai";
|
||||
import * as React from "react";
|
||||
import { getBlockingCommand } from "./shellblocking";
|
||||
import { computeTheme, DefaultTermTheme } from "./termutil";
|
||||
import { computeTheme, DefaultTermTheme, trimTerminalSelection } from "./termutil";
|
||||
import { TermWrap, WebGLSupported } from "./termwrap";
|
||||
|
||||
export class TermViewModel implements ViewModel {
|
||||
|
|
@ -750,10 +750,13 @@ export class TermViewModel implements ViewModel {
|
|||
} else if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:c")) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const sel = this.termRef.current?.terminal.getSelection();
|
||||
let sel = this.termRef.current?.terminal.getSelection();
|
||||
if (!sel) {
|
||||
return false;
|
||||
}
|
||||
if (globalStore.get(getSettingsKeyAtom("term:trimtrailingwhitespace")) !== false) {
|
||||
sel = trimTerminalSelection(sel);
|
||||
}
|
||||
navigator.clipboard.writeText(sel);
|
||||
return false;
|
||||
} else if (keyutil.checkKeyPressed(waveEvent, "Cmd:k")) {
|
||||
|
|
@ -829,7 +832,11 @@ export class TermViewModel implements ViewModel {
|
|||
label: "Copy",
|
||||
click: () => {
|
||||
if (selection) {
|
||||
navigator.clipboard.writeText(selection);
|
||||
const text =
|
||||
globalStore.get(getSettingsKeyAtom("term:trimtrailingwhitespace")) !== false
|
||||
? trimTerminalSelection(selection)
|
||||
: selection;
|
||||
navigator.clipboard.writeText(text);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,13 @@ import { colord } from "colord";
|
|||
|
||||
export type GenClipboardItem = { text?: string; image?: Blob };
|
||||
|
||||
export function trimTerminalSelection(text: string): string {
|
||||
return text
|
||||
.split("\n")
|
||||
.map((line) => line.trimEnd())
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function normalizeCursorStyle(cursorStyle: string): TermTypes.Terminal["options"]["cursorStyle"] {
|
||||
if (cursorStyle === "underline" || cursorStyle === "bar") {
|
||||
return cursorStyle;
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import {
|
|||
extractAllClipboardData,
|
||||
normalizeCursorStyle,
|
||||
quoteForPosixShell,
|
||||
trimTerminalSelection,
|
||||
} from "./termutil";
|
||||
|
||||
const dlog = debug("wave:termwrap");
|
||||
|
|
@ -380,6 +381,7 @@ export class TermWrap {
|
|||
|
||||
async initTerminal() {
|
||||
const copyOnSelectAtom = getSettingsKeyAtom("term:copyonselect");
|
||||
const trimTrailingWhitespaceAtom = getSettingsKeyAtom("term:trimtrailingwhitespace");
|
||||
this.toDispose.push(this.terminal.onData(this.handleTermData.bind(this)));
|
||||
this.toDispose.push(
|
||||
this.terminal.onSelectionChange(
|
||||
|
|
@ -393,8 +395,11 @@ export class TermWrap {
|
|||
if (active != null && active.closest(".search-container") != null) {
|
||||
return;
|
||||
}
|
||||
const selectedText = this.terminal.getSelection();
|
||||
let selectedText = this.terminal.getSelection();
|
||||
if (selectedText.length > 0) {
|
||||
if (globalStore.get(trimTrailingWhitespaceAtom) !== false) {
|
||||
selectedText = trimTerminalSelection(selectedText);
|
||||
}
|
||||
navigator.clipboard.writeText(selectedText);
|
||||
}
|
||||
})
|
||||
|
|
|
|||
1
frontend/types/gotypes.d.ts
vendored
1
frontend/types/gotypes.d.ts
vendored
|
|
@ -1425,6 +1425,7 @@ declare global {
|
|||
"term:osc52"?: string;
|
||||
"term:durable"?: boolean;
|
||||
"term:showsplitbuttons"?: boolean;
|
||||
"term:trimtrailingwhitespace"?: boolean;
|
||||
"editor:minimapenabled"?: boolean;
|
||||
"editor:stickyscrollenabled"?: boolean;
|
||||
"editor:wordwrap"?: boolean;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
"term:cursor": "block",
|
||||
"term:cursorblink": false,
|
||||
"term:copyonselect": true,
|
||||
"term:trimtrailingwhitespace": true,
|
||||
"term:durable": false,
|
||||
"waveai:showcloudmodes": true,
|
||||
"waveai:defaultmode": "waveai@balanced",
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ const (
|
|||
ConfigKey_TermOsc52 = "term:osc52"
|
||||
ConfigKey_TermDurable = "term:durable"
|
||||
ConfigKey_TermShowSplitButtons = "term:showsplitbuttons"
|
||||
ConfigKey_TermTrimTrailingWhitespace = "term:trimtrailingwhitespace"
|
||||
|
||||
ConfigKey_EditorMinimapEnabled = "editor:minimapenabled"
|
||||
ConfigKey_EditorStickyScrollEnabled = "editor:stickyscrollenabled"
|
||||
|
|
|
|||
|
|
@ -109,8 +109,9 @@ type SettingsType struct {
|
|||
TermBellSound *bool `json:"term:bellsound,omitempty"`
|
||||
TermBellIndicator *bool `json:"term:bellindicator,omitempty"`
|
||||
TermOsc52 string `json:"term:osc52,omitempty" jsonschema:"enum=focus,enum=always"`
|
||||
TermDurable *bool `json:"term:durable,omitempty"`
|
||||
TermShowSplitButtons bool `json:"term:showsplitbuttons,omitempty"`
|
||||
TermDurable *bool `json:"term:durable,omitempty"`
|
||||
TermShowSplitButtons bool `json:"term:showsplitbuttons,omitempty"`
|
||||
TermTrimTrailingWhitespace *bool `json:"term:trimtrailingwhitespace,omitempty"`
|
||||
|
||||
EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`
|
||||
EditorStickyScrollEnabled bool `json:"editor:stickyscrollenabled,omitempty"`
|
||||
|
|
|
|||
|
|
@ -171,6 +171,9 @@
|
|||
"term:showsplitbuttons": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"term:trimtrailingwhitespace": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"editor:minimapenabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue