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.
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 casept = point-maxand the cursor sits on the last viewport row, butghostel--cursor-pending-wrap-preturns nil — so the clamp doesn't fire and Emacs scrollswindow-startby one row to makept"visible," fighting the viewport pin.Reproduction
(0, last-row)on focus loss. Anthropic's Claude Code CLI does this; likely others do too.Evidence
Captured by advising
ghostel--anchor-windowandghostel--focus-change(loggingpt,point-max, terminal cursor(col . row),ghostel--cursor-pending-wrap-p,window-startbefore/after):pt = point-max,cursor-pos = (0 . 41)(last row, col 0),pending-wrap = nil. After the anchor returns, Emacs redisplay shiftswindow-startby one terminal row.ptis back mid-buffer, anchor restoreswindow-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-wrapanswers "did writing past the row boundary leave the cursor in a wrap-on-next-write state?" That's one way to land atpt = point-maxon 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-maxoff-screen given the currentwindow-start" — which is exactly whatpos-visible-in-window-panswers.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
ptinto view.Coverage
pt = pmaxpos-visible-in-window-p(0, last-row)(this report)pending-wrapbecomes 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-pis no longer needed byghostel--anchor-window. Could be kept for other consumers / tests.pos-visible-in-window-pconsults the just-pinnedwindow-start, so the answer reflects the state we want it to.(0, body-height-1)via CUP (not pending-wrap) withpt = point-maxand assertswpis clamped.