Last active
November 26, 2025 10:13
-
-
Save dofy/79d6b5a49fa573d21c1e4f24d09b5bf4 to your computer and use it in GitHub Desktop.
A HammerSpoon script that make mouse follow the active window.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- Hammerspoon Configuration | |
| -- other scripts ... | |
| -- Load modules | |
| local mouseFollow = require("mouse-follow") | |
| -- Configure mouse follow | |
| mouseFollow.config.moveDelay = 0.0 | |
| mouseFollow.config.clickDetectWindow = 0.3 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- Mouse Follow Module | |
| -- 功能:当窗口获得焦点时,自动将鼠标移动到该窗口所在屏幕的中心 | |
| local M = {} | |
| -- Configuration | |
| M.config = { | |
| enabled = true, | |
| moveDelay = 0.0, -- delay before moving mouse (seconds) | |
| clickDetectWindow = 0.3 -- time window to detect recent clicks (seconds) | |
| } | |
| -- Private variables | |
| local wf = nil | |
| local lastClickTime = 0 | |
| local clickEventTap = nil | |
| local function centerOfScreen(scr) | |
| local f = scr and scr:frame() | |
| if not f then return nil end | |
| return {x = f.x + f.w / 2, y = f.y + f.h / 2} | |
| end | |
| local function maybeWarpMouseTo(win) | |
| if not M.config.enabled then return end | |
| if win == nil or not win:isStandard() then return end | |
| local scr = win:screen() | |
| if scr == nil then return end | |
| local targetPos = centerOfScreen(scr) | |
| if targetPos == nil then return end | |
| -- Check if activation was caused by a recent mouse click | |
| local timeSinceClick = hs.timer.secondsSinceEpoch() - lastClickTime | |
| if timeSinceClick < M.config.clickDetectWindow then return end | |
| local mouseScr = hs.mouse.getCurrentScreen() | |
| -- If mouse is already on the target screen, don't move | |
| if mouseScr and scr and mouseScr:id() == scr:id() then return end | |
| -- Mouse is on a different screen, move it to target screen center | |
| hs.mouse.absolutePosition(targetPos) | |
| end | |
| function M.start() | |
| if wf then M.stop() end | |
| -- Track mouse clicks to avoid moving when clicking activates window | |
| clickEventTap = hs.eventtap.new({hs.eventtap.event.types.leftMouseDown}, | |
| function(event) | |
| lastClickTime = hs.timer.secondsSinceEpoch() | |
| return false -- don't block the event | |
| end) | |
| clickEventTap:start() | |
| -- Create window filter | |
| wf = hs.window.filter.new(true) | |
| -- Handle window focus events | |
| wf:subscribe(hs.window.filter.windowFocused, function(win) | |
| if M.config.moveDelay > 0 then | |
| hs.timer.doAfter(M.config.moveDelay, | |
| function() maybeWarpMouseTo(win) end) | |
| else | |
| maybeWarpMouseTo(win) | |
| end | |
| end) | |
| -- Handle window creation (some apps create before focusing) | |
| wf:subscribe(hs.window.filter.windowCreated, function(win) | |
| hs.timer.doAfter(0.05, function() maybeWarpMouseTo(win) end) | |
| end) | |
| return M | |
| end | |
| function M.stop() | |
| if wf then | |
| wf:unsubscribeAll() | |
| wf = nil | |
| end | |
| if clickEventTap then | |
| clickEventTap:stop() | |
| clickEventTap = nil | |
| end | |
| return M | |
| end | |
| function M.toggle() | |
| M.config.enabled = not M.config.enabled | |
| local status = M.config.enabled and "ON" or "OFF" | |
| hs.alert.show("Mouse Follow: " .. status) | |
| return M | |
| end | |
| return M | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment