a73x

b5011067

Add visible selection design spec

a73x   2026-04-09 17:02

Commit message
Add visible selection design spec

docs/superpowers/specs/2026-04-09-visible-selection-design.md
Old New
@@ -0,0 +1,214 @@
1 # Visible Selection Design
2
3 ## Goal
4
5 Add XTerm-style text selection for the visible terminal grid so users can drag to highlight text and press `Ctrl+Shift+C` to copy it to the Wayland clipboard.
6
7 ## Scope
8
9 This change covers only text currently visible in the terminal grid.
10
11 Included:
12 - Left-button drag selection across visible cells
13 - Persistent visible highlight after mouse release
14 - `Ctrl+Shift+C` copying the selected text to the clipboard
15 - Clearing or replacing the current visible selection with a new drag
16
17 Excluded:
18 - Scrollback selection
19 - Auto-copy on select
20 - Primary selection / middle-click paste
21 - Word-wise or line-wise selection modes
22 - Keyboard-driven selection
23
24 ## Current Context
25
26 `waystty` already has:
27 - A `ghostty-vt` backed terminal model exposed through `src/vt.zig`
28 - Per-frame terminal snapshots via `term.snapshot()`
29 - Row/cell instance generation in `src/main.zig`
30 - Wayland clipboard paste support in `src/wayland.zig`
31
32 `waystty` does not yet have:
33 - Wayland pointer handling
34 - Any selection model
35 - Clipboard ownership for serving copied text to other clients
36
37 The visible-grid-only scope is intentionally chosen because the current renderer already operates on a snapshot of the visible terminal state each frame. That gives a stable source for both highlight decisions and copy extraction without introducing scrollback traversal.
38
39 ## Approach Options
40
41 ### 1. UI-owned visible selection state in `main.zig` (recommended)
42
43 Add a `SelectionState` alongside the main event loop. Pointer events update selection coordinates in grid space. Rendering checks whether each visible cell falls inside the active selection and swaps to selection colors. Copy walks the visible snapshot and exports the selected text through Wayland clipboard ownership.
44
45 Pros:
46 - Smallest change set
47 - Matches existing separation where `main.zig` owns window/UI behavior
48 - Keeps `vt.zig` focused on terminal emulation rather than GUI state
49 - Easiest path to visible-only selection
50
51 Cons:
52 - If selection later expands into scrollback/search semantics, some logic may later move into a more dedicated module
53
54 ### 2. Terminal-wrapper-owned selection in `src/vt.zig`
55
56 Store selection state beside the `ghostty-vt` wrapper and expose helper APIs for rendering and extraction.
57
58 Pros:
59 - Centralizes terminal-text operations
60
61 Cons:
62 - Blurs the current boundary between VT state and window/input state
63 - Adds API surface before the project needs it
64
65 ### 3. Renderer overlay plus separate extraction pass
66
67 Render selection as an independent overlay and keep copy generation in a separate traversal.
68
69 Pros:
70 - Can support more advanced visuals later
71
72 Cons:
73 - Adds rendering complexity immediately
74 - Does not simplify copy logic
75 - Overbuilt for the current terminal
76
77 ## Recommended Design
78
79 ### Architecture
80
81 Selection state will live in `src/main.zig` as part of the Wayland/UI event loop. The state will store:
82 - Whether a drag is active
83 - The anchor cell where the drag started
84 - The current drag cell
85 - The last committed visible selection span, if any
86
87 Selection coordinates are grid-relative `(col, row)` positions clamped to the current visible terminal dimensions.
88
89 Pointer support will be added in `src/wayland.zig` and exposed similarly to the existing keyboard queue model: collect pointer button/motion events into a queue that `main.zig` drains each loop iteration.
90
91 Clipboard support in `src/wayland.zig` will be extended so `waystty` can become the current clipboard source and serve a UTF-8 payload to other Wayland clients.
92
93 ### Interaction Model
94
95 - Left button press starts a new selection anchored at the hovered cell.
96 - Pointer motion with the left button held updates the active selection end.
97 - Left button release finalizes the selection and leaves it highlighted.
98 - A click-drag over a new region replaces any old selection.
99 - `Ctrl+Shift+C` copies the current selection if one exists.
100 - A left click that does not create a span still resets the anchor; if press and release stay on the same cell, the resulting selection is empty and no highlight remains.
101
102 This is intentionally narrower than full XTerm behavior. It provides the core visible-drag selection loop without word-select, line-select, or primary selection side effects.
103
104 ### Selection Semantics
105
106 The first implementation will treat selection as a linear text span over visible cells, not a rectangular block selection.
107
108 Normalization rules:
109 - If the drag end precedes the anchor in reading order, normalize the start/end positions before use.
110 - Selection includes both start and end cells.
111 - Rows between start and end are included fully.
112 - On the first row, include cells from `start.col` through the row end.
113 - On the last row, include cells from column `0` through `end.col`.
114
115 This matches the usual terminal expectation for drag selection better than rectangular selection.
116
117 ### Rendering
118
119 Highlighting will be applied during existing cell instance generation in `src/main.zig`.
120
121 For each rendered cell:
122 - Determine whether its `(row, col)` falls inside the normalized selection span.
123 - If selected, override fg/bg colors with dedicated selection colors.
124 - Keep glyph placement unchanged.
125
126 This avoids adding a second draw pass or renderer-specific overlay state.
127
128 Initial colors:
129 - Selection background: a light neutral accent that stays legible against the terminal theme
130 - Selection foreground: dark text for contrast
131
132 If there is already an established color constant pattern nearby, follow it there. Otherwise define a small pair of constants near the selection helpers in `main.zig`.
133
134 ### Copy Extraction
135
136 `Ctrl+Shift+C` will trigger selection export only when a non-empty selection exists.
137
138 Copy extraction will:
139 - Read from the same visible snapshot used for rendering
140 - Walk selected rows in reading order
141 - Append selected cell text into a UTF-8 buffer
142 - Insert `\n` between selected rows
143 - Trim trailing blank cells on each selected row before inserting row text
144
145 This keeps copied text aligned with what users typically expect from terminal selections: visible text content without right-margin padding from empty cells.
146
147 If a cell does not produce a printable glyph in the visible snapshot, it contributes nothing to the copied buffer.
148
149 ### Resizing and Lifecycle
150
151 - If the terminal grid size changes, clamp or clear the selection if it falls outside the new visible bounds.
152 - If a snapshot update causes content changes while a selection is present, the selection remains attached to visible grid coordinates, not stable text identity.
153 - If the window loses the selection due to a new empty click, the highlight disappears immediately.
154
155 This is acceptable for the visible-only version because the selection is explicitly a UI overlay on the current grid, not a scrollback-aware logical text range.
156
157 ## Components To Change
158
159 ### `src/wayland.zig`
160
161 Add:
162 - Pointer object setup from the seat
163 - Pointer event structs and queue
164 - Surface enter/leave tracking if needed for hover validity
165 - Clipboard source support for sending copied UTF-8 text to requesters
166
167 ### `src/main.zig`
168
169 Add:
170 - Selection state types and helpers
171 - Pointer queue handling in the main loop
172 - Grid-coordinate mapping from surface pixels to terminal cells
173 - Copy shortcut detection for `Ctrl+Shift+C`
174 - Selected-text extraction from the visible snapshot
175 - Selection-aware color override during row instance generation
176
177 ### `src/vt.zig`
178
179 Likely no structural change beyond possible helper exposure if snapshot cell text extraction needs a small convenience wrapper. Avoid moving selection ownership here.
180
181 ## Error Handling
182
183 - Clipboard ownership failures must not crash the terminal; log a warning and leave the selection intact.
184 - Pointer events outside the surface bounds should clamp to the nearest visible cell or be ignored when no valid hover target exists.
185 - Empty selections should not attempt clipboard export.
186
187 ## Testing Strategy
188
189 Add unit tests for:
190 - Selection span normalization
191 - Inclusion checks for cells inside/outside a span
192 - Selected-text extraction across single-row and multi-row selections
193 - Copy shortcut detection for `Ctrl+Shift+C`
194
195 Manual verification after implementation:
196 - Drag left-to-right within one row and copy
197 - Drag across multiple rows and copy
198 - Drag right-to-left and verify normalized selection
199 - Resize after selection and verify behavior stays safe
200 - Paste copied text into another app through the Wayland clipboard
201
202 ## Risks
203
204 - `ghostty-vt` render cells may require careful handling to recover copied text exactly as shown, especially around wide glyphs or style-only cells.
205 - Wayland clipboard source handling is more involved than clipboard receive and must be wired so request lifetimes are correct.
206 - Pointer coordinate mapping must use surface coordinates rather than buffer-scaled pixels so HiDPI behavior stays correct.
207
208 ## Success Criteria
209
210 The feature is complete when:
211 - Users can drag-select visible terminal text with the left mouse button
212 - Selected cells are visibly highlighted
213 - `Ctrl+Shift+C` copies the highlighted visible text to the clipboard
214 - The terminal remains stable across resize and HiDPI scale changes