Skip to content

ghostel--anchor-window: viewport scrolls on focus-out when TUI parks cursor at bottom-left (pt = point-max, no pending-wrap) #157

Description

@emil-e

Summary

The pending-wrap-narrowed clamp added in ad8536e ("Narrow window-point clamp to pending-wrap only") fixes #146 but reopens a variant of #138 for TUIs that move their cursor to the bottom of the screen via CUP rather than via writing-then-wrapping. In that case pt = point-max and the cursor sits on the last viewport row, but ghostel--cursor-pending-wrap-p returns nil — so the clamp doesn't fire and Emacs scrolls window-start by one row to make pt "visible," fighting the viewport pin.

Reproduction

  • Run an interactive TUI in a ghostel buffer that sends a CUP move to (0, last-row) on focus loss. Anthropic's Claude Code CLI does this; likely others do too.
  • Display the buffer in a window, focus a different Emacs window, then refocus.
  • The viewport shifts down by one line on focus-out and back up on focus-in.

Evidence

Captured by advising ghostel--anchor-window and ghostel--focus-change (logging pt, point-max, terminal cursor (col . row), ghostel--cursor-pending-wrap-p, window-start before/after):

  • Focus-out anchor: pt = point-max, cursor-pos = (0 . 41) (last row, col 0), pending-wrap = nil. After the anchor returns, Emacs redisplay shifts window-start by one terminal row.
  • Focus-in anchor: pt is back mid-buffer, anchor restores window-start.

So #138's underlying scroll behavior is back; pending-wrap is just no longer the reliable predicate for it.

Why pending-wrap is too narrow

pending-wrap answers "did writing past the row boundary leave the cursor in a wrap-on-next-write state?" That's one way to land at pt = point-max on the last visible row, but not the only way. CUP placements to (0, last-row) reach the same position without setting pending-wrap.

The real precondition for the bug is "Emacs considers pt = point-max off-screen given the current window-start" — which is exactly what pos-visible-in-window-p answers.

Proposed fix

Draft PR: see linked PR below. Replaces the pending-wrap predicate with a direct visibility check: clamp iff Emacs would otherwise need to scroll to bring pt into view.

Coverage

Scenario pt = pmax pos-visible-in-window-p Clamp? Correct?
#138 pending-wrap, last row yes nil yes yes
#146 shell prompt mid-window yes t no yes
Shell prompt on last row, mid-col yes t no yes
Shell prompt last row + pending-wrap yes nil yes yes (visual cursor is at last cell anyway)
CUP-park at (0, last-row) (this report) yes nil yes yes
Mid-buffer pt no no yes

pending-wrap becomes a special case of "pt off-screen", so the #138 fix is preserved and #146's mid-window typing case continues to render correctly.

Tested locally

Patched my install with the proposed change. Original symptom (focus-shift on Claude Code buffer) is gone. Normal shell prompt typing still renders the block cursor after the last character, including past the right margin.

Notes

  • ghostel--cursor-pending-wrap-p is no longer needed by ghostel--anchor-window. Could be kept for other consumers / tests.
  • pos-visible-in-window-p consults the just-pinned window-start, so the answer reflects the state we want it to.
  • Worth adding a regression test that places the cursor at (0, body-height-1) via CUP (not pending-wrap) with pt = point-max and asserts wp is clamped.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions