@@ -3091,6 +3091,15 @@ viewport. After the fix, the scrolled-up view is preserved."
30913091 (set-window-start (selected-window) (point-min) t)
30923092 (goto-char (point-min))
30933093 (set-window-point (selected-window) (point-min))
3094+ ;; Real-world flow: some PTY output arrives between the
3095+ ;; wheel-up and `M-x', so an output-driven redraw captures
3096+ ;; the scrolled window into `ghostel--scroll-positions'
3097+ ;; before the resize fires. Without this intermediate
3098+ ;; capture the resize redraw's drift heuristic would
3099+ ;; (correctly, by that heuristic) classify this window as
3100+ ;; drifted-but-anchored and snap it back.
3101+ (ghostel--delayed-redraw buf)
3102+ (should (assq (selected-window) ghostel--scroll-positions))
30943103 (let ((ws-before (window-start (selected-window)))
30953104 (wp-before (window-point (selected-window))))
30963105
@@ -3125,6 +3134,86 @@ viewport. After the fix, the scrolled-up view is preserved."
31253134 (set-window-buffer (selected-window) orig-buf))
31263135 (kill-buffer buf))))
31273136
3137+ (ert-deftest ghostel-test-redraw-resize-preserves-anchor-when-emacs-drifts-ws ()
3138+ "Resize keeps the window anchored when Emacs drifted `window-start' below it.
3139+ Regression test for issue #127: in TUIs whose cursor sits above the
3140+ viewport bottom, opening the minibuffer shrinks the window body and
3141+ Emacs's `keep-point-visible' moves `window-start' forward so the TUI
3142+ cursor stays on screen. The resulting `ws < anchor' looked identical
3143+ to a real user scroll, so the force redraw captured a blank-row key,
3144+ found it at `point-min', and jumped `window-start' to 1.
3145+
3146+ With the fix, a force redraw classifies a window as anchored when it
3147+ wasn't recorded in `ghostel--scroll-positions' at the prior redraw —
3148+ so an Emacs-driven drift is treated as drift, not a scroll."
3149+ (let ((buf (generate-new-buffer " *ghostel-test-resize-anchor-drift*"))
3150+ (orig-buf (window-buffer (selected-window))))
3151+ (unwind-protect
3152+ (with-current-buffer buf
3153+ (ghostel-mode)
3154+ (let* ((term (ghostel--new 10 40 200))
3155+ (ghostel--term term)
3156+ (ghostel--term-rows 10)
3157+ (inhibit-read-only t))
3158+ ;; Write enough blank-terminated lines that a drifted
3159+ ;; ws-key would ambiguously match near `point-min'.
3160+ (dotimes (i 30)
3161+ (ghostel--write-input term (format "row-%02d\r\n" i)))
3162+ (ghostel--redraw term t)
3163+ (set-window-buffer (selected-window) buf)
3164+ ;; Steady-state auto-follow; prior redraw seeds the anchor.
3165+ (goto-char (point-max))
3166+ (set-window-point (selected-window) (point-max))
3167+ (let ((vp (save-excursion
3168+ (goto-char (point-max))
3169+ (forward-line -9)
3170+ (line-beginning-position))))
3171+ (set-window-start (selected-window) vp t))
3172+ (setq ghostel--force-next-redraw t)
3173+ (ghostel--delayed-redraw buf)
3174+ (should ghostel--last-anchor-position)
3175+ (should-not ghostel--scroll-positions)
3176+
3177+ ;; Simulate Emacs drift: `keep-point-visible' on a
3178+ ;; minibuffer-triggered resize slides `window-start' a
3179+ ;; couple rows below the anchor. Point stays in the live
3180+ ;; viewport (TUI cursor on a row above the bottom).
3181+ (let ((drifted-ws (save-excursion
3182+ (goto-char ghostel--last-anchor-position)
3183+ (forward-line -2)
3184+ (line-beginning-position))))
3185+ (should (< drifted-ws ghostel--last-anchor-position))
3186+ (set-window-start (selected-window) drifted-ws t))
3187+ ;; Window is NOT in `ghostel--scroll-positions' — it was
3188+ ;; auto-following, not user-scrolled.
3189+ (should-not ghostel--scroll-positions)
3190+
3191+ ;; Resize path (same harness as the scrolled-view test).
3192+ (cl-letf (((default-value 'window-adjust-process-window-size-function)
3193+ (lambda (&rest _) (cons 40 6)))
3194+ ((symbol-function 'set-process-window-size) #'ignore))
3195+ (setq ghostel--process
3196+ (make-pipe-process :name "ghostel-test-fake"
3197+ :buffer buf
3198+ :noquery t
3199+ :filter #'ignore
3200+ :sentinel #'ignore))
3201+ (unwind-protect
3202+ (ghostel--window-adjust-process-window-size
3203+ ghostel--process
3204+ (list (selected-window)))
3205+ (delete-process ghostel--process)
3206+ (setq ghostel--process nil)))
3207+
3208+ ;; Window must be re-anchored to the live viewport, NOT
3209+ ;; yanked to `point-min'.
3210+ (should (= (ghostel--viewport-start)
3211+ (window-start (selected-window))))
3212+ (should (> (window-start (selected-window)) 1))))
3213+ (when (buffer-live-p orig-buf)
3214+ (set-window-buffer (selected-window) orig-buf))
3215+ (kill-buffer buf))))
3216+
31283217(ert-deftest ghostel-test-viewport-start-skips-trailing-newline ()
31293218 "`ghostel--viewport-start' must not be off-by-one on a trailing \\n.
31303219Partial redraws can leave the buffer ending with \\n (e.g. after
0 commit comments