Skip to content

Commit 3b6c980

Browse files
committed
Let user scroll packages handle wheel events
Replace hardcoded scroll-down/up 3 with an emulation-mode-map-alists intercept that only captures wheel events when terminal mouse tracking is active (vim, htop, etc.). When tracking is off the event is re-dispatched through the real event loop so ultra-scroll, pixel-scroll-precision-mode, or plain mwheel-scroll run with full event-loop semantics (velocity tracking, this-command, etc.). Removes the buffer-local pixel-scroll-precision-mode suppression that was previously needed to force wheel events into ghostel-mode-map. Fixes #97
1 parent f01de74 commit 3b6c980

2 files changed

Lines changed: 104 additions & 61 deletions

File tree

ghostel.el

Lines changed: 72 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,74 @@ variable re-enables automatic renaming for the next title update.")
657657
"List of prompt positions as (buffer-line . exit-status) pairs.
658658
Used for prompt navigation and optional re-application after full redraws.")
659659

660+
(defvar-local ghostel--scroll-intercept-active nil
661+
"Non-nil when ghostel's scroll-event intercept is active.
662+
Used as the activation key in `emulation-mode-map-alists'.")
663+
664+
665+
666+
;;; Scroll intercept via emulation-mode-map-alists
667+
;;
668+
;; We need highest-priority interception of wheel events so that terminal
669+
;; mouse tracking (vim, htop, etc.) receives scroll events. When mouse
670+
;; tracking is off, we fall through to whatever scroll package the user
671+
;; has configured (ultra-scroll, pixel-scroll-precision-mode, etc.).
672+
673+
(defun ghostel--scroll-intercept-up (event)
674+
"Intercept wheel-up EVENT for terminal mouse tracking.
675+
If the terminal is tracking mouse events, forward as button 4.
676+
Otherwise, re-dispatch EVENT through the normal event loop so the
677+
user's scroll package handles it."
678+
(interactive "e")
679+
(unless (ghostel--forward-scroll-event event 4)
680+
(ghostel--redispatch-scroll-event event)))
681+
682+
(defun ghostel--scroll-intercept-down (event)
683+
"Intercept wheel-down EVENT for terminal mouse tracking.
684+
If the terminal is tracking mouse events, forward as button 5.
685+
Otherwise, re-dispatch EVENT through the normal event loop so the
686+
user's scroll package handles it."
687+
(interactive "e")
688+
(unless (ghostel--forward-scroll-event event 5)
689+
(ghostel--redispatch-scroll-event event)))
690+
691+
(defun ghostel--redispatch-scroll-event (event)
692+
"Re-dispatch scroll EVENT through the event loop without our intercept.
693+
Temporarily disables the emulation-map intercept and pushes the event
694+
back as unread input. The next key-lookup therefore skips our map and
695+
finds the user's scroll handler. A `pre-command-hook' re-enables the
696+
intercept before that handler runs, so subsequent events are intercepted
697+
again."
698+
(setq ghostel--scroll-intercept-active nil)
699+
(push event unread-command-events)
700+
;; pre-command-hook fires *after* key lookup but *before* the command,
701+
;; so the re-dispatched event is looked up with our intercept disabled
702+
;; and the intercept is back on before the next event after that.
703+
(add-hook 'pre-command-hook #'ghostel--reenable-scroll-intercept nil t))
704+
705+
(defun ghostel--reenable-scroll-intercept ()
706+
"Re-enable the scroll-event intercept after a re-dispatched event."
707+
(setq ghostel--scroll-intercept-active t)
708+
(remove-hook 'pre-command-hook #'ghostel--reenable-scroll-intercept t))
709+
710+
(defvar ghostel--scroll-intercept-map
711+
(let ((map (make-sparse-keymap)))
712+
(define-key map [mouse-4] #'ghostel--scroll-intercept-up)
713+
(define-key map [mouse-5] #'ghostel--scroll-intercept-down)
714+
(define-key map [wheel-up] #'ghostel--scroll-intercept-up)
715+
(define-key map [wheel-down] #'ghostel--scroll-intercept-down)
716+
map)
717+
"Keymap for `emulation-mode-map-alists' to intercept scroll events.
718+
Active only in ghostel buffers where `ghostel--scroll-intercept-active'
719+
is non-nil.")
720+
721+
(defvar ghostel--emulation-alist
722+
`((ghostel--scroll-intercept-active . ,ghostel--scroll-intercept-map))
723+
"Alist for `emulation-mode-map-alists'.")
724+
725+
(unless (memq 'ghostel--emulation-alist emulation-mode-map-alists)
726+
(push 'ghostel--emulation-alist emulation-mode-map-alists))
727+
660728

661729

662730
;;; Keymap
@@ -723,11 +791,6 @@ Used for prompt navigation and optional re-application after full redraws.")
723791
;; Prompt navigation (OSC 133)
724792
(define-key map (kbd "C-c C-n") #'ghostel-next-prompt)
725793
(define-key map (kbd "C-c C-p") #'ghostel-previous-prompt)
726-
;; Mouse wheel for scrollback
727-
(define-key map (kbd "<mouse-4>") #'ghostel--scroll-up)
728-
(define-key map (kbd "<mouse-5>") #'ghostel--scroll-down)
729-
(define-key map (kbd "<wheel-up>") #'ghostel--scroll-up)
730-
(define-key map (kbd "<wheel-down>") #'ghostel--scroll-down)
731794
;; Mouse click events (for terminal mouse tracking)
732795
(define-key map (kbd "<down-mouse-1>") #'ghostel--mouse-press)
733796
(define-key map (kbd "<mouse-1>") #'ghostel--mouse-release)
@@ -1111,22 +1174,6 @@ Return non-nil if the event was forwarded (mouse tracking is active)."
11111174
row col
11121175
(ghostel--mouse-mods event)))))
11131176

1114-
(defun ghostel--scroll-up (&optional event)
1115-
"Scroll the Emacs window up (toward older scrollback).
1116-
When the terminal has mouse tracking enabled, forward EVENT as a
1117-
scroll event to the running application instead."
1118-
(interactive "e")
1119-
(unless (ghostel--forward-scroll-event event 4) ; button 4 = scroll up
1120-
(scroll-down 3)))
1121-
1122-
(defun ghostel--scroll-down (&optional event)
1123-
"Scroll the Emacs window down (toward newer content).
1124-
When the terminal has mouse tracking enabled, forward EVENT as a
1125-
scroll event to the running application instead."
1126-
(interactive "e")
1127-
(unless (ghostel--forward-scroll-event event 5) ; button 5 = scroll down
1128-
(scroll-up 3)))
1129-
11301177

11311178
(defun ghostel-copy-mode-previous-line ()
11321179
"Move to the previous line in copy mode."
@@ -1240,11 +1287,6 @@ scroll event to the running application instead."
12401287
;; Prompt navigation works in copy mode too
12411288
(define-key map (kbd "C-c C-n") #'ghostel-next-prompt)
12421289
(define-key map (kbd "C-c C-p") #'ghostel-previous-prompt)
1243-
;; Scrollback
1244-
(define-key map (kbd "<mouse-4>") #'ghostel--scroll-up)
1245-
(define-key map (kbd "<mouse-5>") #'ghostel--scroll-down)
1246-
(define-key map (kbd "<wheel-up>") #'ghostel--scroll-up)
1247-
(define-key map (kbd "<wheel-down>") #'ghostel--scroll-down)
12481290
(define-key map (kbd "C-n") #'ghostel-copy-mode-next-line)
12491291
(define-key map (kbd "C-p") #'ghostel-copy-mode-previous-line)
12501292
(define-key map (kbd "M-<") #'ghostel-copy-mode-beginning-of-buffer)
@@ -2303,13 +2345,13 @@ PROCESS is the shell process, WINDOWS is the list of windows."
23032345
(setq-local scroll-conservatively 101)
23042346
(setq-local line-spacing 0)
23052347
(add-function :after after-focus-change-function #'ghostel--focus-change)
2306-
(ghostel--suppress-interfering-modes))
2348+
(ghostel--suppress-interfering-modes)
2349+
(setq ghostel--scroll-intercept-active t))
23072350

23082351
(defun ghostel--suppress-interfering-modes ()
23092352
"Disable global minor modes that interfere with ghostel.
23102353
Suppresses `global-hl-line-mode' (and buffer-local `hl-line-mode') to
2311-
prevent redraw flicker, and `pixel-scroll-precision-mode' so that
2312-
wheel events reach ghostel's own scroll commands."
2354+
prevent redraw flicker."
23132355
;; global-hl-line-mode: opt this buffer out by setting the variable
23142356
;; buffer-locally to nil (as documented in the hl-line.el commentary).
23152357
(when (bound-and-true-p global-hl-line-mode)
@@ -2320,12 +2362,7 @@ wheel events reach ghostel's own scroll commands."
23202362
;; Buffer-local hl-line-mode
23212363
(when (bound-and-true-p hl-line-mode)
23222364
(setq ghostel--saved-hl-line-mode t)
2323-
(hl-line-mode -1))
2324-
;; pixel-scroll-precision-mode: setting the variable buffer-locally to nil
2325-
;; makes Emacs skip its minor-mode-map-alist entry for this buffer, so
2326-
;; wheel-up/wheel-down reach ghostel-mode-map instead.
2327-
(when (bound-and-true-p pixel-scroll-precision-mode)
2328-
(setq-local pixel-scroll-precision-mode nil)))
2365+
(hl-line-mode -1)))
23292366

23302367

23312368
;;; Entry point

test/ghostel-test.el

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2458,65 +2458,71 @@ buffer and hand nil to the native module."
24582458
(should-not scroll-bottom-called)
24592459
(should-not ghostel--force-next-redraw))))
24602460

2461-
(ert-deftest ghostel-test-scroll-forwards-mouse-tracking ()
2462-
"Scroll-up/down forward events when mouse tracking is active."
2461+
(ert-deftest ghostel-test-scroll-intercept-forwards-mouse-tracking ()
2462+
"Scroll intercept forwards events when mouse tracking is active."
24632463
(let ((ghostel--term 'fake)
24642464
(ghostel--process 'fake)
24652465
(ghostel--copy-mode-active nil)
2466+
(ghostel--scroll-intercept-active t)
24662467
(mouse-event-args nil)
2467-
(scroll-called nil)
24682468
;; Fake wheel-up event at row 5, col 10
24692469
(fake-event `(wheel-up (,(selected-window) 1 (10 . 5) 0))))
24702470
;; Mouse tracking active: ghostel--mouse-event returns non-nil
24712471
(cl-letf (((symbol-function 'ghostel--mouse-event)
24722472
(lambda (_term action button row col mods)
24732473
(setq mouse-event-args (list action button row col mods))
24742474
t))
2475-
((symbol-function 'scroll-down)
2476-
(lambda (&optional _) (setq scroll-called t)))
24772475
((symbol-function 'process-live-p) (lambda (_p) t)))
2478-
(ghostel--scroll-up fake-event)
2476+
(ghostel--scroll-intercept-up fake-event)
24792477
(should mouse-event-args)
24802478
(should (equal 0 (nth 0 mouse-event-args))) ; action = press
24812479
(should (equal 4 (nth 1 mouse-event-args))) ; button 4 = scroll up
24822480
(should (equal 5 (nth 2 mouse-event-args))) ; row
24832481
(should (equal 10 (nth 3 mouse-event-args))) ; col
2484-
(should-not scroll-called))
2482+
;; Event should NOT be re-dispatched
2483+
(should ghostel--scroll-intercept-active)
2484+
(should-not unread-command-events))
24852485
;; Reset and test scroll-down with a wheel-down event
2486-
(setq mouse-event-args nil scroll-called nil)
2486+
(setq mouse-event-args nil)
24872487
(let ((fake-down-event `(wheel-down (,(selected-window) 1 (10 . 5) 0))))
24882488
(cl-letf (((symbol-function 'ghostel--mouse-event)
24892489
(lambda (_term action button row col mods)
24902490
(setq mouse-event-args (list action button row col mods))
24912491
t))
2492-
((symbol-function 'scroll-up)
2493-
(lambda (&optional _) (setq scroll-called t)))
24942492
((symbol-function 'process-live-p) (lambda (_p) t)))
2495-
(ghostel--scroll-down fake-down-event)
2493+
(ghostel--scroll-intercept-down fake-down-event)
24962494
(should mouse-event-args)
24972495
(should (equal 5 (nth 1 mouse-event-args))) ; button 5 = scroll down
2498-
(should-not scroll-called)))))
2496+
(should ghostel--scroll-intercept-active)
2497+
(should-not unread-command-events)))))
24992498

2500-
(ert-deftest ghostel-test-scroll-fallback-no-mouse-tracking ()
2501-
"Scroll-up/down fall back to Emacs window scroll when mouse tracking is off."
2499+
(ert-deftest ghostel-test-scroll-intercept-fallthrough ()
2500+
"Scroll intercept re-dispatches when mouse tracking is off."
25022501
(let ((ghostel--term 'fake)
25032502
(ghostel--process 'fake)
25042503
(ghostel--copy-mode-active nil)
2505-
(scroll-down-arg nil)
2506-
(scroll-up-arg nil)
2504+
(ghostel--scroll-intercept-active t)
25072505
(fake-up-event `(wheel-up (,(selected-window) 1 (10 . 5) 0)))
25082506
(fake-down-event `(wheel-down (,(selected-window) 1 (10 . 5) 0))))
2507+
;; Mouse tracking off: ghostel--mouse-event returns nil
25092508
(cl-letf (((symbol-function 'ghostel--mouse-event)
25102509
(lambda (_term _action _button _row _col _mods) nil))
2511-
((symbol-function 'scroll-down)
2512-
(lambda (&optional n) (setq scroll-down-arg n)))
2513-
((symbol-function 'scroll-up)
2514-
(lambda (&optional n) (setq scroll-up-arg n)))
25152510
((symbol-function 'process-live-p) (lambda (_p) t)))
2516-
(ghostel--scroll-up fake-up-event)
2517-
(should (equal 3 scroll-down-arg))
2518-
(ghostel--scroll-down fake-down-event)
2519-
(should (equal 3 scroll-up-arg)))))
2511+
;; Test wheel-up re-dispatch
2512+
(ghostel--scroll-intercept-up fake-up-event)
2513+
;; Intercept should be disabled so the event loop skips our map
2514+
(should-not ghostel--scroll-intercept-active)
2515+
;; Event should be pushed back for re-processing
2516+
(should (equal fake-up-event (car unread-command-events)))
2517+
;; Clean up for next assertion
2518+
(setq unread-command-events nil)
2519+
(ghostel--reenable-scroll-intercept)
2520+
;; Test wheel-down re-dispatch
2521+
(ghostel--scroll-intercept-down fake-down-event)
2522+
(should-not ghostel--scroll-intercept-active)
2523+
(should (equal fake-down-event (car unread-command-events)))
2524+
(setq unread-command-events nil)
2525+
(ghostel--reenable-scroll-intercept))))
25202526

25212527
(ert-deftest ghostel-test-control-key-bindings ()
25222528
"All non-exception C-<letter> keys should be bound in ghostel-mode-map."
@@ -2971,8 +2977,8 @@ while :; do sleep 0.1; done'\n")
29712977
ghostel-test-scroll-on-input-self-insert
29722978
ghostel-test-scroll-on-input-send-event
29732979
ghostel-test-scroll-on-input-disabled
2974-
ghostel-test-scroll-forwards-mouse-tracking
2975-
ghostel-test-scroll-fallback-no-mouse-tracking
2980+
ghostel-test-scroll-intercept-forwards-mouse-tracking
2981+
ghostel-test-scroll-intercept-fallthrough
29762982
ghostel-test-control-key-bindings
29772983
ghostel-test-meta-key-bindings
29782984
ghostel-test-copy-mode-recenter

0 commit comments

Comments
 (0)