` |
| Cursor | none | block copy cursor, which is also the selection's moving end |
| Selection | the **browser's own**, on `user-select: text` content | cell ranges the client computes (`v`, `V`, `y`) |
| Keys | go to the pane; the first one closes the view | intercepted and resolved as vi motions |
| Closed by | typing, `Escape`, or scrolling back to the bottom | `q` / `Escape` / `y`, or scrolling to the bottom |
They share the record, the loader, the chunk merge and the renderer, so a pane has at most one of
them and the difference is only in cursor, selection, key routing and what tmux is told.
**Selection outside copy mode is the browser's.** Terminal content that is not a mouse-tracking
application carries `terminal-selectable` (`user-select: text`), so dragging, double-clicking and
triple-clicking select text on the live screen and in the scroll view without entering any mode —
and `Cmd+C` copies it. Only copy mode drives selection from the client, because only there is the
selection anchored to a cursor the keyboard moves. Panes whose application tracks the mouse are
deliberately left unselectable: there the drag belongs to the application and is forwarded as SGR.
Where the browser owns the drag, the pane does not take the hidden keyboard input's focus on the
press: focusing an input collapses the page's selection, so a drag across text selected nothing. The
keyboard actor hands focus to the input on the release instead, and only if nothing was selected.
While the browser holds a selection on the live screen, the screen holds still
(`hooks/useHeldWhileSelected.ts`): the live grid reuses row nodes by screen index, so output
that scrolled the screen slid other text under the highlight and `Cmd+C` copied the wrong lines.
The program keeps running; the pane catches up the moment the selection collapses. The scroll
view needs none of this — its rows are keyed by absolute line.
**Select all is the client's selection, in either view.** `Cmd+A` (`Ctrl+Shift+A` off macOS) selects a
pane's whole scrollback — opening the scroll view on a pane that has no view yet, since there is
nothing else to hold a selection over history that is not on screen. On a pane whose application owns the screen (alternate screen, or tmux's own mode) it does nothing —
the same gate the wheel uses, since there is no scrollback behind such a screen. It is a _client_
selection (a line selection from the first row of history to the last row on screen), not the
browser's: most of
the history has no DOM node to select, so the browser could not express it and the copy could not
read it. `Cmd+C` (`Ctrl+Shift+C` off macOS — plain `Ctrl+C` stays the interrupt) copies it from the
loaded rows and closes the view with the usual blink, as a yank does. Because every selected row has
to be backed by loaded cells, select-all keeps asking for the next unloaded gap
(`firstUnloadedGap`) until there are none — lazy loading leaves holes on purpose, and a hole under a
select-all is a hole in the middle of the copied text.
## Architecture
```
Input (keyboard / mouse / wheel / touch)
│
├─ keyboard ───────────────────────────────────────────────────────────────┐
│ when the active pane is in copy mode, keyboardActor.ts intercepts │
│ keydowns (preventDefault) and sends COPY_MODE_KEY; copyModeKeys.ts │
│ (handleCopyModeKey) resolves the vi motion/selection client-side. │
│ ▼
├─ wheel / touch / drag ───────────────────────────────┐ tmux (control mode)
│ usePaneMouse.ts / usePaneTouch.ts scroll the │ copy-mode, send-keys -X,
│ ScrollbackTerminal's native scroll container; │ capture-pane -p -e -S -E
│ onScroll → COPY_MODE_SCROLL. Mouse drag / double- │ │
│ / triple-click → COPY_MODE_* selection events. │ │
│ ▼ ▼
└─ appMachine.ts copyMode actions (actions/copyMode.ts) ◄───────── FETCH_SCROLLBACK_CELLS
own copyModeStates[paneId]: cursor, selection, (tmuxActor → adapter.invoke
scrollTop, and the loaded scrollback `lines` map. 'get_scrollback_cells')
│
▼
ScrollbackTerminal.tsx renders the loaded cells + the client cursor/selection into a tall,
natively-scrollable