Skip to content

Instantly share code, notes, and snippets.

@temocj
Last active September 10, 2024 13:45
Show Gist options
  • Save temocj/b0af557fce7449a21b1c217b2d074711 to your computer and use it in GitHub Desktop.
Save temocj/b0af557fce7449a21b1c217b2d074711 to your computer and use it in GitHub Desktop.
Hammerspoon script to mimic MS Windows behavior when clicking on un-focused windows.
-- Description:
-- Mimics Microsoft Windows click-through
-- behavior when changing focused window.
-- Installation:
-- Add anywhere to your Hammerspoon config.
-- Hammerspoon modules used:
-- hs.eventtap, hs.eventtap.event, hs.mouse, hs.timer, hs.window.filter
-- Compatibility:
-- Tested with Hammerspoon 0.9.90 and macOS 10.15.4 (Catalina)
-- Todo:
-- 1. Right- and middle-click? 2. Application opt-out filter.
-- License:
-- Public Domain
local evt <const> = hs.eventtap.event
-- Sequence overview:
-- 1a. MOUSE BUTTON PRESSED DOWN (or trackpad drag started)
-- 2. mouse down event
-- 3. window focus changed
-- 4. post virtual mouse down event (enables step 1b)
-- 5. script is unaffected by it, but target app receives the event
-- 6. virtual mouse up event occurs (HS bug) and we suppress it
-- 1b. MOUSE CAN BE DRAGGED AROUND to select text or whatnot
-- 1c. MOUSE BUTTON RELEASED (or trackpad drag ended)
-- 2. mouse up event
--
-- 1. TRACKPAD TAPPED (briefly)
-- 2. mouse down event
-- 3. window focus changed
-- 4a. mouse up event (while we're still responding to step 3)
-- 4b. post virtual mouse down event
-- 5. script sees it and posts a virtual mouse up event
-- 6. script is unaffected, but target app receives both events (in the correct order hopefully)
local clickthroughInProgress
local virtualEventNumber -- need to keep track of it in order to work around HS bug
local function isVirtual (event)
return virtualEventNumber == event:getProperty(evt.properties.mouseEventNumber)
end
local function mouseDown (event)
-- check if virtual down-event occured AFTER corresponding up-event (HS bug)
if isVirtual(event) and not clickthroughInProgress then
-- post another up-event to get it unstuck
local cursorPos = hs.mouse.absolutePosition()
local mouseUpEvent = evt.newMouseEvent(evt.types.leftMouseUp, cursorPos)
mouseUpEvent:setProperty(evt.properties.mouseEventNumber, 1)
mouseUpEvent:post()
return false -- race condition here? (we want the up-event to occur last), but it always works for me
end
clickthroughInProgress = true
-- dirty fix for erratic behavior when dragging items between apps
-- makes windowFocusChanged inert after 0.1 seconds
hs.timer.doAfter(0.1, function() clickthroughInProgress = false end)
return false
end
local function mouseUp (event)
clickthroughInProgress = false
return isVirtual(event) -- ignore extraneous up-event (HS bug)
end
local function windowFocusChanged ()
if clickthroughInProgress then
-- post another down-event so we can continue dragging (to select text or whatnot)
local cursorPos = hs.mouse.absolutePosition()
local mouseDownEvent = evt.newMouseEvent(evt.types.leftMouseDown, cursorPos)
virtualEventNumber = mouseDownEvent:getProperty(evt.properties.mouseEventNumber)
mouseDownEvent:post() -- causes extraneous up-event when dragging & switching window (HS bug)
end
end
listener1 = hs.eventtap.new({evt.types.leftMouseDown}, mouseDown):start()
listener2 = hs.eventtap.new({evt.types.leftMouseUp}, mouseUp):start()
-- unfortunately hs.window.filter may cause a bit of unresponsiveness during config load
hs.window.filter.default:subscribe(hs.window.filter.windowFocused, windowFocusChanged)
@temocj
Copy link
Author

temocj commented Jun 27, 2021

Known issues

  • Not working: Finder, iTerm2, Disk Utility, and probably others
  • [FIXED] click gets stuck when using trackpad (Hammerspoon bug)
  • [FIXED] stops working after a few minutes (garbage collector releases listener1 and listener2)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment