Summary
When using ghostel with an animated terminal program (something redrawing
at ~30fps) under evil, the viewport re-anchoring in ghostel--anchor-window
fights any attempt to move point off the live cursor or scroll into scrollback.
I've worked around it with two define-advice forms, but they feel like they're
papering over something ghostel could expose (or do) itself — hence this issue.
My setup: Ghostty → Zellij → Emacs -nw (terminal Emacs), with evil +
a small evil-ghostel layer that keeps the terminal cursor synced to point
across evil state changes.
The two seams
1. The redraw anchor fights normal-state motion
Each redraw, ghostel--redraw-now re-anchors any window following the live
viewport via ghostel--anchor-window, whose final set-window-point snaps
point back to the terminal cursor (ghostel--cursor-char-pos). On a static
terminal redraws are rare, so this is invisible. On an animated one it fires
~30 times/sec and yanks point back the instant evil normal-state motion
(hjkl) tries to move it off the live cursor.
My workaround skips the anchor while point is parked off the live cursor in a
motion-capable evil state, and lets auto-follow resume on return to insert or
back onto the cursor row. FORCE anchors (paste/yank) are always honored:
(define-advice ghostel--anchor-window
(:around (orig &optional window force) evil-ghostel-roam)
(if (and (bound-and-true-p evil-ghostel-mode)
(not force)
(memq evil-state '(normal visual operator motion))
ghostel--cursor-char-pos
(/= (point) ghostel--cursor-char-pos))
nil
(funcall orig window force)))
2. Wheel scroll into scrollback gets snapped back
In insert state the redraw anchor is still live, so a mouse-wheel scroll into
scrollback is immediately yanked back to the live cursor. Normal state is
exactly where the advice above suppresses the anchor, so I flip the buffer to
normal state on any wheel event over a ghostel buffer. ghostel redispatches
wheel events to mwheel-scroll when it scrolls the Emacs buffer (mouse tracking
off), so that's what I advise — only switching from insert/emacs (normal/visual
already roam, and I mustn't drop a visual selection):
(define-advice mwheel-scroll
(:before (event &rest _) evil-ghostel-wheel-normal)
(let ((buf (window-buffer (posn-window (event-start event)))))
(when (buffer-live-p buf)
(with-current-buffer buf
(when (and (bound-and-true-p evil-ghostel-mode)
(memq evil-state '(insert emacs)))
(evil-normal-state))))))
Questions
-
Is there a cleaner, blessed extension point? Advising
ghostel--anchor-window and mwheel-scroll (both internal) is brittle — a
hook, an "anchor predicate" variable, or a ghostel-follow-mode-style toggle
that callers could flip while point is parked off the cursor would let this
live without advice.
-
Could roaming-off-the-cursor / scroll-into-scrollback become the default
(or a built-in option)? The core friction is that the redraw anchor has no
notion of "the user has intentionally moved point away." Is making that a
first-class state something ghostel would consider, or is the
always-follow-the-live-cursor behavior intentional for performance/simplicity
reasons I'm missing?
Happy to turn any of this into a PR if you can point me at the shape you'd want.
Summary
When using ghostel with an animated terminal program (something redrawing
at ~30fps) under evil, the viewport re-anchoring in
ghostel--anchor-windowfights any attempt to move point off the live cursor or scroll into scrollback.
I've worked around it with two
define-adviceforms, but they feel like they'repapering over something ghostel could expose (or do) itself — hence this issue.
My setup: Ghostty → Zellij → Emacs
-nw(terminal Emacs), withevil+a small
evil-ghostellayer that keeps the terminal cursor synced to pointacross evil state changes.
The two seams
1. The redraw anchor fights normal-state motion
Each redraw,
ghostel--redraw-nowre-anchors any window following the liveviewport via
ghostel--anchor-window, whose finalset-window-pointsnapspoint back to the terminal cursor (
ghostel--cursor-char-pos). On a staticterminal redraws are rare, so this is invisible. On an animated one it fires
~30 times/sec and yanks point back the instant evil normal-state motion
(
hjkl) tries to move it off the live cursor.My workaround skips the anchor while point is parked off the live cursor in a
motion-capable evil state, and lets auto-follow resume on return to insert or
back onto the cursor row. FORCE anchors (paste/yank) are always honored:
2. Wheel scroll into scrollback gets snapped back
In insert state the redraw anchor is still live, so a mouse-wheel scroll into
scrollback is immediately yanked back to the live cursor. Normal state is
exactly where the advice above suppresses the anchor, so I flip the buffer to
normal state on any wheel event over a ghostel buffer. ghostel redispatches
wheel events to
mwheel-scrollwhen it scrolls the Emacs buffer (mouse trackingoff), so that's what I advise — only switching from insert/emacs (normal/visual
already roam, and I mustn't drop a visual selection):
Questions
Is there a cleaner, blessed extension point? Advising
ghostel--anchor-windowandmwheel-scroll(both internal) is brittle — ahook, an "anchor predicate" variable, or a
ghostel-follow-mode-style togglethat callers could flip while point is parked off the cursor would let this
live without advice.
Could roaming-off-the-cursor / scroll-into-scrollback become the default
(or a built-in option)? The core friction is that the redraw anchor has no
notion of "the user has intentionally moved point away." Is making that a
first-class state something ghostel would consider, or is the
always-follow-the-live-cursor behavior intentional for performance/simplicity
reasons I'm missing?
Happy to turn any of this into a PR if you can point me at the shape you'd want.