feat: full-featured Emacs-native magnifier, remove macOS zoom daemon
This commit is contained in:
240
config.el
240
config.el
@@ -817,64 +817,114 @@ Keeps the status bar and tab bar fully visible at any zoom level.")
|
||||
;;; ============================================================
|
||||
;;; 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.
|
||||
;; Pure Emacs-native split-screen magnifier. Žádné externí procesy, žádné macOS API.
|
||||
;;
|
||||
;; SPC z m toggle split magnifier on/off
|
||||
|
||||
(defvar my/mag-zoom-level 4
|
||||
"Text scale level for the magnified pane (4 ≈ 2× size).")
|
||||
;; Architektura:
|
||||
;; LEFT pane (40%): normální editace — zde pracuješ
|
||||
;; RIGHT pane (60%): zvětšený pohled — indirect buffer, sleduje cursor
|
||||
;;
|
||||
;; Funkce:
|
||||
;; - Sleduje cursor v aktivním okně (post-command-hook)
|
||||
;; - Přepíná automaticky při změně bufferu/okna (buffer switch)
|
||||
;; - SPC z + / SPC z = zoom in (jen magnifier, ne globálně)
|
||||
;; - SPC z - zoom out (jen magnifier)
|
||||
;; - SPC z 0 reset zoom magnifieru na výchozí
|
||||
;; - SPC z m toggle on/off
|
||||
;; - Všechny zoom příkazy jsou no-op pokud magnifier vypnut
|
||||
;; - Kurzor v magnifier pane skrytý (cursor-type nil)
|
||||
;; - Magnifier pane je read-only zobrazení (žádné edit v indirect buf)
|
||||
;; - Správné cleanup při window-configuration-change
|
||||
|
||||
(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.")
|
||||
(defvar my/mag--buffer nil "Current indirect buffer for magnification.")
|
||||
(defvar my/mag--source nil "Buffer currently being magnified.")
|
||||
(defvar my/mag--zoom-level 4 "Current text-scale-set value for magnifier (4 ≈ 2× default).")
|
||||
(defvar my/mag--zoom-default 4 "Default zoom level for reset.")
|
||||
|
||||
(defun my/mag--kill-indirect ()
|
||||
"Kill the current magnifier indirect buffer if it exists."
|
||||
(when (and my/mag--buffer (buffer-live-p my/mag--buffer))
|
||||
(kill-buffer my/mag--buffer))
|
||||
(setq my/mag--buffer nil))
|
||||
|
||||
(defun my/mag--switch-source ()
|
||||
"Switch magnifier to track the current buffer."
|
||||
(let* ((new-source (current-buffer))
|
||||
(mag-name (format "*mag:%s*" (buffer-name new-source))))
|
||||
(my/mag--kill-indirect)
|
||||
(setq my/mag--source new-source)
|
||||
(setq my/mag--buffer (make-indirect-buffer new-source mag-name t))
|
||||
(when (and my/mag--window (window-live-p my/mag--window))
|
||||
(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)))))
|
||||
|
||||
(defun my/mag--sync ()
|
||||
"Sync magnified pane to current cursor position."
|
||||
(when my/mag--active
|
||||
;; Ignore if we're in the magnifier pane itself
|
||||
(when (eq (selected-window) my/mag--window)
|
||||
(cl-return-from my/mag--sync))
|
||||
;; Check magnifier window is still alive
|
||||
(unless (and my/mag--window (window-live-p my/mag--window))
|
||||
(cl-return-from my/mag--sync))
|
||||
;; Switch source if buffer changed
|
||||
(unless (eq (current-buffer) my/mag--source)
|
||||
(my/mag--switch-source))
|
||||
;; Sync point + recenter
|
||||
(when (and 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--on-window-change ()
|
||||
"Clean up if magnifier window was closed by user."
|
||||
(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)))))
|
||||
(not (and my/mag--window (window-live-p my/mag--window))))
|
||||
(my/mag--disable)))
|
||||
|
||||
(defun my/mag--enable ()
|
||||
"Enable split-screen magnifier."
|
||||
(let* ((base-buf (current-buffer))
|
||||
(mag-name (format "*mag:%s*" (buffer-name base-buf))))
|
||||
(delete-other-windows)
|
||||
(setq my/mag--source (current-buffer))
|
||||
(let ((mag-name (format "*mag:%s*" (buffer-name my/mag--source))))
|
||||
;; 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)))
|
||||
(setq my/mag--buffer (make-indirect-buffer my/mag--source mag-name t)))
|
||||
;; Split: left 40%, right 60%
|
||||
(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)
|
||||
(add-hook 'window-configuration-change-hook #'my/mag--on-window-change)
|
||||
(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)
|
||||
(remove-hook 'window-configuration-change-hook #'my/mag--on-window-change)
|
||||
(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))
|
||||
;; Kill all *mag:* buffers
|
||||
(dolist (buf (buffer-list))
|
||||
(when (string-prefix-p "*mag:" (buffer-name buf))
|
||||
(kill-buffer buf)))
|
||||
(setq my/mag--active nil
|
||||
my/mag--window nil
|
||||
my/mag--buffer nil)
|
||||
my/mag--buffer nil
|
||||
my/mag--source nil)
|
||||
(message "Split magnifier OFF"))
|
||||
|
||||
(defun my/mag-toggle ()
|
||||
@@ -884,89 +934,61 @@ Keeps the status bar and tab bar fully visible at any zoom level.")
|
||||
(my/mag--disable)
|
||||
(my/mag--enable)))
|
||||
|
||||
(defun my/mag-zoom-in ()
|
||||
"Increase magnifier zoom level."
|
||||
(interactive)
|
||||
(when my/mag--active
|
||||
(cl-incf my/mag--zoom-level)
|
||||
(when (and my/mag--window (window-live-p my/mag--window))
|
||||
(with-selected-window my/mag--window
|
||||
(text-scale-set my/mag--zoom-level)))
|
||||
(message "Magnifier zoom %+d" my/mag--zoom-level)))
|
||||
|
||||
;;; ============================================================
|
||||
;;; ACCESSIBILITY — MACOS ZOOM CURSOR TRACKING (SPC z t)
|
||||
;;; ============================================================
|
||||
;; 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.
|
||||
;;
|
||||
;; 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, screen image moves: Continuously with pointer"
|
||||
;; Privacy & Security → Accessibility → povolit Terminal (nebo Emacs)
|
||||
;;
|
||||
;; SPC z t toggle cursor tracking (spustí/zastaví daemon)
|
||||
(defun my/mag-zoom-out ()
|
||||
"Decrease magnifier zoom level."
|
||||
(interactive)
|
||||
(when my/mag--active
|
||||
(cl-decf my/mag--zoom-level)
|
||||
(when (and my/mag--window (window-live-p my/mag--window))
|
||||
(with-selected-window my/mag--window
|
||||
(text-scale-set my/mag--zoom-level)))
|
||||
(message "Magnifier zoom %+d" my/mag--zoom-level)))
|
||||
|
||||
(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.")
|
||||
(defun my/mag-zoom-reset ()
|
||||
"Reset magnifier zoom to default."
|
||||
(interactive)
|
||||
(when my/mag--active
|
||||
(setq my/mag--zoom-level my/mag--zoom-default)
|
||||
(when (and my/mag--window (window-live-p my/mag--window))
|
||||
(with-selected-window my/mag--window
|
||||
(text-scale-set my/mag--zoom-level)))
|
||||
(message "Magnifier zoom reset to %+d" my/mag--zoom-level)))
|
||||
|
||||
(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/mag-or-global-zoom-in ()
|
||||
"Zoom in: magnifier if active, otherwise global."
|
||||
(interactive)
|
||||
(if my/mag--active (my/mag-zoom-in) (my/zoom-in)))
|
||||
|
||||
(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/mag-or-global-zoom-out ()
|
||||
"Zoom out: magnifier if active, otherwise global."
|
||||
(interactive)
|
||||
(if my/mag--active (my/mag-zoom-out) (my/zoom-out)))
|
||||
|
||||
(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/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
|
||||
"Cursor tracking pro macOS Zoom (via Python daemon)."
|
||||
:global t
|
||||
(if my/cursor-zoom-mode
|
||||
(progn
|
||||
(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)))
|
||||
(defun my/mag-or-global-zoom-reset ()
|
||||
"Reset zoom: magnifier if active, otherwise global."
|
||||
(interactive)
|
||||
(if my/mag--active (my/mag-zoom-reset) (my/zoom-reset)))
|
||||
|
||||
;; --------------- keybindings ---------------
|
||||
|
||||
(map! :leader
|
||||
(:prefix ("z" . "zoom")
|
||||
:desc "Zoom in (×1.5)" "+" #'my/zoom-in
|
||||
:desc "Zoom in (×1.5)" "=" #'my/zoom-in
|
||||
: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 "Split magnifier" "m" #'my/mag-toggle))
|
||||
:desc "Zoom in (global ×1.5)" "+" #'my/mag-or-global-zoom-in
|
||||
:desc "Zoom in (global ×1.5)" "=" #'my/mag-or-global-zoom-in
|
||||
:desc "Zoom out (global ÷1.5)" "-" #'my/mag-or-global-zoom-out
|
||||
:desc "Reset zoom" "0" #'my/mag-or-global-zoom-reset
|
||||
:desc "Restore global zoom" "z" #'my/zoom-restore
|
||||
:desc "Split magnifier" "m" #'my/mag-toggle))
|
||||
|
||||
|
||||
;;; ============================================================
|
||||
|
||||
Reference in New Issue
Block a user