copying scrollback: the host terminal only ever sees one painted screen
open by a73x
Labels: backlog, client, daily-driver, copypaste
[claude 2026-08-15] Copy/paste discovery, 2026-08-15. The third piece, and the only one whose everyday half currently WORKS. WHAT WORKS TODAY, and why. The client never enables mouse tracking on the host tty (measured: ESC[?1000h and ESC[?1006h both absent from what the client writes, even with the session asking for them). With mouse reporting off, your own terminal keeps the mouse, so select-and-copy over mux's painted screen is your terminal's native selection and it works. That is why copy has been survivable without any mux feature at all. THE LIMIT. What your terminal can select is the 80x24 (or whatever) of PAINTED cells. mux keeps 10k rows of scrollback in the daemon's engine and repaints the viewport in place when you scroll — so scrollback is selectable only one screenful at a time, by scrolling, selecting, scrolling, selecting. There is no select-across-history, no search-and-copy, no rectangle. tmux's copy-mode is what fills this and mux has no equivalent. INTERACTION WITH THE MODE-MIRROR ISSUE — read before implementing that one. Mirroring the session's mouse modes to the host tty is the fix for 'the wheel does nothing in mux' and 'vim mouse is dead', and it WILL take the mouse away from your terminal whenever the app inside asks for it. That is correct behaviour (it is what every terminal does under tmux) but it means the mode mirror TRADES the copy path that currently works for one that does not exist yet. The two issues are therefore ordered: a copy mechanism of mux's own should land with, or before, the mouse half of the mirror. Bracketed paste has no such coupling and can go first alone. UNVERIFIED, and named as such: everything above about native selection is reasoned from 'no mouse reporting is ever enabled' plus the alt-screen paint, and confirmed only in the negative (the escapes are absent). Nobody has sat at a real terminal and dragged across a mux screen to check that wide chars, trailing spaces and the sync bracket do not mangle the selected text. That is a five-minute human check and it should happen before any design is written on top of it. SHAPE, when it comes: a client-side copy mode over the replica grid (which already holds the scrollback the daemon serves lazily), reusing the existing scroll-mode plumbing for movement, and terminating in the OSC 52 write from the sibling issue — so the two land together and 'copy' means the same thing whether the text came from the screen or from history.
Comments
a73x
The owed hands-on check is done, 2026-08-21, against a build of main at
38deccf on a real terminal (Wayland, wl-paste). This ticket has carried
"nobody has sat at a real terminal and dragged across a mux screen" since
2026-08-15; here is the answer, and it moves the argument.
FIXTURE. Six lines, each isolating one hazard: plain ASCII, wide/CJK
(漢字), three trailing spaces, an embedded tab, a 140-character line that
soft-wraps, and an end marker. Shift+drag across all six, Ctrl+Shift+C,
wl-paste to a file, diff against the fixture's own stdout.
DRAGGING WITHOUT SHIFT DOES NOTHING AT ALL. Not degraded selection —
none. Two layers each correctly decline: the host terminal stops its
native selection because the client holds ?1000h+?1006h for the wheel,
and then MouseFilter.wheelNotches returns 0 for anything that is not a
wheel press and the bytes are dropped, which is what keeps `[<0;40;12M`
out of the user's shell. This ticket predicted that in the negative (the
escapes were absent from what the client writes); it is now positive
evidence, and it is the cost the mode-mirror issue said it was trading.
SHIFT+DRAG WORKS, WITH EXACTLY ONE REAL DEFECT.
plain ASCII exact
wide/CJK 漢字 exact — bytes intact, no half-glyph, no replacement
trailing spaces trimmed (you get `trailing3>`, not 60 columns of pad)
tab expanded to spaces
soft-wrapped SPURIOUS NEWLINE at the wrap — 140 chars came back
split 141 + 6 at the terminal's right edge
The tab loss is NOT fallback-specific and should not be counted against
native selection: the grid stores the cells the cursor landed on, never
the tab character, so mux's own extraction loses it identically.
The soft wrap is the one place the two differ, and mux wins it — verified
in the code, not assumed. extractSelection builds its ScreenFormatter with
`.unwrap = true, .trim = true` (engine.zig:542-546), and the test at
engine.zig:625 feeds "abcdeFG" into a 5-column grid and extracts "deFG"
straight across the wrap with no newline in it. Wide-cell correctness is
likewise already daemon-side (engine.zig:663: clicking either half of a
wide glyph extracts the glyph).
WHAT THIS DOES TO THE TICKET. The premise was "the host terminal only
ever sees one painted screen", and that stands — history remains
unselectable and that is still the whole problem. But the secondary worry
in the close-out note, that native selection might mangle wide chars or
trailing spaces, is answered: it does not. The characters are fine. So a
mux-native selection is NOT justified as a correctness fix over the
fallback. Its concrete wins are, in order: history becomes selectable at
all; soft-wrapped lines survive; and no modifier is needed.
Worth recording because it makes the case narrower and more honest than
the ticket assumed, and because one of the three wins (soft wrap) was not
on anyone's list before the measurement.
A design for the mouse half is written up at
docs/superpowers/specs/2026-08-21-cli-drag-selection-design.md — copy on
release, per stripe, sliced M1-M4, with scroll-mode selection named as the
next slice rather than folded in. Note it answers this ticket's need with
the MOUSE; the keyboard copy mode this ticket originally specced stays
unbuilt.
CAVEAT, named rather than assumed: one terminal, on Wayland. The
soft-wrap defect is a property of how terminals store painted rows and is
expected to be universal, but it was not confirmed on a second one.
a73x
Follow-up to the 2026-08-21 measurement above, with the terminals named
and one correction.
TERMINALS: alacritty and foot 1.27, both on Wayland, two runs each. The
"Terminal A" of the previous comment was alacritty.
SOFT WRAP — CONFIRMED, four runs out of four. The 147-character line came
back split every time, at whatever column the window happened to be (141
at one width, 110 at another). Purely a spurious newline, never
truncation: all 147 characters survive. This is the defect the design
argues from and it is now well supported.
CORRECTION — the two terminals do NOT differ. An earlier reading of mine
claimed alacritty trimmed the fixture's trailing spaces while foot kept
them, and drew a conclusion from it ("the answer depends on which terminal
you are sitting at"). That was wrong. On re-run the two pastes were
byte-identical to each other apart from the clock in the prompt line: both
trim, both split at the same column. The claim is withdrawn and the
argument that rested on it is gone from the spec.
What remains unexplained, recorded rather than smoothed over: the FIRST
foot run returned the trailing-space line as exactly 16 bytes — the three
written spaces, not padding out to the window width — while three later
runs trimmed it to 13. Never reproduced, and "the drag swept in the
margin" does not account for it landing on exactly the written content. If
trailing whitespace ever matters, that is the loose thread.
NET EFFECT ON THE CASE FOR THE FEATURE. It is narrower than the previous
comment left it. Three wins, not four: history becomes selectable at all;
soft-wrapped lines survive; no modifier is needed. Trailing-space handling
is a wash — the terminal trims and extractSelection trims too
(`.trim = true`, engine.zig:542-546).
Method note for whoever repeats this: run the fixture, drag, paste to a
file, and diff against the fixture's own stdout. Byte counts settle
questions that eyeballing does not — the whole correction above came from
`length($0)` on one line, and the original error came from trusting a
single run.