Skip to content

fix(desktop): make terminal output selectable - #4980

Open
wesbillman wants to merge 5 commits into
mainfrom
carl/terminal-selection
Open

fix(desktop): make terminal output selectable#4980
wesbillman wants to merge 5 commits into
mainfrom
carl/terminal-selection

Conversation

@wesbillman

Copy link
Copy Markdown
Collaborator

Summary

  • mirror the retained canvas terminal grid into a transparent, selectable text layer
  • preserve the canvas renderer and terminal focus behavior for ordinary clicks
  • reconstruct wide and combining glyphs correctly for clipboard text

Why

Buzz Term renders output entirely on a canvas and deliberately called preventDefault() on viewport mouse-down, so native selection and copy could not work. A canvas has no selectable text even if that cancellation is removed.

The transparent text layer stays aligned with the visible cell grid, lets WebView native selection drive drag highlighting and copy, and follows active-session switches without changing the renderer or PTY protocol.

Validation

  • pnpm --dir desktop typecheck
  • pnpm --dir desktop test — 4,373 passed
  • pre-push desktop-check, desktop-test, and branch-skew hooks passed on 1f2a3f8db63f6fe36b4a28bc911aea3c5186b2b0

Mirror the retained canvas grid into a transparent text layer so native
mouse selection and clipboard copy work without replacing the terminal's
canvas renderer.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
@wesbillman
wesbillman requested a review from a team as a code owner August 5, 2026 23:52
@wesbillman

Copy link
Copy Markdown
Collaborator Author

[P1] Preserve soft-wrap boundaries before exposing clipboard textTerminalGrid.text() unconditionally joins every screen row with "\n". That makes a single terminal line which merely wrapped at the viewport edge copy as multiple newline-delimited commands/records. For example, the backend's existing 5-column fixture feeds abcdef, rendering abcde on row 0 and f on row 1 (desktop/src-tauri/crates/buzz-terminal/tests/clusters.rs:290-313); selecting both rows through this layer copies abcde\nf, not abcdef. This is especially dangerous for copying wrapped shell commands because pasting the result can execute the fragments separately. The backend knows soft wraps via Alacritty's WRAPLINE, but deliberately removes that geometry bit before constructing Style (desktop/src-tauri/crates/buzz-terminal/src/damage.rs:443-466), so the frontend currently cannot distinguish a soft wrap from a real newline. Please retain/transport row-wrap metadata and have the selection serialization omit \n after soft-wrapped rows, with an end-to-end fixture covering both soft wrap and a real newline.

Carry terminal row wrap geometry through the retained-grid transport so
selection serialization joins visual rows only when they belong to the same
logical line.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
@wesbillman

Copy link
Copy Markdown
Collaborator Author

[P1] Do not trim spaces from a soft-wrapped row — The wrap separator is now correct, but text() still calls trimEnd() before deciding whether row.wrapped is true. A terminal row can wrap on a literal space: in a 5-column terminal, output such as abcd ef fills row 0 with abcd , marks that row WRAPLINE when e advances to row 1, and then continues with ef. This serializer turns those rows into abcd + ef, so the clipboard contains abcdef rather than abcd ef. That can still mutate copied shell commands and structured output. For a wrapped row, serialize the retained cells without trimming; only hard-ended rows may drop terminal padding. Please make the frontend regression realistic by filling the soft-wrapped row to its terminal width and include a boundary-space case. The incremental retention path itself looks correct: wrap state is replaced with each changed row, and the backend hash already includes cell.flags.bits(), so a wrap-only transition cannot be deduplicated away.

wesbillman and others added 3 commits August 5, 2026 18:14
Only trim terminal padding from hard-ended rows. A boundary space on a
soft-wrapped row is command data and must survive selection serialization.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Render one selectable DOM row per terminal screen row for accurate native
hit testing, then serialize the selected cell range on copy so soft wraps
remain absent from clipboard text.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Clamp native DOM range endpoints to retained grapheme boundaries before
serializing clipboard text, and keep empty-row placeholder offsets within
the terminal model.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant