Summary
ghostel--window-anchored-p's +1-line tolerance classifies a window scrolled up exactly one line as still anchored, so the per-redraw re-anchor loop in ghostel--redraw-now yanks it back to the bottom during active output. The user-visible effect: while a TUI is streaming, you scroll up one line to peek and the viewport snaps back down on the next frame; scrolling two or more lines sticks.
When it bites (measured, v0.34.0, GNU Emacs 30.2 NS + the prebuilt module)
- Line-count fallback mode (Emacs 28 path /
ghostel--pixel-anchor-supported-p nil): always. Every grid row ends in \n, so an anchored window shows exactly floor(window-screen-lines) rows and window-start sits exactly at that boundary; the predicate accepts floor+1, so a one-line-up window still passes and gets re-anchored on every redraw.
- Pixel-anchor mode: only when the window body height is an exact multiple of the default line height (then vscroll is 0, there is no partial top line to consume the +1 slack, and the same misclassification occurs). For a 14px line height that's ~1/14 of window geometries — easy to hit after a resize.
Reproduced with a synthetic streaming driver in three geometries (non-multiple / exact-multiple / line-fallback): snapback occurs in the latter two with stock, never with a strict predicate.
Suggested fix
Make the predicate mirror the anchor's own arithmetic instead of allowing slack: anchored ⟺ window-start ≥ the buffer position ghostel--anchor-window itself would choose — i.e. point-max moved back (ceiling lines) rows in pixel mode (where a partially-scrolled top line is legitimate) and (floor lines) rows in vscroll-0 mode. With that change (tested as an :around replacement): one-line-up scroll sticks in all three geometries, bottom tracking under growth, resize re-anchoring, short buffers, and multiple windows on one buffer all behave as before.
The +1 slack looks like it was sized for the pixel anchor's partially-visible top line; deriving the threshold per-mode removes the need for slack entirely.
Summary
ghostel--window-anchored-p's +1-line tolerance classifies a window scrolled up exactly one line as still anchored, so the per-redraw re-anchor loop inghostel--redraw-nowyanks it back to the bottom during active output. The user-visible effect: while a TUI is streaming, you scroll up one line to peek and the viewport snaps back down on the next frame; scrolling two or more lines sticks.When it bites (measured, v0.34.0, GNU Emacs 30.2 NS + the prebuilt module)
ghostel--pixel-anchor-supported-pnil): always. Every grid row ends in\n, so an anchored window shows exactlyfloor(window-screen-lines)rows andwindow-startsits exactly at that boundary; the predicate acceptsfloor+1, so a one-line-up window still passes and gets re-anchored on every redraw.Reproduced with a synthetic streaming driver in three geometries (non-multiple / exact-multiple / line-fallback): snapback occurs in the latter two with stock, never with a strict predicate.
Suggested fix
Make the predicate mirror the anchor's own arithmetic instead of allowing slack: anchored ⟺
window-start ≥the buffer positionghostel--anchor-windowitself would choose — i.e.point-maxmoved back(ceiling lines)rows in pixel mode (where a partially-scrolled top line is legitimate) and(floor lines)rows in vscroll-0 mode. With that change (tested as an:aroundreplacement): one-line-up scroll sticks in all three geometries, bottom tracking under growth, resize re-anchoring, short buffers, and multiple windows on one buffer all behave as before.The +1 slack looks like it was sized for the pixel anchor's partially-visible top line; deriving the threshold per-mode removes the need for slack entirely.