grid, protocol, daemon, client, docs: keep a scrolled-back view on its lines as output arrives #169

Merged
natsukium merged 1 commit from daemon/viewport-anchor into main 2026-10-18 17:42:06 +09:00
Owner

A scrolled-back window stays on the lines it shows while the program keeps printing, and the next scroll moves one line from them instead of jumping by the lines printed in between.

  • felis-grid: Grid::viewport_anchor() / anchor_viewport(offset, since) grow a browsing offset by the rows pushed into scrollback, clamped to the retained history; 0 stays live. A resize, an erasing CSI 3 J, RIS or a screen switch moves the anchor's generation and returns the view to live. Embedders holding their own offset (GitHub mirror issue 4) call the same function.
  • daemon: re-anchors each subscriber's offset every compose; browse rows are resent only when the top line or generation changes, so pure growth ships only a ViewportState.
  • wire (minor 2, skippable): GridViewportState.anchor, echoed in InputViewport.anchor, so a step computed while a frame was in flight still lands one line away; SearchMatch.anchor lets the client keep highlights and n/N navigation on their lines. An unanchored request counts from the current bottom, as before.
  • client: echoes the anchor, re-expresses search hits, keeps a selection across growth-only states (and clears it when a browse ends), and restores pipe/run returns with their saved anchor.

A step sent before the previous reply arrives is out of scope and tracked in #164.

Design and implementation each passed a sol review (pi-review); just check passes locally.

Closes #158

A scrolled-back window stays on the lines it shows while the program keeps printing, and the next scroll moves one line from them instead of jumping by the lines printed in between. - **felis-grid**: `Grid::viewport_anchor()` / `anchor_viewport(offset, since)` grow a browsing offset by the rows pushed into scrollback, clamped to the retained history; `0` stays live. A resize, an erasing `CSI 3 J`, RIS or a screen switch moves the anchor's generation and returns the view to live. Embedders holding their own offset (GitHub mirror issue 4) call the same function. - **daemon**: re-anchors each subscriber's offset every compose; browse rows are resent only when the top line or generation changes, so pure growth ships only a `ViewportState`. - **wire (minor 2, skippable)**: `GridViewportState.anchor`, echoed in `InputViewport.anchor`, so a step computed while a frame was in flight still lands one line away; `SearchMatch.anchor` lets the client keep highlights and n/N navigation on their lines. An unanchored request counts from the current bottom, as before. - **client**: echoes the anchor, re-expresses search hits, keeps a selection across growth-only states (and clears it when a browse ends), and restores pipe/run returns with their saved anchor. A step sent before the previous reply arrives is out of scope and tracked in #164. Design and implementation each passed a sol review (`pi-review`); `just check` passes locally. Closes #158
grid, protocol, daemon, client, docs: keep a scrolled-back view on its lines as output arrives
Some checks failed
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 8s
darwin / darwin docs-only no-op (pull_request) Has been skipped
fuzz / cargo fuzz smoke (per target) (pull_request) Has been cancelled
pr / nix flake check (pull_request) Has been cancelled
pr / cargo build / clippy / test / deny (pull_request) Has been cancelled
pr / wire schema is compatible with the base (pull_request) Has been cancelled
pr / frontend smoke (x86_64-linux) (pull_request) Has been cancelled
darwin / build felis (aarch64-darwin) (pull_request) Has been cancelled
windows / detect relevant changes (pull_request) Has been cancelled
windows / cargo clippy (Windows cross) (pull_request) Has been cancelled
windows / cargo nextest (Windows) (pull_request) Has been cancelled
windows / frontend smoke (Windows) (pull_request) Has been cancelled
windows / windows docs-only no-op (pull_request) Has been cancelled
bench / instruction-count regression gate (pull_request) Has been cancelled
074e356053
While a window was scrolled back and the program kept printing, the
daemon kept the subscriber's lines_from_bottom fixed. The rows on screen
stayed the ones composed at the old bottom, but the offset they were
composed at now named lines that many rows newer, so the next scroll
step, computed from that offset, landed as many lines away from what
the user was reading.

The offset now grows by the rows pushed into scrollback, clamped to the
retained history, as kitty grows scrolled_by and alacritty its display
offset; 0 stays the live bottom. A resize, an erasing CSI 3 J, RIS or a
screen switch returns a browsing view to live: each stops a push count
naming the same lines, and a generation counter records it even when one
burst erases history and refills it before the next compose, where a
clamp would land on unrelated rows. The rule lives in felis-grid
(Grid::viewport_anchor / anchor_viewport) so an embedder that holds its
own offset calls the same function. The daemon re-anchors each
subscriber's offset every compose and resends browse rows only when the
top line or the generation changed, so pure growth ships a
ViewportState and no rows.

Reporting the grown offset alone leaves a race: under pull pacing a
frame carrying it is usually in flight while the user scrolls, and on a
remote attachment the round trip exceeds a frame, so a step computed
from the previous offset would be read against the newer bottom.
ViewportState therefore carries the ViewportAnchor its offset is
expressed against (pushed rows plus that generation, present only
while browsing), and InputViewport echoes it; an unanchored request
counts from the current bottom as before, and 0 is live whatever its
anchor. Search matches carry the anchor they were found at, so the
client re-expresses highlights against the view it shows and navigates
to a hit in the hit's own frame. A selection survives a growth-only
state and is cleared when the view leaves a browse, whose visible-row
coordinates then name rows it never covered; a pipe/run return restores
the saved offset with its saved anchor. The fields are skippable
minor-2 additions; minor 2 is still unreleased.

A step sent before the reply to the previous one arrives still starts
from the acknowledged offset; that is independent of this change and
tracked in #164.

Closes #158

Assisted-by: Claude Code
natsukium force-pushed daemon/viewport-anchor from 074e356053
Some checks failed
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 8s
darwin / darwin docs-only no-op (pull_request) Has been skipped
fuzz / cargo fuzz smoke (per target) (pull_request) Has been cancelled
pr / nix flake check (pull_request) Has been cancelled
pr / cargo build / clippy / test / deny (pull_request) Has been cancelled
pr / wire schema is compatible with the base (pull_request) Has been cancelled
pr / frontend smoke (x86_64-linux) (pull_request) Has been cancelled
darwin / build felis (aarch64-darwin) (pull_request) Has been cancelled
windows / detect relevant changes (pull_request) Has been cancelled
windows / cargo clippy (Windows cross) (pull_request) Has been cancelled
windows / cargo nextest (Windows) (pull_request) Has been cancelled
windows / frontend smoke (Windows) (pull_request) Has been cancelled
windows / windows docs-only no-op (pull_request) Has been cancelled
bench / instruction-count regression gate (pull_request) Has been cancelled
to 0805a6a1d3
Some checks failed
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 8s
darwin / darwin docs-only no-op (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Successful in 1m19s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m9s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m14s
pr / nix flake check (pull_request) Successful in 46s
pr / cargo build / clippy / test / deny (pull_request) Successful in 2m56s
pr / wire schema is compatible with the base (pull_request) Successful in 4s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 1m20s
windows / cargo clippy (Windows cross) (pull_request) Successful in 38s
windows / detect relevant changes (pull_request) Successful in 23s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / cargo nextest (Windows) (pull_request) Has started running
windows / frontend smoke (Windows) (pull_request) Has been cancelled
2026-10-18 15:32:20 +09:00
Compare
natsukium scheduled this pull request to auto merge when all checks succeed 2026-10-18 15:34:32 +09:00
natsukium force-pushed daemon/viewport-anchor from 0805a6a1d3
Some checks failed
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 8s
darwin / darwin docs-only no-op (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Successful in 1m19s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m9s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m14s
pr / nix flake check (pull_request) Successful in 46s
pr / cargo build / clippy / test / deny (pull_request) Successful in 2m56s
pr / wire schema is compatible with the base (pull_request) Successful in 4s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 1m20s
windows / cargo clippy (Windows cross) (pull_request) Successful in 38s
windows / detect relevant changes (pull_request) Successful in 23s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / cargo nextest (Windows) (pull_request) Has started running
windows / frontend smoke (Windows) (pull_request) Has been cancelled
to 7974260360
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 9s
darwin / darwin docs-only no-op (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Successful in 52s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m12s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m5s
pr / nix flake check (pull_request) Successful in 45s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m43s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 33s
windows / cargo clippy (Windows cross) (pull_request) Successful in 17s
windows / detect relevant changes (pull_request) Successful in 24s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / cargo nextest (Windows) (pull_request) Successful in 9m12s
windows / frontend smoke (Windows) (pull_request) Successful in 2m42s
2026-10-18 17:23:09 +09:00
Compare
natsukium deleted branch daemon/viewport-anchor 2026-10-18 17:42:07 +09:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
felis-terminal/felis!169
No description provided.