feat: Hammerspoon IPC cursor tracking + split magnifier (SPC z m)
This commit is contained in:
105
hammerspoon/cursor-warp.lua
Normal file
105
hammerspoon/cursor-warp.lua
Normal file
@@ -0,0 +1,105 @@
|
||||
-- cursor-warp.lua — Emacs cursor tracking for macOS Zoom
|
||||
--
|
||||
-- WHAT: Reads cursor coordinates written by Emacs to ~/.emacs-cursor-pos
|
||||
-- and posts a real mouseMoved HID event so macOS Zoom follows the
|
||||
-- text cursor. CGWarpMouseCursorPosition (used by Emacs internally)
|
||||
-- is silently ignored by macOS Zoom — this workaround fixes that.
|
||||
--
|
||||
-- SETUP:
|
||||
-- 1. Install Hammerspoon: https://www.hammerspoon.org/
|
||||
-- 2. Grant Accessibility permission: System Settings → Privacy & Security
|
||||
-- → Accessibility → enable Hammerspoon
|
||||
-- 3. Copy this file to ~/.hammerspoon/cursor-warp.lua
|
||||
-- 4. In ~/.hammerspoon/init.lua add: require("cursor-warp")
|
||||
-- 5. In Emacs: SPC z t enables cursor tracking (writes ~/.emacs-cursor-pos)
|
||||
-- 6. Toggle in Hammerspoon: call emacsZoomToggle() or bind a hotkey
|
||||
--
|
||||
-- FILE FORMAT (~/.emacs-cursor-pos):
|
||||
-- Line 1: X Y (screen-absolute cursor position)
|
||||
-- Line 2: FX FY (frame-position, for debug cross-check)
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Global toggle
|
||||
emacsZoomEnabled = true
|
||||
emacsZoomDebug = false
|
||||
|
||||
local cursorPosFile = os.getenv("HOME") .. "/.emacs-cursor-pos"
|
||||
local lastMtime = 0
|
||||
local pollTimer = nil
|
||||
|
||||
local function readCursorPos()
|
||||
local f = io.open(cursorPosFile, "r")
|
||||
if not f then return nil end
|
||||
local line1 = f:read("*l")
|
||||
local line2 = f:read("*l")
|
||||
f:close()
|
||||
if not line1 then return nil end
|
||||
local x, y = line1:match("^(%d+) (%d+)$")
|
||||
if not x then return nil end
|
||||
local fx, fy = nil, nil
|
||||
if line2 then
|
||||
fx, fy = line2:match("^(%-?%d+) (%-?%d+)$")
|
||||
end
|
||||
return tonumber(x), tonumber(y), tonumber(fx), tonumber(fy)
|
||||
end
|
||||
|
||||
local function pollAndWarp()
|
||||
if not emacsZoomEnabled then return end
|
||||
|
||||
-- Check file modification time to avoid unnecessary reads
|
||||
local attrs = hs.fs.attributes(cursorPosFile)
|
||||
if not attrs then return end
|
||||
local mtime = attrs.modification
|
||||
if mtime == lastMtime then return end
|
||||
lastMtime = mtime
|
||||
|
||||
local x, y, fx, fy = readCursorPos()
|
||||
if not x then return end
|
||||
|
||||
-- Post a real mouseMoved event (not CGWarp)
|
||||
hs.eventtap.event.newMouseEvent(
|
||||
hs.eventtap.event.types.mouseMoved,
|
||||
hs.geometry.point(x, y)
|
||||
):post()
|
||||
|
||||
if emacsZoomDebug then
|
||||
local msg = string.format("cursor-warp: move to (%d, %d)", x, y)
|
||||
if fx then
|
||||
-- Cross-check with Hammerspoon's view of Emacs frame
|
||||
local emacs = hs.application.get("Emacs")
|
||||
if emacs then
|
||||
local win = emacs:focusedWindow()
|
||||
if win then
|
||||
local frame = win:frame()
|
||||
msg = msg .. string.format(
|
||||
" | emacs-frame=(%d,%d) hs-frame=(%d,%d)",
|
||||
fx, fy, math.floor(frame.x), math.floor(frame.y))
|
||||
end
|
||||
end
|
||||
end
|
||||
print(msg)
|
||||
end
|
||||
end
|
||||
|
||||
function emacsZoomToggle()
|
||||
emacsZoomEnabled = not emacsZoomEnabled
|
||||
local state = emacsZoomEnabled and "ON" or "OFF"
|
||||
hs.alert.show("Emacs Zoom tracking: " .. state)
|
||||
print("cursor-warp: " .. state)
|
||||
end
|
||||
|
||||
function emacsZoomDebugToggle()
|
||||
emacsZoomDebug = not emacsZoomDebug
|
||||
hs.alert.show("Emacs Zoom debug: " .. (emacsZoomDebug and "ON" or "OFF"))
|
||||
end
|
||||
|
||||
-- Start polling
|
||||
pollTimer = hs.timer.doEvery(0.05, pollAndWarp)
|
||||
|
||||
-- Optional hotkey: Ctrl+Opt+Cmd+Z to toggle
|
||||
hs.hotkey.bind({"ctrl", "alt", "cmd"}, "Z", emacsZoomToggle)
|
||||
|
||||
print("cursor-warp: loaded (polling every 50ms, hotkey Ctrl+Opt+Cmd+Z)")
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user