-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathinit.lua
More file actions
180 lines (158 loc) · 6.95 KB
/
init.lua
File metadata and controls
180 lines (158 loc) · 6.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
-- Helper: Find window under mouse
local function windowUnderMouse()
local mousePos = hs.mouse.absolutePosition()
local function rectContains(pos, size)
if not pos or not size then return false end
local x = pos.x or pos.X or pos[1]
local y = pos.y or pos.Y or pos[2]
local w = size.w or size.width or size[1]
local h = size.h or size.height or size[2]
if not (x and y and w and h) then return false end
return mousePos.x >= x and mousePos.x <= (x + w) and mousePos.y >= y and mousePos.y <= (y + h)
end
-- 1) Check normal Hammerspoon window objects (ordered by z-order)
for _, win in ipairs(hs.window.orderedWindows()) do
local ok, vis = pcall(function() return win:isVisible() end)
if ok and vis then
local frame = win:frame()
if frame and rectContains({x = frame.x, y = frame.y}, {w = frame.w, h = frame.h}) then
return win
end
end
end
-- 2) Also check any visible windows listing (some windows may not appear in orderedWindows)
if hs.window.visibleWindows then
for _, win in ipairs(hs.window.visibleWindows()) do
local ok, vis = pcall(function() return win:isVisible() end)
if ok and vis then
local frame = win:frame()
if frame and rectContains({x = frame.x, y = frame.y}, {w = frame.w, h = frame.h}) then
return win
end
end
end
end
-- 3) Recursively scan accessibility (AX) elements across all running applications.
local function findAxElementAtPoint(elem)
if not elem then return nil end
local okRole, role = pcall(function() return elem:attributeValue("AXRole") end)
if not okRole then return nil end
local okPos, pos = pcall(function() return elem:attributeValue("AXPosition") end)
local okSize, size = pcall(function() return elem:attributeValue("AXSize") end)
local okVisible, visible = pcall(function() return elem:attributeValue("AXVisible") end)
if okPos and okSize and pos and size then
-- If AXVisible is not provided, assume visible (some apps omit it)
if (visible == nil) or (visible == true) then
if rectContains(pos, size) then
return elem
end
end
end
-- Recurse into AXChildren
local okChildren, children = pcall(function() return elem:attributeValue("AXChildren") end)
if okChildren and children and type(children) == "table" then
for _, child in ipairs(children) do
local found = findAxElementAtPoint(child)
if found then return found end
end
end
-- Also check AXWindows attribute if present
local okWindows, winChildren = pcall(function() return elem:attributeValue("AXWindows") end)
if okWindows and winChildren and type(winChildren) == "table" then
for _, child in ipairs(winChildren) do
local found = findAxElementAtPoint(child)
if found then return found end
end
end
return nil
end
-- Iterate all running applications to catch panels, popovers, toolbars, etc.
for _, app in ipairs(hs.application.runningApplications()) do
local axApp = nil
local okApp, appElem = pcall(function() return hs.axuielement.applicationElement(app) end)
if okApp then axApp = appElem end
if axApp then
local ok, windowsAttr = pcall(function() return axApp:attributeValue("AXWindows") end)
if ok and windowsAttr and type(windowsAttr) == "table" then
for _, e in ipairs(windowsAttr) do
local found = findAxElementAtPoint(e)
if found then return found end
end
end
local okC, children = pcall(function() return axApp:attributeValue("AXChildren") end)
if okC and children and type(children) == "table" then
for _, e in ipairs(children) do
local found = findAxElementAtPoint(e)
if found then return found end
end
end
end
end
-- 4) As a last resort, check the system-wide accessibility tree (menus, status items)
local okSys, sys = pcall(function() return hs.axuielement.systemWide() end)
if okSys and sys then
local found = findAxElementAtPoint(sys)
if found then return found end
end
return nil
end
-- Log file setup
local logfilePath = os.getenv("HOME") .. "/hammerspoon_clickthrough.log"
local logfile = assert(io.open(logfilePath, "a"))
local function log(message, level)
level = level or "INFO"
local timestamp = os.date("%Y-%m-%d %H:%M:%S")
logfile:write(string.format("%s [%s] %s\n", timestamp, level, message))
logfile:flush()
end
-- Eventtap: Focus window under mouse and log click (with menu/dialog handling)
clickLogger = hs.eventtap.new({hs.eventtap.event.types.leftMouseDown}, function(event)
local ax = require("hs.axuielement")
local mousePos = hs.mouse.absolutePosition()
local element = ax.systemElementAtPosition(mousePos)
if element then
local role = element:attributeValue("AXRole")
log("role: " .. (role or "Untitled role"))
if role == "AXMenu" or role == "AXMenuItem" then
log("Skip on role: " .. role)
return false -- allow original click
end
end
local frontmost = hs.window.frontmostWindow()
if frontmost and frontmost:subrole() == "AXDialog" then
log("Skip on subrole: " .. frontmost:subrole())
return false -- allow original click
end
local win = windowUnderMouse()
if win then
-- If it's a normal window
if win.id and win.title then
if win:id() ~= (frontmost and frontmost:id()) then
win:focus()
log("Focused window: " .. (win:title() or "Untitled"))
else
log("Clicked already-focused window: " .. (win:title() or "Untitled"))
end
else
-- It's an AX element (e.g. About menu)
local role = win:attributeValue("AXRole") or "AXUIElement"
local title = win:attributeValue("AXTitle") or role
-- Try to focus the element (not always possible)
if win:attributeValue("AXFocused") ~= true then
pcall(function() win:setAttributeValue("AXFocused", true) end)
log("Focused AX element: " .. title .. " (" .. role .. ")")
else
log("Clicked already-focused AX element: " .. title .. " (" .. role .. ")")
end
end
else
log("No window or AX element under mouse")
end
return false -- allow original click
end)
clickLogger:start()
-- Graceful shutdown
hs.shutdownCallback = function()
log("Hammerspoon shutting down", "INFO")
logfile:close()
end