Skip to content

Broaden anchor-window clamp to CUP-park on empty trailing row (#157)#159

Merged
dakra merged 1 commit intomainfrom
fix-157-cup-park-empty-row
Apr 21, 2026
Merged

Broaden anchor-window clamp to CUP-park on empty trailing row (#157)#159
dakra merged 1 commit intomainfrom
fix-157-cup-park-empty-row

Conversation

@dakra
Copy link
Copy Markdown
Owner

@dakra dakra commented Apr 20, 2026

Summary

Fixes the #157 variant of #138: TUIs that CUP-park their cursor onto an empty trailing row (e.g. Claude Code on focus loss per the reporter's evidence) hit the same `pt = point-max` / window-start-shift symptom that #138 fixed, but without pending-wrap — so the `ad8536e` clamp narrowing lets them through.

Adds a second terminal-side predicate `ghostel--cursor-on-empty-row-p` (backed by a new `render.isRowEmptyAt` helper) and broadens the `ghostel--anchor-window` clamp guard to `(or pending-wrap cursor-on-empty-row)`. Predicate returns t iff the cursor's row has no written cells and no non-default styling — exactly when `buildRowContent` produces `byte_len == 0`.

Why not `pos-visible-in-window-p` (the approach in the now-closed #158): it reflects the previous redisplay, not the `window-start` we just pinned under `inhibit-redisplay`, and returns nil in batch — breaks existing tests. Terminal-side predicate answers the question without needing redisplay state.

Module version bumped 0.16.2 → 0.16.3 (new exported function).

Test plan

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a regression variant of the anchor-window scroll/clamp behavior (#157) by expanding the clamp guard beyond pending-wrap to also handle TUIs that CUP-position the cursor onto an empty trailing row, which can otherwise leave pt == point-max and trigger redisplay to scroll window-start.

Changes:

  • Add a native render helper (render.isRowEmptyAt) and new exported module predicate ghostel--cursor-on-empty-row-p.
  • Broaden ghostel--anchor-window’s clamp condition to (or pending-wrap cursor-on-empty-row).
  • Add ERT coverage for the new predicate and for the #157 clamp/no-clamp scenarios; bump module/package version to 0.16.3.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
ghostel.el Expands the clamp condition in ghostel--anchor-window and updates module version/min-version + function declaration.
src/render.zig Implements isRowEmptyAt to detect rows that would render as empty buffer lines.
src/module.zig Exports ghostel--cursor-on-empty-row-p and bumps native module version.
src/ghostty.zig Adds DATA_CURSOR_Y constant binding used by the new predicate.
test/ghostel-test.el Adds unit/regression tests for empty-row predicate and anchor-window clamp behavior.
build.zig.zon Bumps package version to 0.16.3.
evil-ghostel.el Bumps package version to 0.16.3.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ghostel.el
Comment thread src/module.zig Outdated
dakra added a commit that referenced this pull request Apr 20, 2026
Addresses Copilot review on #159.  `isRowEmptyAt' iterates the render
state's viewport row iterator, so `cy' should come from the same
coordinate space.  `DATA_CURSOR_Y' is active-area-relative and happens
to match viewport Y in the common case (viewport pinned to bottom by
redraw), but the render-state pair exposes a `HAS_VALUE' flag that
correctly returns nil when the cursor isn't visible in the current
viewport — which is also the safe answer for the anchor-window clamp.

Move `render_state_update' to `fnCursorOnEmptyRow' and drop the
redundant call inside `isRowEmptyAt'; update the helper docstring to
state the caller contract.  Remove the now-unused `DATA_CURSOR_Y'
constant.
ad8536e narrowed the `ghostel--anchor-window' clamp to fire only when
libghostty reports pending-wrap, which fixed the #146 regression but
reopened a variant of #138 for TUIs that move their cursor to the
bottom of the screen via absolute positioning (CUP) rather than via
writing-then-wrapping.  In that case `pt' equals `point-max' and the
cursor sits on the last viewport row, but pending-wrap is nil — so the
clamp doesn't fire and Emacs shifts `window-start' by one row to make
`pt' "visible," fighting the viewport pin.

Expose a second terminal-side predicate `ghostel--cursor-on-empty-row-p'
backed by a new `render.isRowEmptyAt' helper, and widen the clamp guard
to `(or pending-wrap cursor-on-empty-row)'.  The predicate returns t iff
the row containing the cursor has no written cells and no cells with
non-default styling — exactly the condition under which `buildRowContent'
produces `byte_len == 0'.  Wide-spacer-tail cells are skipped to mirror
`buildRowContent' (defensive — in practice spacer tails always follow a
wide grapheme).

Source `cy' from `RS_DATA_CURSOR_VIEWPORT_Y' gated by `...HAS_VALUE' so
the coordinate space matches the viewport row iterator that
`isRowEmptyAt' walks, and the predicate returns nil when the cursor
isn't visible in the current viewport.  Caller owns the
`ghostty_render_state_update' refresh so it only happens once per call.

An earlier draft (#158) tried `pos-visible-in-window-p' but it reflects
the previous redisplay rather than the just-pinned `window-start', and
breaks in batch.  The terminal-side predicate answers the real
question — is there anything on this row to anchor `pt' to? — without
consulting Emacs redisplay state.

Bump module version to 0.16.3 (new exported function).

Test changes:
- `ghostel-test-cursor-on-empty-row-p' covers the predicate across
  fresh-terminal / post-write / post-CRLF / post-CUP cursor positions.
- `ghostel-test-anchor-window-clamps-on-empty-row' is the #157
  regression test: feeds "foo\r\nbar\r\n" to park the cursor on an
  empty last row, asserts PT lands at `point-max', pending-wrap is nil,
  empty-row is t, and the clamp fires.
- `ghostel-test-anchor-window-no-clamp-on-populated-last-row'
  complements #146: cursor at `point-max' on a last row that does have
  content (e.g. a shell prompt) must NOT be clamped regardless of which
  predicate is consulted.

Closes #157.
@dakra dakra force-pushed the fix-157-cup-park-empty-row branch from 3b18124 to d4fdc8e Compare April 20, 2026 21:46
@dakra dakra merged commit d4fdc8e into main Apr 21, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants