129 lines
3.4 KiB
Lua
129 lines
3.4 KiB
Lua
-- ~/.hammerspoon/Monocle.lua
|
|
local Monocle = {}
|
|
|
|
-- CONFIGURATION
|
|
local blurRadius = 20
|
|
local dimAlpha = 0.3
|
|
local shakeTrigger = 250 -- Energy required (higher = harder shake)
|
|
|
|
local overlay = nil
|
|
local filter = nil
|
|
local mouseTimer = nil
|
|
local clickWatcher = nil
|
|
local energy = 0
|
|
|
|
function Monocle.show()
|
|
local screen = hs.screen.mainScreen()
|
|
local res = screen:fullFrame()
|
|
local win = hs.window.focusedWindow()
|
|
if not win then return end
|
|
|
|
if not overlay then
|
|
overlay = hs.canvas.new(res)
|
|
overlay:level(101)
|
|
overlay:behavior({
|
|
hs.canvas.windowBehaviors.canJoinAllSpaces,
|
|
hs.canvas.windowBehaviors.ignoresMouseEvents
|
|
})
|
|
|
|
-- Layer 1: Background Dimming
|
|
overlay[1] = {
|
|
type = "rectangle",
|
|
action = "fill",
|
|
fillColor = { black = 1, alpha = dimAlpha },
|
|
frame = { x = 0, y = 0, w = "100%", h = "100%" },
|
|
}
|
|
|
|
-- APPLY THE BLUR FILTER TO THE WHOLE CANVAS
|
|
overlay:setFilter({
|
|
name = "CIGaussianBlur",
|
|
inputRadius = blurRadius
|
|
})
|
|
end
|
|
|
|
overlay:frame(res)
|
|
overlay:show()
|
|
win:raise()
|
|
end
|
|
|
|
function Monocle.hide()
|
|
if overlay then overlay:hide() end
|
|
energy = 0
|
|
end
|
|
|
|
function Monocle.toggle()
|
|
if overlay and overlay:isShowing() then
|
|
Monocle.hide()
|
|
else
|
|
Monocle.show()
|
|
end
|
|
end
|
|
|
|
function Monocle.init()
|
|
-- 1. HYPERKEY BINDING
|
|
if hyper then
|
|
hs.hotkey.bind(hyper, "f", function()
|
|
Monocle.toggle()
|
|
end)
|
|
end
|
|
|
|
-- 2. Focus Follower
|
|
filter = hs.window.filter.new(nil)
|
|
filter:subscribe(hs.window.filter.windowFocused, function()
|
|
if overlay and overlay:isShowing() then
|
|
Monocle.show()
|
|
end
|
|
end)
|
|
|
|
-- 3. Click Outside to Disable
|
|
clickWatcher = hs.eventtap.new({1}, function(event)
|
|
if overlay and overlay:isShowing() then
|
|
local clickPoint = event:location()
|
|
local win = hs.window.focusedWindow()
|
|
if win then
|
|
local f = win:frame()
|
|
local isInside = (clickPoint.x >= f.x and clickPoint.x <= (f.x + f.w) and
|
|
clickPoint.y >= f.y and clickPoint.y <= (f.y + f.h))
|
|
if not isInside then
|
|
Monocle.hide()
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end):start()
|
|
|
|
-- 4. Vector-Based Shake Detection
|
|
local lastPos = hs.mouse.absolutePosition()
|
|
local lastDx, lastDy = 0, 0
|
|
|
|
mouseTimer = hs.timer.doEvery(0.02, function()
|
|
local newPos = hs.mouse.absolutePosition()
|
|
local dx = newPos.x - lastPos.x
|
|
local dy = newPos.y - lastPos.y
|
|
|
|
-- If current direction (dx) is opposite of last direction (lastDx)
|
|
-- That indicates a shake reversal.
|
|
local reversedX = (dx * lastDx < 0)
|
|
local reversedY = (dy * lastDy < 0)
|
|
|
|
if reversedX or reversedY then
|
|
local speed = math.sqrt(dx*dx + dy*dy)
|
|
if speed > 15 then
|
|
energy = energy + (speed * 2)
|
|
end
|
|
else
|
|
-- Constant decay (15 units per cycle)
|
|
energy = math.max(0, energy - 15)
|
|
end
|
|
|
|
if energy > shakeTrigger then
|
|
Monocle.toggle()
|
|
energy = 0
|
|
end
|
|
|
|
lastDx, lastDy = dx, dy
|
|
lastPos = newPos
|
|
end)
|
|
end
|
|
|
|
return Monocle |