feat: Hammerspoon IPC cursor tracking + split magnifier (SPC z m)

This commit is contained in:
2026-02-22 22:05:36 +01:00
parent 21b1637390
commit c6b45f6867
2 changed files with 197 additions and 7 deletions

View File

@@ -814,25 +814,102 @@ Keeps the status bar and tab bar fully visible at any zoom level.")
my/zoom-saved-steps nil)))
;;; ============================================================
;;; ACCESSIBILITY — SPLIT-SCREEN MAGNIFIER (SPC z m)
;;; ============================================================
;; Split view: LEFT 40% = normal editing, RIGHT 60% = zoomed indirect buffer.
;; The zoomed pane follows cursor position via post-command-hook sync.
;; Useful for presentations or low-vision pairing.
;;
;; SPC z m toggle split magnifier on/off
(defvar my/mag-zoom-level 4
"Text scale level for the magnified pane (4 ≈ 2× size).")
(defvar my/mag--active nil "Non-nil when split magnifier is enabled.")
(defvar my/mag--window nil "The magnified (right) window.")
(defvar my/mag--buffer nil "The indirect buffer used for magnification.")
(defun my/mag--sync ()
"Sync magnified pane to current cursor position."
(when (and my/mag--active
my/mag--window
(window-live-p my/mag--window)
my/mag--buffer
(buffer-live-p my/mag--buffer))
(let ((pt (point)))
(with-selected-window my/mag--window
(goto-char pt)
(recenter)))))
(defun my/mag--enable ()
"Enable split-screen magnifier."
(let* ((base-buf (current-buffer))
(mag-name (format "*mag:%s*" (buffer-name base-buf))))
;; Clean up stale buffer
(when-let ((old (get-buffer mag-name)))
(kill-buffer old))
(setq my/mag--buffer (make-indirect-buffer base-buf mag-name t))
;; Split: left 40%, right 60%
(delete-other-windows)
(setq my/mag--window
(split-window (selected-window)
(floor (* 0.4 (window-total-width)))
'right))
(set-window-buffer my/mag--window my/mag--buffer)
(with-selected-window my/mag--window
(text-scale-set my/mag-zoom-level)
(setq-local cursor-type nil)
(setq-local scroll-margin 0))
(setq my/mag--active t)
(add-hook 'post-command-hook #'my/mag--sync)
(message "Split magnifier ON (zoom %+d)" my/mag-zoom-level)))
(defun my/mag--disable ()
"Disable split-screen magnifier."
(remove-hook 'post-command-hook #'my/mag--sync)
(when (and my/mag--window (window-live-p my/mag--window))
(delete-window my/mag--window))
(when (and my/mag--buffer (buffer-live-p my/mag--buffer))
(kill-buffer my/mag--buffer))
(setq my/mag--active nil
my/mag--window nil
my/mag--buffer nil)
(message "Split magnifier OFF"))
(defun my/mag-toggle ()
"Toggle split-screen magnifier on/off."
(interactive)
(if my/mag--active
(my/mag--disable)
(my/mag--enable)))
;;; ============================================================
;;; ACCESSIBILITY — MACOS ZOOM CURSOR TRACKING (SPC z t)
;;; ============================================================
;; Warps mouse pointer to text cursor so macOS Zoom ("Follow mouse cursor")
;; tracks editing position. Uses window-absolute-pixel-position (Emacs 26+)
;; paired with set-mouse-absolute-pixel-position — no manual coordinate math.
;; Writes cursor screen position to ~/.emacs-cursor-pos for Hammerspoon.
;; Hammerspoon posts a real mouseMoved HID event — required for macOS Zoom
;; "Follow mouse cursor" (CGWarpMouseCursorPosition is silently ignored).
;;
;; Debounced via run-with-idle-timer 0.05s: rapid j/k spam produces one warp.
;; Debounced via run-with-idle-timer 0.05s: rapid j/k spam produces one write.
;; CCM compatible: idle timer fires after ccm recenter + redisplay.
;;
;; SETUP macOS: System Settings → Accessibility → Zoom → Full Screen
;; "When zoomed in, the screen image moves: Continuously with pointer"
;; SETUP Hammerspoon: copy hammerspoon/cursor-warp.lua to ~/.hammerspoon/
;;
;; SPC z t toggle cursor tracking on/off
(defvar my/warp-timer nil "Debounce timer for cursor warp.")
(defvar my/cursor-pos-file (expand-file-name "~/.emacs-cursor-pos")
"File path for Hammerspoon cursor tracking IPC.")
(defun my/warp-mouse-to-cursor ()
"Schedule debounced mouse warp to current cursor position."
"Write cursor screen position to file for Hammerspoon to pick up.
Hammerspoon posts a real mouseMoved HID event — required for macOS Zoom
'Follow mouse cursor' (CGWarpMouseCursorPosition is silently ignored by Zoom)."
(when my/warp-timer (cancel-timer my/warp-timer))
(setq my/warp-timer
(run-with-idle-timer 0.05 nil
@@ -840,7 +917,14 @@ Keeps the status bar and tab bar fully visible at any zoom level.")
(setq my/warp-timer nil)
(when (display-graphic-p)
(when-let ((pos (window-absolute-pixel-position)))
(set-mouse-absolute-pixel-position (car pos) (cdr pos))))))))
;; Write screen-absolute coords for Hammerspoon
;; Also write frame-position for Hammerspoon cross-check
(let* ((fp (frame-position))
(content (format "%d %d\n%d %d\n"
(car pos) (cdr pos)
(car fp) (cdr fp))))
(with-temp-file my/cursor-pos-file
(insert content)))))))))
(defun my/refresh-frame-position ()
"Workaround for bug#71912: stale frame position after sleep/fullscreen."
@@ -872,7 +956,8 @@ Keeps the status bar and tab bar fully visible at any zoom level.")
:desc "Zoom out (÷1.5)" "-" #'my/zoom-out
:desc "Reset na výchozí" "0" #'my/zoom-reset
:desc "Restore předchozí" "z" #'my/zoom-restore
:desc "Toggle cursor track" "t" #'my/cursor-zoom-mode))
:desc "Toggle cursor track" "t" #'my/cursor-zoom-mode
:desc "Split magnifier" "m" #'my/mag-toggle))
;;; ============================================================