diff --git a/frontend/app/view/term/term-model.ts b/frontend/app/view/term/term-model.ts index bf77ef9535..09ccce3e54 100644 --- a/frontend/app/view/term/term-model.ts +++ b/frontend/app/view/term/term-model.ts @@ -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); } }, }); diff --git a/frontend/app/view/term/termutil.ts b/frontend/app/view/term/termutil.ts index 2fea30404a..838b8aaf92 100644 --- a/frontend/app/view/term/termutil.ts +++ b/frontend/app/view/term/termutil.ts @@ -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; diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index e1b129b72d..d10b600459 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -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); } }) diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 5c61e4199e..d7f0f0dc7a 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -1423,6 +1423,7 @@ declare global { "term:osc52"?: string; "term:durable"?: boolean; "term:showsplitbuttons"?: boolean; + "term:trimtrailingwhitespace"?: boolean; "editor:minimapenabled"?: boolean; "editor:stickyscrollenabled"?: boolean; "editor:wordwrap"?: boolean; diff --git a/pkg/wconfig/defaultconfig/settings.json b/pkg/wconfig/defaultconfig/settings.json index ff6dbbe48a..d8847cabf2 100644 --- a/pkg/wconfig/defaultconfig/settings.json +++ b/pkg/wconfig/defaultconfig/settings.json @@ -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", diff --git a/pkg/wconfig/metaconsts.go b/pkg/wconfig/metaconsts.go index 6c813954f7..7d5bba5d9d 100644 --- a/pkg/wconfig/metaconsts.go +++ b/pkg/wconfig/metaconsts.go @@ -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" diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 2a1927630f..a516364279 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -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"` diff --git a/schema/settings.json b/schema/settings.json index 91de939c38..f341a0f365 100644 --- a/schema/settings.json +++ b/schema/settings.json @@ -171,6 +171,9 @@ "term:showsplitbuttons": { "type": "boolean" }, + "term:trimtrailingwhitespace": { + "type": "boolean" + }, "editor:minimapenabled": { "type": "boolean" },