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)
|
;;; ACCESSIBILITY — SPLIT-SCREEN MAGNIFIER (SPC z m)
|
||||||
;;; ============================================================
|
;;; ============================================================
|
||||||
;; Split view: LEFT 40% = normal editing, RIGHT 60% = zoomed indirect buffer.
|
;; Pure Emacs-native split-screen magnifier. Žádné externí procesy, žádné macOS API.
|
||||||
;; 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
|
;; Architektura:
|
||||||
|
;; LEFT pane (40%): normální editace — zde pracuješ
|
||||||
(defvar my/mag-zoom-level 4
|
;; RIGHT pane (60%): zvětšený pohled — indirect buffer, sleduje cursor
|
||||||
"Text scale level for the magnified pane (4 ≈ 2× size).")
|
;;
|
||||||
|
;; 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--active nil "Non-nil when split magnifier is enabled.")
|
||||||
(defvar my/mag--window nil "The magnified (right) window.")
|
(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 ()
|
(defun my/mag--sync ()
|
||||||
"Sync magnified pane to current cursor position."
|
"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
|
(when (and my/mag--active
|
||||||
my/mag--window
|
(not (and my/mag--window (window-live-p my/mag--window))))
|
||||||
(window-live-p my/mag--window)
|
(my/mag--disable)))
|
||||||
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 ()
|
(defun my/mag--enable ()
|
||||||
"Enable split-screen magnifier."
|
"Enable split-screen magnifier."
|
||||||
(let* ((base-buf (current-buffer))
|
(delete-other-windows)
|
||||||
(mag-name (format "*mag:%s*" (buffer-name base-buf))))
|
(setq my/mag--source (current-buffer))
|
||||||
|
(let ((mag-name (format "*mag:%s*" (buffer-name my/mag--source))))
|
||||||
;; Clean up stale buffer
|
;; Clean up stale buffer
|
||||||
(when-let ((old (get-buffer mag-name)))
|
(when-let ((old (get-buffer mag-name)))
|
||||||
(kill-buffer old))
|
(kill-buffer old))
|
||||||
(setq my/mag--buffer (make-indirect-buffer base-buf mag-name t))
|
(setq my/mag--buffer (make-indirect-buffer my/mag--source mag-name t)))
|
||||||
;; Split: left 40%, right 60%
|
;; Split: left 40%, right 60%
|
||||||
(delete-other-windows)
|
(setq my/mag--window
|
||||||
(setq my/mag--window
|
(split-window (selected-window)
|
||||||
(split-window (selected-window)
|
(floor (* 0.4 (window-total-width)))
|
||||||
(floor (* 0.4 (window-total-width)))
|
'right))
|
||||||
'right))
|
(set-window-buffer my/mag--window my/mag--buffer)
|
||||||
(set-window-buffer my/mag--window my/mag--buffer)
|
(with-selected-window my/mag--window
|
||||||
(with-selected-window my/mag--window
|
(text-scale-set my/mag--zoom-level)
|
||||||
(text-scale-set my/mag-zoom-level)
|
(setq-local cursor-type nil)
|
||||||
(setq-local cursor-type nil)
|
(setq-local scroll-margin 0))
|
||||||
(setq-local scroll-margin 0))
|
(setq my/mag--active t)
|
||||||
(setq my/mag--active t)
|
(add-hook 'post-command-hook #'my/mag--sync)
|
||||||
(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)))
|
(message "Split magnifier ON (zoom %+d)" my/mag--zoom-level))
|
||||||
|
|
||||||
(defun my/mag--disable ()
|
(defun my/mag--disable ()
|
||||||
"Disable split-screen magnifier."
|
"Disable split-screen magnifier."
|
||||||
(remove-hook 'post-command-hook #'my/mag--sync)
|
(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))
|
(when (and my/mag--window (window-live-p my/mag--window))
|
||||||
(delete-window my/mag--window))
|
(delete-window my/mag--window))
|
||||||
(when (and my/mag--buffer (buffer-live-p my/mag--buffer))
|
;; Kill all *mag:* buffers
|
||||||
(kill-buffer my/mag--buffer))
|
(dolist (buf (buffer-list))
|
||||||
|
(when (string-prefix-p "*mag:" (buffer-name buf))
|
||||||
|
(kill-buffer buf)))
|
||||||
(setq my/mag--active nil
|
(setq my/mag--active nil
|
||||||
my/mag--window nil
|
my/mag--window nil
|
||||||
my/mag--buffer nil)
|
my/mag--buffer nil
|
||||||
|
my/mag--source nil)
|
||||||
(message "Split magnifier OFF"))
|
(message "Split magnifier OFF"))
|
||||||
|
|
||||||
(defun my/mag-toggle ()
|
(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--disable)
|
||||||
(my/mag--enable)))
|
(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)))
|
||||||
|
|
||||||
;;; ============================================================
|
(defun my/mag-zoom-out ()
|
||||||
;;; ACCESSIBILITY — MACOS ZOOM CURSOR TRACKING (SPC z t)
|
"Decrease magnifier zoom level."
|
||||||
;;; ============================================================
|
(interactive)
|
||||||
;; Python daemon (scripts/macos-zoom-daemon.py) čte frame-relative
|
(when my/mag--active
|
||||||
;; souřadnice kurzoru ze stdin, získá přesnou pozici Emacs okna přes
|
(cl-decf my/mag--zoom-level)
|
||||||
;; macOS AX API, a postne real CGEvent.mouseMoved → macOS Zoom sleduje.
|
(when (and my/mag--window (window-live-p my/mag--window))
|
||||||
;;
|
(with-selected-window my/mag--window
|
||||||
;; Výhody oproti přímému Emacs warpu:
|
(text-scale-set my/mag--zoom-level)))
|
||||||
;; - CGEventPost (ne CGWarpMouseCursorPosition) → Zoom skutečně reaguje
|
(message "Magnifier zoom %+d" my/mag--zoom-level)))
|
||||||
;; - 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)
|
|
||||||
|
|
||||||
(defvar my/zoom-daemon-process nil "Subprocess: macos-zoom-daemon.py.")
|
(defun my/mag-zoom-reset ()
|
||||||
(defvar my/zoom-warp-timer nil "Debounce timer pro cursor tracking.")
|
"Reset magnifier zoom to default."
|
||||||
(defvar my/zoom-daemon-script
|
(interactive)
|
||||||
(expand-file-name "scripts/macos-zoom-daemon.py" doom-private-dir)
|
(when my/mag--active
|
||||||
"Cesta k macos-zoom-daemon.py.")
|
(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 ()
|
(defun my/mag-or-global-zoom-in ()
|
||||||
"Spustí Python cursor tracking daemon."
|
"Zoom in: magnifier if active, otherwise global."
|
||||||
(when (and (display-graphic-p) (file-exists-p my/zoom-daemon-script))
|
(interactive)
|
||||||
(setq my/zoom-daemon-process
|
(if my/mag--active (my/mag-zoom-in) (my/zoom-in)))
|
||||||
(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/zoom-daemon-stop ()
|
(defun my/mag-or-global-zoom-out ()
|
||||||
"Zastaví Python cursor tracking daemon."
|
"Zoom out: magnifier if active, otherwise global."
|
||||||
(when (and my/zoom-daemon-process (process-live-p my/zoom-daemon-process))
|
(interactive)
|
||||||
(delete-process my/zoom-daemon-process))
|
(if my/mag--active (my/mag-zoom-out) (my/zoom-out)))
|
||||||
(setq my/zoom-daemon-process nil)
|
|
||||||
(message "Cursor tracking OFF"))
|
|
||||||
|
|
||||||
(defun my/zoom-send-cursor-pos ()
|
(defun my/mag-or-global-zoom-reset ()
|
||||||
"Pošle frame-relative pozici kurzoru do daemon stdin (debounced 50ms)."
|
"Reset zoom: magnifier if active, otherwise global."
|
||||||
(when my/zoom-warp-timer (cancel-timer my/zoom-warp-timer))
|
(interactive)
|
||||||
(setq my/zoom-warp-timer
|
(if my/mag--active (my/mag-zoom-reset) (my/zoom-reset)))
|
||||||
(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)))
|
|
||||||
|
|
||||||
;; --------------- keybindings ---------------
|
;; --------------- keybindings ---------------
|
||||||
|
|
||||||
(map! :leader
|
(map! :leader
|
||||||
(:prefix ("z" . "zoom")
|
(:prefix ("z" . "zoom")
|
||||||
:desc "Zoom in (×1.5)" "+" #'my/zoom-in
|
:desc "Zoom in (global ×1.5)" "+" #'my/mag-or-global-zoom-in
|
||||||
:desc "Zoom in (×1.5)" "=" #'my/zoom-in
|
:desc "Zoom in (global ×1.5)" "=" #'my/mag-or-global-zoom-in
|
||||||
:desc "Zoom out (÷1.5)" "-" #'my/zoom-out
|
:desc "Zoom out (global ÷1.5)" "-" #'my/mag-or-global-zoom-out
|
||||||
:desc "Reset na výchozí" "0" #'my/zoom-reset
|
:desc "Reset zoom" "0" #'my/mag-or-global-zoom-reset
|
||||||
:desc "Restore předchozí" "z" #'my/zoom-restore
|
:desc "Restore global zoom" "z" #'my/zoom-restore
|
||||||
:desc "Toggle cursor track" "t" #'my/cursor-zoom-mode
|
:desc "Split magnifier" "m" #'my/mag-toggle))
|
||||||
:desc "Split magnifier" "m" #'my/mag-toggle))
|
|
||||||
|
|
||||||
|
|
||||||
;;; ============================================================
|
;;; ============================================================
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""macOS Zoom cursor tracking daemon for Emacs.
|
|
||||||
|
|
||||||
Reads frame-relative cursor coordinates from stdin (sent by Emacs via
|
|
||||||
process-send-string), obtains the Emacs window position via macOS
|
|
||||||
Accessibility API, and posts a CGEvent.mouseMoved so macOS Zoom
|
|
||||||
"Follow mouse cursor" tracks the text cursor.
|
|
||||||
|
|
||||||
Usage: python3 macos-zoom-daemon.py [--debug]
|
|
||||||
"""
|
|
||||||
|
|
||||||
import ctypes
|
|
||||||
import ctypes.util
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# --- Constants ---
|
|
||||||
TITLE_BAR_H = int(os.environ.get("EMACS_ZOOM_TITLE_BAR_H", "22"))
|
|
||||||
AX_CACHE_INTERVAL = 30 # refresh AX window frame every N events
|
|
||||||
DEBUG = "--debug" in sys.argv
|
|
||||||
|
|
||||||
# --- ctypes structures ---
|
|
||||||
class CGPoint(ctypes.Structure):
|
|
||||||
_fields_ = [("x", ctypes.c_double), ("y", ctypes.c_double)]
|
|
||||||
|
|
||||||
class CGSize(ctypes.Structure):
|
|
||||||
_fields_ = [("width", ctypes.c_double), ("height", ctypes.c_double)]
|
|
||||||
|
|
||||||
class CGRect(ctypes.Structure):
|
|
||||||
_fields_ = [("origin", CGPoint), ("size", CGSize)]
|
|
||||||
|
|
||||||
# --- Load frameworks ---
|
|
||||||
CG = ctypes.CDLL("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")
|
|
||||||
CF = ctypes.CDLL("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")
|
|
||||||
AS = ctypes.CDLL("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices")
|
|
||||||
|
|
||||||
# CGEvent functions
|
|
||||||
CG.CGEventCreateMouseEvent.restype = ctypes.c_void_p
|
|
||||||
CG.CGEventCreateMouseEvent.argtypes = [
|
|
||||||
ctypes.c_void_p, ctypes.c_uint32, CGPoint, ctypes.c_uint32
|
|
||||||
]
|
|
||||||
CG.CGEventPost.restype = None
|
|
||||||
CG.CGEventPost.argtypes = [ctypes.c_uint32, ctypes.c_void_p]
|
|
||||||
|
|
||||||
CF.CFRelease.restype = None
|
|
||||||
CF.CFRelease.argtypes = [ctypes.c_void_p]
|
|
||||||
|
|
||||||
# AX functions
|
|
||||||
AS.AXUIElementCreateApplication.restype = ctypes.c_void_p
|
|
||||||
AS.AXUIElementCreateApplication.argtypes = [ctypes.c_int]
|
|
||||||
|
|
||||||
AS.AXUIElementCopyAttributeValue.restype = ctypes.c_int32
|
|
||||||
AS.AXUIElementCopyAttributeValue.argtypes = [
|
|
||||||
ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p)
|
|
||||||
]
|
|
||||||
|
|
||||||
AS.AXValueGetValue.restype = ctypes.c_bool
|
|
||||||
AS.AXValueGetValue.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p]
|
|
||||||
|
|
||||||
# CFString helper
|
|
||||||
CF.CFStringCreateWithCString.restype = ctypes.c_void_p
|
|
||||||
CF.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32]
|
|
||||||
kCFStringEncodingUTF8 = 0x08000100
|
|
||||||
|
|
||||||
def cfstr(s):
|
|
||||||
return CF.CFStringCreateWithCString(None, s.encode("utf-8"), kCFStringEncodingUTF8)
|
|
||||||
|
|
||||||
# Pre-create CFStrings
|
|
||||||
kAXFocusedWindow = cfstr("AXFocusedWindow")
|
|
||||||
kAXPosition = cfstr("AXPosition")
|
|
||||||
kAXSize = cfstr("AXSize")
|
|
||||||
|
|
||||||
# AXValue types
|
|
||||||
kAXValueCGPointType = 1
|
|
||||||
kAXValueCGSizeType = 2
|
|
||||||
|
|
||||||
# --- Emacs PID ---
|
|
||||||
_emacs_pid = None
|
|
||||||
|
|
||||||
def get_emacs_pid():
|
|
||||||
global _emacs_pid
|
|
||||||
if _emacs_pid is not None:
|
|
||||||
# Check if still alive
|
|
||||||
try:
|
|
||||||
os.kill(_emacs_pid, 0)
|
|
||||||
return _emacs_pid
|
|
||||||
except OSError:
|
|
||||||
_emacs_pid = None
|
|
||||||
try:
|
|
||||||
# Match any Emacs variant: Emacs, Emacs-arm64-11, Emacs-x86_64-10_14, etc.
|
|
||||||
out = subprocess.check_output(
|
|
||||||
["pgrep", "-f", "Emacs.app/Contents/MacOS/Emacs"], text=True
|
|
||||||
).strip()
|
|
||||||
pids = out.split("\n")
|
|
||||||
_emacs_pid = int(pids[0])
|
|
||||||
return _emacs_pid
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# --- AX window frame ---
|
|
||||||
_cached_origin = None # (x, y)
|
|
||||||
_cache_counter = 0
|
|
||||||
|
|
||||||
def get_window_origin(force=False):
|
|
||||||
global _cached_origin, _cache_counter
|
|
||||||
_cache_counter += 1
|
|
||||||
if not force and _cached_origin is not None and _cache_counter % AX_CACHE_INTERVAL != 0:
|
|
||||||
return _cached_origin
|
|
||||||
|
|
||||||
pid = get_emacs_pid()
|
|
||||||
if pid is None:
|
|
||||||
return _cached_origin
|
|
||||||
|
|
||||||
try:
|
|
||||||
app = AS.AXUIElementCreateApplication(pid)
|
|
||||||
if not app:
|
|
||||||
return _cached_origin
|
|
||||||
|
|
||||||
win = ctypes.c_void_p()
|
|
||||||
err = AS.AXUIElementCopyAttributeValue(app, kAXFocusedWindow, ctypes.byref(win))
|
|
||||||
CF.CFRelease(app)
|
|
||||||
if err != 0 or not win.value:
|
|
||||||
return _cached_origin
|
|
||||||
|
|
||||||
# Get position
|
|
||||||
pos_val = ctypes.c_void_p()
|
|
||||||
err = AS.AXUIElementCopyAttributeValue(win.value, kAXPosition, ctypes.byref(pos_val))
|
|
||||||
if err != 0 or not pos_val.value:
|
|
||||||
CF.CFRelease(win)
|
|
||||||
return _cached_origin
|
|
||||||
|
|
||||||
point = CGPoint()
|
|
||||||
ok = AS.AXValueGetValue(pos_val.value, kAXValueCGPointType, ctypes.byref(point))
|
|
||||||
CF.CFRelease(pos_val)
|
|
||||||
CF.CFRelease(win)
|
|
||||||
|
|
||||||
if ok:
|
|
||||||
_cached_origin = (point.x, point.y)
|
|
||||||
if DEBUG:
|
|
||||||
print(f"[AX] window origin: {_cached_origin}", file=sys.stderr)
|
|
||||||
return _cached_origin
|
|
||||||
except Exception as e:
|
|
||||||
if DEBUG:
|
|
||||||
print(f"[AX] error: {e}", file=sys.stderr)
|
|
||||||
return _cached_origin
|
|
||||||
|
|
||||||
# --- Post mouse event ---
|
|
||||||
def post_mouse_moved(sx, sy):
|
|
||||||
pt = CGPoint(sx, sy)
|
|
||||||
evt = CG.CGEventCreateMouseEvent(None, 5, pt, 0) # 5 = kCGEventMouseMoved
|
|
||||||
if evt:
|
|
||||||
CG.CGEventPost(0, evt) # 0 = kCGHIDEventTap
|
|
||||||
CF.CFRelease(evt)
|
|
||||||
|
|
||||||
# --- Main loop ---
|
|
||||||
def main():
|
|
||||||
if DEBUG:
|
|
||||||
print(f"[daemon] started, TITLE_BAR_H={TITLE_BAR_H}", file=sys.stderr)
|
|
||||||
|
|
||||||
# Force initial AX query
|
|
||||||
get_window_origin(force=True)
|
|
||||||
|
|
||||||
for line in sys.stdin:
|
|
||||||
parts = line.strip().split()
|
|
||||||
if len(parts) < 2:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
ex, ey = float(parts[0]), float(parts[1])
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
origin = get_window_origin()
|
|
||||||
if origin is None:
|
|
||||||
if DEBUG:
|
|
||||||
print("[daemon] no window origin, skipping", file=sys.stderr)
|
|
||||||
continue
|
|
||||||
|
|
||||||
sx = origin[0] + ex
|
|
||||||
sy = origin[1] + TITLE_BAR_H + ey
|
|
||||||
|
|
||||||
if DEBUG:
|
|
||||||
print(f"[daemon] frame({ex:.0f},{ey:.0f}) → screen({sx:.0f},{sy:.0f})", file=sys.stderr)
|
|
||||||
|
|
||||||
post_mouse_moved(sx, sy)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
Reference in New Issue
Block a user