Skip to content

Commit 6728ffc

Browse files
committed
Redraw synchronously on resize and anchor window-start
Two changes to eliminate the remaining resize flicker: 1. The resize handler now calls ghostel--delayed-redraw synchronously instead of deferring to a 16ms timer. This ensures the buffer is fully rebuilt before Emacs gets a chance to display the stale content at the new window size. 2. After every redraw, set-window-start is called for all windows showing the buffer, anchored to the viewport origin. This prevents Emacs's auto-scroll from producing a visible jump when the buffer is rebuilt. Ref: #85
1 parent 5966043 commit 6728ffc

2 files changed

Lines changed: 115 additions & 7 deletions

File tree

ghostel.el

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2218,8 +2218,23 @@ interrupted by live output updating the terminal cursor."
22182218
;; `find-file-other-window'), the ghostel window's
22192219
;; window-point is stale and the terminal cursor will display
22202220
;; at the wrong place when the user reselects it. Sync it.
2221-
(let ((pt (point)))
2221+
;;
2222+
;; Also anchor window-start to the viewport origin when point
2223+
;; is in the viewport. Without this, a resize (which erases
2224+
;; and rebuilds the buffer inside redraw) leaves window-start
2225+
;; clamped to 1 and Emacs's auto-scroll produces a visible
2226+
;; jump. Skip the anchor when point is in scrollback so that
2227+
;; users reading history are not yanked back to the bottom.
2228+
(let* ((pt (point))
2229+
(tr (or ghostel--term-rows 0))
2230+
(vs (when (> tr 0)
2231+
(save-excursion
2232+
(goto-char (point-max))
2233+
(forward-line (- (1- tr)))
2234+
(line-beginning-position)))))
22222235
(dolist (win (get-buffer-window-list buffer nil t))
2236+
(when (and vs (>= pt vs))
2237+
(set-window-start win vs t))
22232238
(set-window-point win pt))))))))
22242239

22252240
(defun ghostel-force-redraw ()
@@ -2254,7 +2269,12 @@ PROCESS is the shell process, WINDOWS is the list of windows."
22542269
(ghostel--set-size ghostel--term (max 1 height) (max 1 width))
22552270
(setq ghostel--term-rows height)
22562271
(setq ghostel--force-next-redraw t)
2257-
(ghostel--invalidate))))
2272+
;; Redraw synchronously so the buffer is updated before
2273+
;; Emacs displays the stale content at the new window size.
2274+
(when ghostel--redraw-timer
2275+
(cancel-timer ghostel--redraw-timer)
2276+
(setq ghostel--redraw-timer nil))
2277+
(ghostel--delayed-redraw buffer))))
22582278
;; Return size — Emacs calls set-process-window-size (SIGWINCH)
22592279
;; after this function returns, matching eat/vterm timing.
22602280
size))

test/ghostel-test.el

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,6 +1447,94 @@ for new size inside BSU/ESU → verify buffer shows new content."
14471447
(should-not (string-match-p "OLD-LINE" content)))))
14481448
(kill-buffer buf))))
14491449

1450+
;; -----------------------------------------------------------------------
1451+
;; Test: resize preserves old frame until redraw replaces it
1452+
;; -----------------------------------------------------------------------
1453+
1454+
(ert-deftest ghostel-test-resize-no-blank-flash ()
1455+
"Buffer keeps old content after resize; redraw replaces it atomically.
1456+
Regression test: fnSetSize used to call erase-buffer synchronously,
1457+
leaving the buffer visibly empty until the next timer-driven redraw.
1458+
Now the erasure is deferred into redraw() under inhibit-redisplay."
1459+
(let ((buf (generate-new-buffer " *ghostel-test-resize-no-blank*")))
1460+
(unwind-protect
1461+
(with-current-buffer buf
1462+
(ghostel-mode)
1463+
(let* ((term (ghostel--new 10 40 100))
1464+
(ghostel--term term)
1465+
(ghostel--term-rows 10)
1466+
(inhibit-read-only t))
1467+
;; Fill the viewport with identifiable content.
1468+
(dotimes (i 10)
1469+
(ghostel--write-input term (format "LINE-%02d\r\n" i)))
1470+
(ghostel--redraw term t)
1471+
(let ((pre-content (buffer-substring-no-properties
1472+
(point-min) (point-max))))
1473+
(should (string-match-p "LINE-00" pre-content))
1474+
(should (string-match-p "LINE-09" pre-content))
1475+
1476+
;; Resize — old content must survive in the buffer.
1477+
(ghostel--set-size term 6 40)
1478+
(setq ghostel--term-rows 6)
1479+
(let ((mid-content (buffer-substring-no-properties
1480+
(point-min) (point-max))))
1481+
(should (> (length mid-content) 0))
1482+
(should (string-match-p "LINE-" mid-content)))
1483+
1484+
;; Redraw rebuilds the buffer from the new terminal state.
1485+
(ghostel--redraw term t)
1486+
(let ((post-content (buffer-substring-no-properties
1487+
(point-min) (point-max))))
1488+
(should (> (length post-content) 0))
1489+
;; Viewport should have the new row count; extra lines
1490+
;; above are scrollback from the old viewport rows.
1491+
(should (>= (count-lines (point-min) (point-max)) 6))))))
1492+
(kill-buffer buf))))
1493+
1494+
(ert-deftest ghostel-test-resize-redraw-anchors-window-start ()
1495+
"After resize + redraw, window-start is at the viewport origin.
1496+
Without explicit anchoring, erase+rebuild inside redraw() clamps
1497+
window-start to 1 (top of scrollback), causing a visible jump when
1498+
Emacs auto-scrolls to make point visible."
1499+
(let ((buf (generate-new-buffer " *ghostel-test-resize-anchor*"))
1500+
(orig-buf (window-buffer (selected-window))))
1501+
(unwind-protect
1502+
(with-current-buffer buf
1503+
(ghostel-mode)
1504+
(let* ((term (ghostel--new 10 40 200))
1505+
(ghostel--term term)
1506+
(ghostel--term-rows 10)
1507+
(ghostel--force-next-redraw nil)
1508+
(inhibit-read-only t))
1509+
;; Build up scrollback so the viewport is not at buffer start.
1510+
(dotimes (i 30)
1511+
(ghostel--write-input term (format "scroll-%02d\r\n" i)))
1512+
(ghostel--write-input term "prompt> ")
1513+
(ghostel--redraw term t)
1514+
(should (> (line-number-at-pos (point-max)) 10))
1515+
1516+
;; Display in a real window so we can test window-start.
1517+
(set-window-buffer (selected-window) buf)
1518+
1519+
;; Resize + redraw via delayed-redraw (simulates the real path).
1520+
(ghostel--set-size term 6 40)
1521+
(setq ghostel--term-rows 6)
1522+
(setq ghostel--force-next-redraw t)
1523+
(ghostel--delayed-redraw buf)
1524+
1525+
;; window-start should be at the viewport, not at buffer start.
1526+
(let* ((ws (window-start (selected-window)))
1527+
(wp (window-point (selected-window)))
1528+
(vp-start (save-excursion
1529+
(goto-char (point-max))
1530+
(forward-line -5)
1531+
(line-beginning-position))))
1532+
(should (= ws vp-start))
1533+
(should (>= wp vp-start)))))
1534+
(when (buffer-live-p orig-buf)
1535+
(set-window-buffer (selected-window) orig-buf))
1536+
(kill-buffer buf))))
1537+
14501538
;; -----------------------------------------------------------------------
14511539
;; Test: resize with real process — verify PTY and buffer content
14521540
;; -----------------------------------------------------------------------
@@ -2398,12 +2486,12 @@ ncurses apps like htop at start-up size and breaks live resize."
23982486
(let ((ghostel--term 'fake)
23992487
(ghostel--force-next-redraw nil)
24002488
(set-size-args nil)
2401-
(invalidate-called nil))
2489+
(redraw-called nil))
24022490
(let ((cur-buf (current-buffer)))
24032491
(cl-letf (((symbol-function 'ghostel--set-size)
24042492
(lambda (_term h w) (setq set-size-args (list h w))))
2405-
((symbol-function 'ghostel--invalidate)
2406-
(lambda () (setq invalidate-called t)))
2493+
((symbol-function 'ghostel--delayed-redraw)
2494+
(lambda (_buf) (setq redraw-called t)))
24072495
((symbol-function 'process-buffer)
24082496
(lambda (_proc) cur-buf))
24092497
((default-value 'window-adjust-process-window-size-function)
@@ -2413,7 +2501,7 @@ ncurses apps like htop at start-up size and breaks live resize."
24132501
(should (equal '(120 . 40) result))
24142502
(should (equal '(40 120) set-size-args))
24152503
(should ghostel--force-next-redraw)
2416-
(should invalidate-called)))))))
2504+
(should redraw-called)))))))
24172505

24182506
(ert-deftest ghostel-test-resize-nil-size ()
24192507
"When default function returns nil, no resize happens."
@@ -2564,7 +2652,7 @@ while :; do sleep 0.1; done'\n")
25642652
(let ((ghostel--term 'fake-term))
25652653
(cl-letf (((symbol-function 'ghostel--set-size)
25662654
(lambda (_t _h _w) nil))
2567-
((symbol-function 'ghostel--invalidate) #'ignore)
2655+
((symbol-function 'ghostel--delayed-redraw) #'ignore)
25682656
((default-value 'window-adjust-process-window-size-function)
25692657
(lambda (_p _w) (cons 120 30))))
25702658
;; Invoke the handler as Emacs would.

0 commit comments

Comments
 (0)