a1b93332
fix: preserve native selection while scrolling history
a73x 2026-09-06 09:54
Commit message
RETRO.md
| Old | New | ||
|---|---|---|---|
| @@ -1320,3 +1320,46 @@ The localhost page is http://127.0.0.1:18776/wheel-scrolling/ ; publishing is no | |||
| 1320 | longer a pending action under the clarified workflow. Owned recording/browser | 1320 | longer a pending action under the clarified workflow. Owned recording/browser |
| 1321 | and compositor fixtures are stopped after validation; the local review server | 1321 | and compositor fixtures are stopped after validation; the local review server |
| 1322 | remains pending acceptance. | 1322 | remains pending acceptance. |
| 1323 | |||
| 1324 | |||
| 1325 | ### Wheel selection feedback — 2026-09-06 | ||
| 1326 | |||
| 1327 | Hands-on feedback corrected the initial cancellation policy: a completed | ||
| 1328 | highlight must follow selected text through viewport scrolling, including | ||
| 1329 | scrolling away and back. The pump already distinguishes absolute source rows | ||
| 1330 | from the viewport origin. Removed the three viewport-only invalidations and | ||
| 1331 | made input invalidation explicit in the mailbox; no new production state, | ||
| 1332 | modules or dependencies were needed (four fewer production lines). | ||
| 1333 | |||
| 1334 | Luna implemented the pump change; Terra independently reviewed freshness and | ||
| 1335 | input/resize/reconnect guards. Root corrected the regression fixture's history | ||
| 1336 | setup and verified actual pixels and independent Wayland clipboard reads. | ||
| 1337 | Held selection keeps its original anchor, with pointer motion/release resolving | ||
| 1338 | the endpoint in the current viewport. This intentionally permits extending a | ||
| 1339 | selection through scrolled history. | ||
| 1340 | |||
| 1341 | The user requested hands-on review instead of a new demo recording for this | ||
| 1342 | feedback fix. The existing recording is retained and labelled as preceding this | ||
| 1343 | change. Demo acceptance remains pending. | ||
| 1344 | |||
| 1345 | Lesson for the next mouse slice: distinguish source validity from viewport | ||
| 1346 | position; reuse the existing absolute selection coordinates. Keep the required | ||
| 1347 | Shift+drag override in that slice's acceptance criteria. | ||
| 1348 | |||
| 1349 | Validation: client/native unit tests and the full native integration gate pass. | ||
| 1350 | Real NVIDIA Wayland wheel/selection checks pass all ten checkpoints at the | ||
| 1351 | retained scale transitions; the separate ten-checkpoint copy suite also passes, | ||
| 1352 | including stale output, delayed replies, resize and detach. Logs are retained as | ||
| 1353 | `dist/wheel-scrolling/selection-preserve-*.log`. The first unit attempt exposed | ||
| 1354 | an incomplete test setup after returning live; restoring history fixed the | ||
| 1355 | fixture, and the final run passed. The isolated test compositor was stopped. | ||
| 1356 | This bounded feedback fix makes no new performance or macOS claim. | ||
| 1357 | |||
| 1358 | Repository `make check` passed inside the full `make ci` attempt. That attempt | ||
| 1359 | stopped in the existing terminal-client `agent-nested` exit wait (10 seconds), | ||
| 1360 | after the preceding e2e groups passed. The targeted `10_agent` rerun passed all | ||
| 1361 | six scenarios without source changes. Retain the failure rather than claiming | ||
| 1362 | a fully green CI run; the later e2e groups did not run in this attempt. Next | ||
| 1363 | trigger for the agent-test owner: investigate if this exit-wait timeout recurs. | ||
| 1364 | |||
| 1365 | The remaining agent and throughput gates passed in the follow-up run. | ||
docs/demos/native-wheel-scrolling.html
| Old | New | ||
|---|---|---|---|
| @@ -18,8 +18,9 @@ video{width:100%;display:block;background:#101114;border-radius:10px;margin:22px | |||
| 18 | <p class="status">Implemented · Final checks passed · Demo acceptance pending</p> | 18 | <p class="status">Implemented · Final checks passed · Demo acceptance pending</p> |
| 19 | <video controls playsinline preload="metadata" poster="preview.png"><source src="demo.mp4" type="video/mp4">Your browser can <a href="demo.mp4">download the recording</a>.</video> | 19 | <video controls playsinline preload="metadata" poster="preview.png"><source src="demo.mp4" type="video/mp4">Your browser can <a href="demo.mp4">download the recording</a>.</video> |
| 20 | <p class="muted">27-second continuous recording · NVIDIA RTX 3080 · Wayland at 200% scale · <a href="demo.mp4">Open video</a></p> | 20 | <p class="muted">27-second continuous recording · NVIDIA RTX 3080 · Wayland at 200% scale · <a href="demo.mp4">Open video</a></p> |
| 21 | <div class="cards"><div class="card"><strong>Shell history</strong>Three rows per notch. Scroll down to live output, or type to return immediately.</div><div class="card"><strong>Application input</strong>Alternate-screen arrows honor cursor-key mode; mouse-aware apps receive the encoding they request.</div><div class="card"><strong>History selection</strong>Drag to copy the displayed history rows. Scrolling that pane cancels an active selection safely.</div></div> | 21 | <div class="cards"><div class="card"><strong>Shell history</strong>Three rows per notch. Scroll down to live output, or type to return immediately.</div><div class="card"><strong>Application input</strong>Alternate-screen arrows honor cursor-key mode; mouse-aware apps receive the encoding they request.</div><div class="card"><strong>History selection</strong>Drag to copy the displayed history rows. The highlight follows its text when you scroll, including away and back.</div></div> |
| 22 | <h2>What the recording shows</h2> | 22 | <h2>What the recording shows</h2> |
| 23 | <p class="muted">The recording below predates the selection-preservation fix. That fix is ready for hands-on review; no replacement recording was requested.</p> | ||
| 23 | <p>Three panes on two daemons. Scroll an unfocused shell pane, select a history row, read its text with a separate desktop clipboard client, return to live output, then scroll a real less process. Pointer motion, selection and wheel events go through the Wayland compositor. Setup commands use the ordinary SDL keyboard event path.</p> | 24 | <p>Three panes on two daemons. Scroll an unfocused shell pane, select a history row, read its text with a separate desktop clipboard client, return to live output, then scroll a real less process. Pointer motion, selection and wheel events go through the Wayland compositor. Setup commands use the ordinary SDL keyboard event path.</p> |
| 24 | <h2>Controls</h2> | 25 | <h2>Controls</h2> |
| 25 | <ul><li>Wheel over terminal content to scroll that pane without moving keyboard focus.</li><li>At a shell prompt, wheel up browses history and wheel down returns toward live output. Typing into that pane returns it to live.</li><li>Drag across displayed text and release to copy; Ctrl+Shift+C also copies the current selection.</li><li>Menus, headers, dividers and command mode consume wheel events. Horizontal wheel behavior remains outside this slice.</li></ul> | 26 | <ul><li>Wheel over terminal content to scroll that pane without moving keyboard focus.</li><li>At a shell prompt, wheel up browses history and wheel down returns toward live output. Typing into that pane returns it to live.</li><li>Drag across displayed text and release to copy; Ctrl+Shift+C also copies the current selection.</li><li>Menus, headers, dividers and command mode consume wheel events. Horizontal wheel behavior remains outside this slice.</li></ul> |
docs/superpowers/plans/2026-09-06-native-wheel-scrolling.md
| Old | New | ||
|---|---|---|---|
| @@ -71,9 +71,10 @@ The earlier font-settings cleanup already supplies the useful opening refactor; | |||
| 71 | the current inspection found no additional cleanup needed before this feature. | 71 | the current inspection found no additional cleanup needed before this feature. |
| 72 | Application mouse encodings are checked against the | 72 | Application mouse encodings are checked against the |
| 73 | [xterm mouse protocol](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking). | 73 | [xterm mouse protocol](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking). |
| 74 | Selection on a pane whose viewport moves must cancel; a new selection in history | 74 | Selection stays attached to its source text when the viewport moves, including |
| 75 | must address the displayed history rows. Scrolling another pane must preserve | 75 | scrolling offscreen and back or returning through live view. A held drag keeps |
| 76 | the selected pane's text. These are part of wheel acceptance, not later polish. | 76 | its original text anchor and extends using the current pane coordinates. Source |
| 77 | changes still invalidate selection. These are part of wheel acceptance, not later polish. | ||
| 77 | 78 | ||
| 78 | `frame.zig` owns SDL events and logical-to-framebuffer conversion through | 79 | `frame.zig` owns SDL events and logical-to-framebuffer conversion through |
| 79 | `physicalPoint`. `interaction.Controller` owns pane hit-testing and modal policy; | 80 | `physicalPoint`. `interaction.Controller` owns pane hit-testing and modal policy; |
| @@ -234,3 +235,25 @@ Local review page: http://127.0.0.1:18776/wheel-scrolling/ . The loopback server | |||
| 234 | is recorded in `dist/wheel-scrolling/server.json`; stop it after acceptance. | 235 | is recorded in `dist/wheel-scrolling/server.json`; stop it after acceptance. |
| 235 | No remote publication is required or pending. Publishing is opt-in at the user's | 236 | No remote publication is required or pending. Publishing is opt-in at the user's |
| 236 | request. Sources, recording and all validation logs remain in the worktree. | 237 | request. Sources, recording and all validation logs remain in the worktree. |
| 238 | |||
| 239 | |||
| 240 | ### Hands-on feedback: keep the highlight while scrolling | ||
| 241 | |||
| 242 | The user expects a completed selection to remain attached to its source text, | ||
| 243 | including scrolling it offscreen and back. `Pump` now preserves selection | ||
| 244 | freshness through local viewport requests, history replies and return-to-live. | ||
| 245 | Input explicitly invalidates selection at the mailbox boundary; terminal output, | ||
| 246 | modes, geometry and attachment changes retain their existing guards. Existing | ||
| 247 | absolute selection rows and `Live.view_origin` already provide correct painting | ||
| 248 | and hit testing, so no new production state or abstraction is needed. | ||
| 249 | |||
| 250 | A held drag retains its original anchor; subsequent motion or release resolves | ||
| 251 | its endpoint against the current viewport. The real Wayland regression covers | ||
| 252 | both a release on the moved source row and extending across newly visible rows. | ||
| 253 | The user requested hands-on review instead of a new demo recording for this fix. | ||
| 254 | |||
| 255 | Client/native units, the full native integration suite and real NVIDIA Wayland | ||
| 256 | wheel/copy regressions passed. The repository check gate passed; full CI stopped | ||
| 257 | on an existing nested-agent exit timeout, which passed on targeted rerun without | ||
| 258 | changes. Full-run CI success is therefore not claimed for this feedback commit. | ||
| 259 | Evidence: `dist/wheel-scrolling/selection-preserve-*.log`; details in `RETRO.md`. | ||
src/client/session_pump.zig
| Old | New | ||
|---|---|---|---|
| @@ -212,8 +212,6 @@ pub const Pump = struct { | |||
| 212 | self.scroll_rows = 0; | 212 | self.scroll_rows = 0; |
| 213 | self.history_dirty = false; | 213 | self.history_dirty = false; |
| 214 | self.history_revision +%= 1; | 214 | self.history_revision +%= 1; |
| 215 | self.selection_revision +%= 1; | ||
| 216 | self.invalidateSelectionLocked(); | ||
| 217 | if (self.history) |g| g.deinit(); | 215 | if (self.history) |g| g.deinit(); |
| 218 | self.history = null; | 216 | self.history = null; |
| 219 | } | 217 | } |
| @@ -254,8 +252,6 @@ pub const Pump = struct { | |||
| 254 | if (self.history) |old| old.deinit(); | 252 | if (self.history) |old| old.deinit(); |
| 255 | self.history = view; | 253 | self.history = view; |
| 256 | self.history_start = origin; | 254 | self.history_start = origin; |
| 257 | self.selection_revision +%= 1; | ||
| 258 | self.invalidateSelectionLocked(); | ||
| 259 | self.history_version = .{ .seq = self.replica.last_seq, .history_rows = self.replica.history_rows, .epoch = self.replica.session_epoch, .revision = self.selection_revision }; | 255 | self.history_version = .{ .seq = self.replica.last_seq, .history_rows = self.replica.history_rows, .epoch = self.replica.session_epoch, .revision = self.selection_revision }; |
| 260 | return .changed; | 256 | return .changed; |
| 261 | } | 257 | } |
| @@ -430,6 +426,8 @@ pub const Pump = struct { | |||
| 430 | .input => |bytes| { | 426 | .input => |bytes| { |
| 431 | self.mu.lock(); | 427 | self.mu.lock(); |
| 432 | const scrolled = self.scroll_rows != 0; | 428 | const scrolled = self.scroll_rows != 0; |
| 429 | self.selection_revision +%= 1; | ||
| 430 | self.invalidateSelectionLocked(); | ||
| 433 | self.returnLiveLocked(); | 431 | self.returnLiveLocked(); |
| 434 | self.mu.unlock(); | 432 | self.mu.unlock(); |
| 435 | if (scrolled) self.wake(); | 433 | if (scrolled) self.wake(); |
| @@ -476,8 +474,6 @@ pub const Pump = struct { | |||
| 476 | self.scroll_rows = next; | 474 | self.scroll_rows = next; |
| 477 | self.history_revision +%= 1; | 475 | self.history_revision +%= 1; |
| 478 | self.history_dirty = true; | 476 | self.history_dirty = true; |
| 479 | self.selection_revision +%= 1; | ||
| 480 | self.invalidateSelectionLocked(); | ||
| 481 | } | 477 | } |
| 482 | } | 478 | } |
| 483 | self.mu.unlock(); | 479 | self.mu.unlock(); |
| @@ -1349,7 +1345,10 @@ fn selectionTestBegin(p: *Pump, req: SelectionRequest) ?[proto.selection_req_len | |||
| 1349 | fn selectionTestReply(p: *Pump, id: u32, text: []const u8) !void { | 1345 | fn selectionTestReply(p: *Pump, id: u32, text: []const u8) !void { |
| 1350 | var payload: std.ArrayList(u8) = .empty; | 1346 | var payload: std.ArrayList(u8) = .empty; |
| 1351 | defer payload.deinit(std.testing.allocator); | 1347 | defer payload.deinit(std.testing.allocator); |
| 1352 | try proto.encodeSelectionReply(&payload, std.testing.allocator, id, .ok, 0, text); | 1348 | p.mu.lock(); |
| 1349 | const history_rows = p.replica.history_rows; | ||
| 1350 | p.mu.unlock(); | ||
| 1351 | try proto.encodeSelectionReply(&payload, std.testing.allocator, id, .ok, history_rows, text); | ||
| 1353 | _ = try p.onFrame(.selection_reply, payload.items); | 1352 | _ = try p.onFrame(.selection_reply, payload.items); |
| 1354 | @memset(payload.items, 0xaa); // The published result must own its text. | 1353 | @memset(payload.items, 0xaa); // The published result must own its text. |
| 1355 | } | 1354 | } |
| @@ -1431,7 +1430,9 @@ test "wheel history tombstones, refresh, resize and timeout preserve the live re | |||
| 1431 | var wire = try Wire.init(std.testing.allocator, &tr); | 1430 | var wire = try Wire.init(std.testing.allocator, &tr); |
| 1432 | defer wire.deinit(); | 1431 | defer wire.deinit(); |
| 1433 | const up: Wheel = .{ .notches = 1, .col = 2, .row = 1, .pixel_x = 22, .pixel_y = 18 }; | 1432 | const up: Wheel = .{ .notches = 1, .col = 2, .row = 1, .pixel_x = 22, .pixel_y = 18 }; |
| 1433 | const source_version = p.selectionVersionLocked(); | ||
| 1434 | try p.routeWheel(&wire, up); | 1434 | try p.routeWheel(&wire, up); |
| 1435 | try std.testing.expect(p.selectionFresh(source_version)); | ||
| 1435 | try p.requestHistory(&wire); | 1436 | try p.requestHistory(&wire); |
| 1436 | const first = (try proto.readFrame(std.testing.allocator, outgoing[0])).?; | 1437 | const first = (try proto.readFrame(std.testing.allocator, outgoing[0])).?; |
| 1437 | defer first.deinit(std.testing.allocator); | 1438 | defer first.deinit(std.testing.allocator); |
| @@ -1443,7 +1444,9 @@ test "wheel history tombstones, refresh, resize and timeout preserve the live re | |||
| 1443 | const typed = (try proto.readFrame(std.testing.allocator, outgoing[0])).?; | 1444 | const typed = (try proto.readFrame(std.testing.allocator, outgoing[0])).?; |
| 1444 | defer typed.deinit(std.testing.allocator); | 1445 | defer typed.deinit(std.testing.allocator); |
| 1445 | try std.testing.expectEqualStrings("live", typed.payload); | 1446 | try std.testing.expectEqualStrings("live", typed.payload); |
| 1447 | try std.testing.expect(!p.selectionFresh(source_version)); | ||
| 1446 | try std.testing.expect(p.history_pending != null); // Keep the cancelled on-wire request. | 1448 | try std.testing.expect(p.history_pending != null); // Keep the cancelled on-wire request. |
| 1449 | const after_input = p.selectionVersionLocked(); | ||
| 1447 | try p.routeWheel(&wire, up); | 1450 | try p.routeWheel(&wire, up); |
| 1448 | try p.requestHistory(&wire); | 1451 | try p.requestHistory(&wire); |
| 1449 | try std.testing.expectEqual(revision, p.history_pending.?.revision); | 1452 | try std.testing.expectEqual(revision, p.history_pending.?.revision); |
| @@ -1458,7 +1461,23 @@ test "wheel history tombstones, refresh, resize and timeout preserve the live re | |||
| 1458 | try std.testing.expectEqual(@as(u32, 17), p.viewOriginLocked()); | 1461 | try std.testing.expectEqual(@as(u32, 17), p.viewOriginLocked()); |
| 1459 | try std.testing.expect(p.viewGridLocked() != p.grid); | 1462 | try std.testing.expect(p.viewGridLocked() != p.grid); |
| 1460 | try std.testing.expectEqual(@as(u16, 3), p.viewGridLocked().cursor.y); | 1463 | try std.testing.expectEqual(@as(u16, 3), p.viewGridLocked().cursor.y); |
| 1461 | try std.testing.expect(p.selectionFresh(p.selectionVersionLocked())); | 1464 | try std.testing.expect(p.selectionFresh(after_input)); |
| 1465 | const history_version = p.selectionVersionLocked(); | ||
| 1466 | _ = selectionTestBegin(p, try selectionTestQueue(p, 41)).?; | ||
| 1467 | p.mu.lock(); | ||
| 1468 | p.returnLiveLocked(); | ||
| 1469 | p.mu.unlock(); | ||
| 1470 | try selectionTestReply(p, 41, "kept through viewport"); | ||
| 1471 | const kept = p.takeSelection().?; | ||
| 1472 | defer std.testing.allocator.free(kept.text); | ||
| 1473 | try std.testing.expectEqualStrings("kept through viewport", kept.text); | ||
| 1474 | try std.testing.expectEqualDeep(history_version, kept.version); | ||
| 1475 | try p.routeWheel(&wire, up); | ||
| 1476 | try p.requestHistory(&wire); | ||
| 1477 | const restored = (try proto.readFrame(std.testing.allocator, outgoing[0])).?; | ||
| 1478 | defer restored.deinit(std.testing.allocator); | ||
| 1479 | _ = try p.onFrame(.scrollback_chunk, &chunk); | ||
| 1480 | try std.testing.expect(p.selectionFresh(history_version)); | ||
| 1462 | var newer = testSnapshot(); | 1481 | var newer = testSnapshot(); |
| 1463 | proto.writeSnapshotPrefix(newer[0..proto.snapshot_prefix_len], .{ .seq = 38, .history_rows = 20, .cols = 11, .rows = 3, .epoch = 93 }); | 1482 | proto.writeSnapshotPrefix(newer[0..proto.snapshot_prefix_len], .{ .seq = 38, .history_rows = 20, .cols = 11, .rows = 3, .epoch = 93 }); |
| 1464 | _ = try p.onFrame(.snapshot, &newer); | 1483 | _ = try p.onFrame(.snapshot, &newer); |
test/native_wheel.py
| Old | New | ||
|---|---|---|---|
| @@ -106,21 +106,68 @@ def shell_history(rig, panes): | |||
| 106 | rig.wheel(target, 4) | 106 | rig.wheel(target, 4) |
| 107 | state = rig.wait_state(lambda s: first_number(by_id(s)[target]['painted_text']) == baseline - 12) | 107 | state = rig.wait_state(lambda s: first_number(by_id(s)[target]['painted_text']) == baseline - 12) |
| 108 | first = by_id(state)[target]['painted_text'].splitlines()[0] | 108 | first = by_id(state)[target]['painted_text'].splitlines()[0] |
| 109 | background = cell_background(rig, state, target, 1, 0) | ||
| 109 | rig.select(target, (0, 0), (11, 0)) | 110 | rig.select(target, (0, 0), (11, 0)) |
| 110 | rig.copied(first[:12]) | 111 | rig.copied(first[:12]) |
| 112 | eventually(lambda: cell_background(rig, state, target, 1, 0) != background, | ||
| 113 | 'history selection did not paint') | ||
| 111 | rig.wheel(target, 1) | 114 | rig.wheel(target, 1) |
| 112 | rig.wait_state(lambda s: first_number(by_id(s)[target]['painted_text']) == baseline - 15) | 115 | rig.wait_state(lambda s: first_number(by_id(s)[target]['painted_text']) == baseline - 15) |
| 116 | eventually(lambda: cell_background(rig, state, target, 1, 3) != background and | ||
| 117 | cell_background(rig, state, target, 1, 0) == background, | ||
| 118 | 'highlight did not follow its text three rows down') | ||
| 119 | rig.wheel(target, 1000) | ||
| 120 | rig.wait_state(lambda s: 'HISTORY-0000' in by_id(s)[target]['painted_text']) | ||
| 121 | eventually(lambda: all(cell_background(rig, state, target, 1, row) == background | ||
| 122 | for row in range(by_id(state)[target]['rows'])), | ||
| 123 | 'offscreen selection left a highlight in the viewport') | ||
| 124 | rig.wheel(target, -1000) | ||
| 125 | rig.wait_state(lambda s: by_id(s)[target]['painted_text'] == live[target]) | ||
| 126 | rig.wheel(target, 4) | ||
| 127 | rig.wait_state(lambda s: first_number(by_id(s)[target]['painted_text']) == baseline - 12) | ||
| 128 | eventually(lambda: cell_background(rig, state, target, 1, 0) != background, | ||
| 129 | 'highlight did not return with selected history text') | ||
| 130 | rig.key('copy') | ||
| 113 | rig.unchanged(first[:12]) | 131 | rig.unchanged(first[:12]) |
| 132 | rig.ok('released history highlight follows text, survives offscreen/live round trips and remains copyable') | ||
| 133 | |||
| 134 | # Keep the original anchor while a held drag moves through history. Release | ||
| 135 | # at its new screen row to copy the same source row, then repeat with an | ||
| 136 | # explicit motion to extend the selection across the newly visible rows. | ||
| 137 | held = by_id(rig.state())[target]['painted_text'].splitlines()[1] | ||
| 138 | rig.select(target, (0, 1), (11, 1), release=False) | ||
| 139 | rig.wheel(target, 1, col=11, row=1) | ||
| 140 | rig.wait_state(lambda s: first_number(by_id(s)[target]['painted_text']) == baseline - 15) | ||
| 141 | rig.send('mouseup:' + rig.cell_point(state, target, 11, 4)) | ||
| 142 | rig.copied(held[:12]) | ||
| 114 | rig.select(target, (0, 0), (11, 0), release=False) | 143 | rig.select(target, (0, 0), (11, 0), release=False) |
| 115 | held_state = rig.state() | 144 | rig.wheel(target, 1, col=11, row=0) |
| 145 | moved = rig.wait_state(lambda s: first_number(by_id(s)[target]['painted_text']) == baseline - 18) | ||
| 146 | lines = by_id(moved)[target]['painted_text'].splitlines() | ||
| 147 | rig.send('mousemove:' + rig.cell_point(moved, target, 11, 4)) | ||
| 148 | rig.key('copy') | ||
| 149 | rig.copied(lines[3] + '\n' + lines[4][:12]) | ||
| 150 | rig.send('mouseup:' + rig.cell_point(moved, target, 11, 4)) | ||
| 151 | rig.copied(lines[3] + '\n' + lines[4][:12]) | ||
| 152 | rig.ok('held drag retains its text anchor and extends using the scrolled pane coordinates') | ||
| 153 | |||
| 154 | rig.wheel(target, -1000) | ||
| 155 | rig.wait_state(lambda s: by_id(s)[target]['painted_text'] == live[target]) | ||
| 156 | rig.select(target, (0, 0), (11, 0)) | ||
| 157 | rig.copied(live[target].splitlines()[0][:12]) | ||
| 116 | rig.wheel(target, 1) | 158 | rig.wheel(target, 1) |
| 117 | rig.wait_state(lambda s: first_number(by_id(s)[target]['painted_text']) == baseline - 18) | 159 | rig.wait_state(lambda s: first_number(by_id(s)[target]['painted_text']) == baseline - 3) |
| 118 | rig.send('mouseup:' + rig.cell_point(held_state, target, 11, 0)) | 160 | eventually(lambda: cell_background(rig, state, target, 1, 3) != background, |
| 119 | rig.unchanged(first[:12]) | 161 | 'live selection disappeared on entering history') |
| 120 | rig.focus(target) | 162 | rig.wheel(target, -1) |
| 163 | rig.wait_state(lambda s: by_id(s)[target]['painted_text'] == live[target]) | ||
| 164 | eventually(lambda: cell_background(rig, state, target, 1, 0) != background, | ||
| 165 | 'live selection disappeared on returning from history') | ||
| 121 | rig.shell("printf 'RETURN-%s\\n' LIVE") | 166 | rig.shell("printf 'RETURN-%s\\n' LIVE") |
| 122 | rig.wait_state(lambda s: 'RETURN-LIVE' in by_id(s)[target]['painted_text']) | 167 | rig.wait_state(lambda s: 'RETURN-LIVE' in by_id(s)[target]['painted_text']) |
| 123 | rig.ok('history bounds, history selection/copy, held-drag cancellation and typing back to live') | 168 | eventually(lambda: cell_background(rig, state, target, 1, 0) == background, |
| 169 | 'input failed to clear the selection') | ||
| 170 | rig.ok('live selection survives history navigation; typing returns live and clears it') | ||
| 124 | 171 | ||
| 125 | 172 | ||
| 126 | def raw_reader(rig, pane): | 173 | def raw_reader(rig, pane): |