Skip to content

Cleaner extension point for evil/normal-state roaming over an animated terminal (anchor + wheel scroll)? #454

Description

@amiorin

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

  1. 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.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions