refactor: Python ctypes daemon for macOS Zoom (no external tools)

This commit is contained in:
2026-02-22 22:24:30 +01:00
parent c6b45f6867
commit ce3c7b3127
3 changed files with 239 additions and 150 deletions

View File

@@ -888,64 +888,73 @@ Keeps the status bar and tab bar fully visible at any zoom level.")
;;; ============================================================
;;; ACCESSIBILITY — MACOS ZOOM CURSOR TRACKING (SPC z t)
;;; ============================================================
;; 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).
;; Python daemon (scripts/macos-zoom-daemon.py) čte frame-relative
;; souřadnice kurzoru ze stdin, získá přesnou pozici Emacs okna přes
;; macOS AX API, a postne real CGEvent.mouseMoved → macOS Zoom sleduje.
;;
;; Debounced via run-with-idle-timer 0.05s: rapid j/k spam produces one write.
;; CCM compatible: idle timer fires after ccm recenter + redisplay.
;; Výhody oproti přímému Emacs warpu:
;; - CGEventPost (ne CGWarpMouseCursorPosition) → Zoom skutečně reaguje
;; - Window origin z AX API (ne buggy frame-position Emacsu)
;; - Debounce 50ms: rapid j/k spam → jeden event
;;
;; 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/
;; SETUP macOS:
;; System Settings → Accessibility → Zoom → Full Screen
;; "When zoomed in, screen image moves: Continuously with pointer"
;; Privacy & Security → Accessibility → povolit Terminal (nebo Emacs)
;;
;; SPC z t toggle cursor tracking on/off
;; SPC z t toggle cursor tracking (spustí/zastaví daemon)
(defvar my/warp-timer nil "Debounce timer for cursor warp.")
(defvar my/zoom-daemon-process nil "Subprocess: macos-zoom-daemon.py.")
(defvar my/zoom-warp-timer nil "Debounce timer pro cursor tracking.")
(defvar my/zoom-daemon-script
(expand-file-name "scripts/macos-zoom-daemon.py" doom-private-dir)
"Cesta k macos-zoom-daemon.py.")
(defvar my/cursor-pos-file (expand-file-name "~/.emacs-cursor-pos")
"File path for Hammerspoon cursor tracking IPC.")
(defun my/zoom-daemon-start ()
"Spustí Python cursor tracking daemon."
(when (and (display-graphic-p) (file-exists-p my/zoom-daemon-script))
(setq my/zoom-daemon-process
(start-process "macos-zoom-daemon" nil
"python3" my/zoom-daemon-script))
(set-process-query-on-exit-flag my/zoom-daemon-process nil)
(message "Cursor tracking ON")))
(defun my/warp-mouse-to-cursor ()
"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
(defun my/zoom-daemon-stop ()
"Zastaví Python cursor tracking daemon."
(when (and my/zoom-daemon-process (process-live-p my/zoom-daemon-process))
(delete-process my/zoom-daemon-process))
(setq my/zoom-daemon-process nil)
(message "Cursor tracking OFF"))
(defun my/zoom-send-cursor-pos ()
"Pošle frame-relative pozici kurzoru do daemon stdin (debounced 50ms)."
(when my/zoom-warp-timer (cancel-timer my/zoom-warp-timer))
(setq my/zoom-warp-timer
(run-with-idle-timer 0.05 nil
(lambda ()
(setq my/warp-timer nil)
(when (display-graphic-p)
(when-let ((pos (window-absolute-pixel-position)))
;; 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."
(when (display-graphic-p)
(ignore-errors
(let ((p (frame-position)))
(set-frame-position (selected-frame) (car p) (cdr p))))))
(setq my/zoom-warp-timer nil)
(when (and my/zoom-daemon-process
(process-live-p my/zoom-daemon-process)
(display-graphic-p))
(let* ((win (selected-window))
(vis (pos-visible-in-window-p (window-point win) win t)))
(when (and vis (listp vis))
(let* ((edges (window-pixel-edges win))
(x (+ (nth 0 edges) (nth 0 vis)))
(y (+ (nth 1 edges) (nth 1 vis)
(/ (line-pixel-height) 2))))
(process-send-string my/zoom-daemon-process
(format "%d %d\n" x y))))))))))
(define-minor-mode my/cursor-zoom-mode
"Warp mouse to text cursor for macOS Zoom accessibility tracking."
"Cursor tracking pro macOS Zoom (via Python daemon)."
:global t
(if my/cursor-zoom-mode
(progn
(add-hook 'post-command-hook #'my/warp-mouse-to-cursor)
(add-hook 'window-size-change-functions
(lambda (_) (my/refresh-frame-position)))
(run-with-timer 60 60 #'my/refresh-frame-position))
(remove-hook 'post-command-hook #'my/warp-mouse-to-cursor)))
;; Enable by default
(my/cursor-zoom-mode 1)
(my/zoom-daemon-start)
(add-hook 'post-command-hook #'my/zoom-send-cursor-pos))
(remove-hook 'post-command-hook #'my/zoom-send-cursor-pos)
(my/zoom-daemon-stop)))
;; --------------- keybindings ---------------