summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-02-20 13:31:36 -0600
committerCraig Jennings <c@cjennings.net>2026-02-20 13:31:36 -0600
commite7b8c249b67ee999e506918a10ce7dd56e45ded0 (patch)
tree013325614e95071aecb2de9011bf974f66cea18d
parenta180ba28b966f080d2692d15eef6fb387c074ac3 (diff)
fix(mousetrap): use buffer-local keymaps via emulation-mode-map-alists
minor-mode-map-alist is global, so the last buffer to set its keymap won for all buffers. Dashboard's scroll+primary profile was immediately overwritten by other buffers' disabled profile. Switch to emulation-mode-map-alists with a buffer-local variable so each buffer gets its own independent keymap. Fixes dashboard click navigation.
-rw-r--r--modules/mousetrap-mode.el45
-rw-r--r--tests/test-integration-mousetrap-mode-lighter-click.el14
2 files changed, 41 insertions, 18 deletions
diff --git a/modules/mousetrap-mode.el b/modules/mousetrap-mode.el
index 0df08d7c..d7f422de 100644
--- a/modules/mousetrap-mode.el
+++ b/modules/mousetrap-mode.el
@@ -15,6 +15,9 @@
;; change profiles or mode mappings and re-enable the mode without reloading
;; your Emacs configuration.
;;
+;; Keymaps are buffer-local via `emulation-mode-map-alists', so each buffer
+;; gets the correct profile for its major mode independently.
+;;
;; Inspired by this blog post from Malabarba
;; https://endlessparentheses.com/disable-mouse-only-inside-emacs.html
;;
@@ -127,11 +130,19 @@ the mode is toggled, allowing dynamic behavior without reloading config."
(define-key map (kbd (format "<%s%s-%d>" pref type button)) #'ignore)))))))))
map))
-;;; Minor Mode Definition
+;;; Buffer-local keymap via emulation-mode-map-alists
(defvar-local mouse-trap-mode-map nil
"Keymap for `mouse-trap-mode'. Built dynamically per buffer.")
+(defvar-local mouse-trap--emulation-alist nil
+ "Buffer-local alist for mouse-trap keymap lookup.
+Used via `emulation-mode-map-alists' so each buffer gets its own keymap.")
+
+(add-to-list 'emulation-mode-map-alists 'mouse-trap--emulation-alist)
+
+;;; Minor Mode Definition
+
(defvar mouse-trap--lighter-keymap
(let ((map (make-sparse-keymap)))
(define-key map [mode-line mouse-1]
@@ -174,20 +185,17 @@ See `mouse-trap-profiles' for available profiles and
`mouse-trap-mode-profiles' for mode mappings."
:lighter nil ; We use mode-line-misc-info instead
:group 'convenience
- ;; Build keymap dynamically when mode is activated
(if mouse-trap-mode
(progn
+ ;; Build buffer-local keymap and register via emulation layer
(setq mouse-trap-mode-map (mouse-trap--build-keymap))
- ;; Register keymap so Emacs actually uses it for key dispatch
- (let ((entry (assq 'mouse-trap-mode minor-mode-map-alist)))
- (if entry
- (setcdr entry mouse-trap-mode-map)
- (push (cons 'mouse-trap-mode mouse-trap-mode-map) minor-mode-map-alist)))
+ (setq mouse-trap--emulation-alist
+ `((mouse-trap-mode . ,mouse-trap-mode-map)))
;; Add dynamic lighter to mode-line-misc-info (always visible)
(unless (member '(:eval (mouse-trap--lighter-string)) mode-line-misc-info)
(push '(:eval (mouse-trap--lighter-string)) mode-line-misc-info)))
- ;; When disabling, remove keymap from minor-mode-map-alist
- (setq minor-mode-map-alist (assq-delete-all 'mouse-trap-mode minor-mode-map-alist))
+ ;; When disabling, clear the buffer-local emulation alist and keymap
+ (setq mouse-trap--emulation-alist nil)
(setq mouse-trap-mode-map nil)
;; Note: We keep the lighter in mode-line-misc-info so it shows 🐭 when disabled
))
@@ -200,15 +208,30 @@ These modes are excluded from automatic activation via hooks, but you
can still manually enable mouse-trap-mode in these buffers if desired.")
(defun mouse-trap-maybe-enable ()
- "Enable `mouse-trap-mode' unless in an excluded mode."
+ "Enable `mouse-trap-mode' unless in an excluded mode.
+If already enabled, rebuild the keymap for the current major mode.
+This handles derived modes whose parent hook fires before `major-mode'
+is set to the child (e.g. special-mode-hook runs before dashboard-mode)."
(unless (apply #'derived-mode-p mouse-trap-excluded-modes)
- (mouse-trap-mode 1)))
+ (if mouse-trap-mode
+ ;; Already on — rebuild keymap for the (possibly changed) major mode
+ (progn
+ (setq mouse-trap-mode-map (mouse-trap--build-keymap))
+ (setq mouse-trap--emulation-alist
+ `((mouse-trap-mode . ,mouse-trap-mode-map))))
+ (mouse-trap-mode 1))))
;; Enable in text, prog, and special modes
(add-hook 'text-mode-hook #'mouse-trap-maybe-enable)
(add-hook 'prog-mode-hook #'mouse-trap-maybe-enable)
(add-hook 'special-mode-hook #'mouse-trap-maybe-enable)
+;; Derived modes with custom profiles need their own hooks to rebuild the
+;; keymap after major-mode is set (parent hooks fire before the child sets it).
+(dolist (mode (mapcar #'car mouse-trap-mode-profiles))
+ (let ((hook (intern (format "%s-hook" mode))))
+ (add-hook hook #'mouse-trap-maybe-enable)))
+
(keymap-global-set "C-c M" #'mouse-trap-mode)
(with-eval-after-load 'which-key
diff --git a/tests/test-integration-mousetrap-mode-lighter-click.el b/tests/test-integration-mousetrap-mode-lighter-click.el
index b9f32fda..252e0cf5 100644
--- a/tests/test-integration-mousetrap-mode-lighter-click.el
+++ b/tests/test-integration-mousetrap-mode-lighter-click.el
@@ -35,7 +35,7 @@ Dashboard uses scroll+primary profile which allows scrolling and mouse-1."
(should (eq (lookup-key mouse-trap-mode-map (kbd "<wheel-up>")) nil))
;; Keymap should be in minor-mode-map-alist
- (should (assq 'mouse-trap-mode minor-mode-map-alist)))))
+ (should (assq 'mouse-trap-mode mouse-trap--emulation-alist)))))
(ert-deftest test-integration-lighter-click-disables-mode ()
"Test clicking lighter when mode is enabled disables it and removes keymap."
@@ -43,7 +43,7 @@ Dashboard uses scroll+primary profile which allows scrolling and mouse-1."
(emacs-lisp-mode)
(mouse-trap-mode 1)
(should mouse-trap-mode)
- (should (assq 'mouse-trap-mode minor-mode-map-alist))
+ (should (assq 'mouse-trap-mode mouse-trap--emulation-alist))
;; Simulate clicking lighter to disable
(mouse-trap-mode -1)
@@ -52,7 +52,7 @@ Dashboard uses scroll+primary profile which allows scrolling and mouse-1."
(should-not mouse-trap-mode)
;; Keymap should be removed from minor-mode-map-alist
- (should-not (assq 'mouse-trap-mode minor-mode-map-alist))))
+ (should-not (assq 'mouse-trap-mode mouse-trap--emulation-alist))))
(ert-deftest test-integration-lighter-click-toggle-updates-keymap ()
"Test toggling mode via lighter click rebuilds keymap for current mode.
@@ -142,12 +142,12 @@ Auto-enable is blocked, but manual toggle should still work."
;; But manual toggle should work
(mouse-trap-mode 1)
(should mouse-trap-mode)
- (should (assq 'mouse-trap-mode minor-mode-map-alist))
+ (should (assq 'mouse-trap-mode mouse-trap--emulation-alist))
;; Toggle off
(mouse-trap-mode -1)
(should-not mouse-trap-mode)
- (should-not (assq 'mouse-trap-mode minor-mode-map-alist))))
+ (should-not (assq 'mouse-trap-mode mouse-trap--emulation-alist))))
(ert-deftest test-integration-lighter-click-multiple-rapid-toggles ()
"Test rapid clicking (multiple toggles) is stable and doesn't corrupt state."
@@ -162,12 +162,12 @@ Auto-enable is blocked, but manual toggle should still work."
;; Should end in disabled state (even number of toggles)
(should-not mouse-trap-mode)
- (should-not (assq 'mouse-trap-mode minor-mode-map-alist))
+ (should-not (assq 'mouse-trap-mode mouse-trap--emulation-alist))
;; Enable one more time to end enabled
(mouse-trap-mode 1)
(should mouse-trap-mode)
- (should (assq 'mouse-trap-mode minor-mode-map-alist))
+ (should (assq 'mouse-trap-mode mouse-trap--emulation-alist))
(should (keymapp mouse-trap-mode-map))))
(provide 'test-integration-mousetrap-mode-lighter-click)