feat(hammerspoon): show app name on focus and command key
Extract window highlighting into a dedicated module and centralize shared configuration. Add a configurable app name toast triggered by focus changes and Command key presses.
This commit is contained in:
1 parent
592e4a838c
commit
6d5b84db51
4 files changed
+425
-301
No files matched your search
@@ -0,0 +1,41 @@
|
||||
local accentColor = {
|
||||
red = 0.25,
|
||||
green = 0.65,
|
||||
blue = 1.0,
|
||||
alpha = 0.95,
|
||||
}
|
||||
|
||||
return {
|
||||
windowHighlight = {
|
||||
borderWidth = 5,
|
||||
padding = 4,
|
||||
radius = 10,
|
||||
color = accentColor,
|
||||
|
||||
animation = true,
|
||||
animationDuration = 0.10,
|
||||
animationSteps = 8,
|
||||
|
||||
hideOnFullscreen = false,
|
||||
framePollingFps = 30,
|
||||
},
|
||||
|
||||
appNameToast = {
|
||||
enabled = true,
|
||||
duration = 0.7,
|
||||
showOnFocus = true,
|
||||
showOnCommand = true,
|
||||
|
||||
style = {
|
||||
fillColor = { white = 0.08, alpha = 0.88 },
|
||||
strokeColor = accentColor,
|
||||
strokeWidth = 2,
|
||||
textColor = { white = 1, alpha = 1 },
|
||||
textSize = 20,
|
||||
padding = 10,
|
||||
radius = 10,
|
||||
fadeInDuration = 0.08,
|
||||
fadeOutDuration = 0.12,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,311 +1,29 @@
|
||||
-- Active Window Highlight for Hammerspoon
|
||||
-- Reload: Hammerspoon menu -> Reload Config
|
||||
|
||||
local config = {
|
||||
borderWidth = 5,
|
||||
padding = 4,
|
||||
radius = 10,
|
||||
local config = require("config")
|
||||
local AppNameToast = require("modules.app_name_toast")
|
||||
local WindowHighlight = require("modules.window_highlight")
|
||||
|
||||
-- blue border
|
||||
color = { red = 0.25, green = 0.65, blue = 1.0, alpha = 0.95 },
|
||||
|
||||
-- animation
|
||||
animation = true,
|
||||
animationDuration = 0.10,
|
||||
animationSteps = 8,
|
||||
|
||||
hideOnFullscreen = false,
|
||||
}
|
||||
|
||||
local border = hs.drawing.rectangle(hs.geometry.rect(0, 0, 1, 1))
|
||||
border:setFill(false)
|
||||
border:setStroke(true)
|
||||
border:setStrokeWidth(config.borderWidth)
|
||||
border:setStrokeColor(config.color)
|
||||
border:setRoundedRectRadii(config.radius, config.radius)
|
||||
border:setLevel(hs.drawing.windowLevels.screenSaver)
|
||||
border:setBehaviorByLabels({
|
||||
"canJoinAllSpaces",
|
||||
"stationary",
|
||||
"ignoresCycle",
|
||||
"fullScreenAuxiliary",
|
||||
local appNameToast = AppNameToast.new(config.appNameToast)
|
||||
local windowHighlight = WindowHighlight.new(config.windowHighlight, {
|
||||
onWindowFocused = function()
|
||||
if config.appNameToast.showOnFocus then
|
||||
appNameToast:show()
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
local currentFrame = nil
|
||||
local animationTimer = nil
|
||||
local missionControlActive = false
|
||||
local dockElement = nil
|
||||
local liveWindowWatcher = nil
|
||||
local watchedWindowId = nil
|
||||
local lastPolledWindowId = nil
|
||||
local lastPolledFrame = nil
|
||||
|
||||
local function rectForWindow(win)
|
||||
local f = win:frame()
|
||||
local screen = win:screen()
|
||||
local screenFrame = screen:fullFrame()
|
||||
local visibleFrame = screen:frame()
|
||||
local pad = config.padding
|
||||
|
||||
-- Padding normally expands the highlight outside the window. When a window
|
||||
-- touches a screen edge, align left/right/bottom with the physical display.
|
||||
-- Align the center of the top stroke with the bottom edge of the menu bar so
|
||||
-- there is no visible gap between them.
|
||||
local left = math.max(f.x - pad, screenFrame.x)
|
||||
local top = math.max(f.y - pad, visibleFrame.y)
|
||||
local right = math.min(
|
||||
f.x + f.w + pad,
|
||||
screenFrame.x + screenFrame.w
|
||||
)
|
||||
local bottom = math.min(
|
||||
f.y + f.h + pad,
|
||||
screenFrame.y + screenFrame.h
|
||||
)
|
||||
|
||||
return {
|
||||
x = left,
|
||||
y = top,
|
||||
w = right - left,
|
||||
h = bottom - top,
|
||||
}
|
||||
end
|
||||
|
||||
local function isValidWindow(win)
|
||||
if not win then return false end
|
||||
if not win:isStandard() then return false end
|
||||
if config.hideOnFullscreen and win:isFullScreen() then return false end
|
||||
|
||||
local app = win:application()
|
||||
if not app then return false end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function hideHighlight()
|
||||
if animationTimer then
|
||||
animationTimer:stop()
|
||||
animationTimer = nil
|
||||
end
|
||||
|
||||
border:hide()
|
||||
end
|
||||
|
||||
local function isMissionControlOpen()
|
||||
if not (dockElement and dockElement:isValid()) then
|
||||
local dockApps = hs.application.applicationsForBundleID("com.apple.dock")
|
||||
local dockApp = dockApps and dockApps[1]
|
||||
|
||||
if not dockApp then return false end
|
||||
dockElement = hs.axuielement.applicationElement(dockApp)
|
||||
end
|
||||
|
||||
for _, element in ipairs(dockElement) do
|
||||
if element.AXIdentifier == "mc" then return true end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function setFrame(frame)
|
||||
border:setFrame(frame)
|
||||
border:show()
|
||||
end
|
||||
|
||||
local function lerp(a, b, t)
|
||||
return a + (b - a) * t
|
||||
end
|
||||
|
||||
local function easeOutCubic(t)
|
||||
return 1 - math.pow(1 - t, 3)
|
||||
end
|
||||
|
||||
local function animateTo(targetFrame)
|
||||
if animationTimer then
|
||||
animationTimer:stop()
|
||||
animationTimer = nil
|
||||
end
|
||||
|
||||
if not config.animation or not currentFrame then
|
||||
currentFrame = targetFrame
|
||||
setFrame(targetFrame)
|
||||
return
|
||||
end
|
||||
|
||||
local startFrame = currentFrame
|
||||
local step = 0
|
||||
local steps = config.animationSteps
|
||||
local interval = config.animationDuration / steps
|
||||
|
||||
animationTimer = hs.timer.doEvery(interval, function()
|
||||
step = step + 1
|
||||
local t = easeOutCubic(step / steps)
|
||||
|
||||
local frame = {
|
||||
x = lerp(startFrame.x, targetFrame.x, t),
|
||||
y = lerp(startFrame.y, targetFrame.y, t),
|
||||
w = lerp(startFrame.w, targetFrame.w, t),
|
||||
h = lerp(startFrame.h, targetFrame.h, t),
|
||||
}
|
||||
|
||||
currentFrame = frame
|
||||
setFrame(frame)
|
||||
|
||||
if step >= steps then
|
||||
animationTimer:stop()
|
||||
animationTimer = nil
|
||||
currentFrame = targetFrame
|
||||
setFrame(targetFrame)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function updateHighlight(immediate)
|
||||
if missionControlActive then
|
||||
hideHighlight()
|
||||
currentFrame = nil
|
||||
return
|
||||
end
|
||||
|
||||
local win = hs.window.focusedWindow()
|
||||
|
||||
if not isValidWindow(win) then
|
||||
hideHighlight()
|
||||
currentFrame = nil
|
||||
return
|
||||
end
|
||||
|
||||
local targetFrame = rectForWindow(win)
|
||||
|
||||
if immediate then
|
||||
if animationTimer then
|
||||
animationTimer:stop()
|
||||
animationTimer = nil
|
||||
end
|
||||
|
||||
currentFrame = targetFrame
|
||||
setFrame(targetFrame)
|
||||
else
|
||||
animateTo(targetFrame)
|
||||
end
|
||||
end
|
||||
|
||||
local function watchFocusedWindowLive()
|
||||
local win = hs.window.focusedWindow()
|
||||
local windowId = win and win:id() or nil
|
||||
|
||||
if liveWindowWatcher and watchedWindowId == windowId then return end
|
||||
|
||||
if liveWindowWatcher then
|
||||
liveWindowWatcher:stop()
|
||||
liveWindowWatcher = nil
|
||||
end
|
||||
|
||||
watchedWindowId = windowId
|
||||
if not win then return end
|
||||
|
||||
liveWindowWatcher = win:newWatcher(function(_, event)
|
||||
if event == hs.uielement.watcher.windowMoved
|
||||
or event == hs.uielement.watcher.windowResized then
|
||||
updateHighlight(true)
|
||||
end
|
||||
end)
|
||||
|
||||
liveWindowWatcher:start({
|
||||
hs.uielement.watcher.windowMoved,
|
||||
hs.uielement.watcher.windowResized,
|
||||
hs.uielement.watcher.elementDestroyed,
|
||||
})
|
||||
end
|
||||
|
||||
local wf = hs.window.filter.new(nil)
|
||||
|
||||
-- window.filter reports both moves and resizes as windowMoved. The separate
|
||||
-- windowResized event only exists on hs.uielement.watcher, so subscribing to
|
||||
-- hs.window.filter.windowResized would pass nil and make subscribe() fail.
|
||||
wf:subscribe({
|
||||
hs.window.filter.windowFocused,
|
||||
hs.window.filter.windowMoved,
|
||||
hs.window.filter.windowTitleChanged,
|
||||
hs.window.filter.windowUnfocused,
|
||||
}, updateHighlight)
|
||||
|
||||
wf:subscribe(hs.window.filter.windowFocused, function()
|
||||
watchFocusedWindowLive()
|
||||
appNameToast:setShouldShow(function()
|
||||
return not windowHighlight:isMissionControlActive()
|
||||
end)
|
||||
|
||||
-- Keep watcher references alive for as long as the config is loaded.
|
||||
local applicationWatcher = hs.application.watcher.new(function()
|
||||
hs.timer.doAfter(0.05, function()
|
||||
watchFocusedWindowLive()
|
||||
updateHighlight()
|
||||
end)
|
||||
end)
|
||||
applicationWatcher:start()
|
||||
appNameToast:start()
|
||||
windowHighlight:start()
|
||||
|
||||
local spacesWatcher = hs.spaces.watcher.new(function()
|
||||
hs.timer.doAfter(0.15, updateHighlight)
|
||||
end)
|
||||
spacesWatcher:start()
|
||||
|
||||
-- macOS exposes Mission Control as the "mc" element in Dock's accessibility
|
||||
-- tree. Polling this state also catches trackpad gestures and hot corners.
|
||||
local missionControlWatcher = hs.timer.doEvery(0.1, function()
|
||||
local ok, isOpen = pcall(isMissionControlOpen)
|
||||
if not ok or isOpen == missionControlActive then return end
|
||||
|
||||
missionControlActive = isOpen
|
||||
currentFrame = nil
|
||||
|
||||
if missionControlActive then
|
||||
hideHighlight()
|
||||
else
|
||||
hs.timer.doAfter(0.15, updateHighlight)
|
||||
end
|
||||
end)
|
||||
|
||||
local function sameFrame(a, b)
|
||||
if not a or not b then return false end
|
||||
|
||||
return math.abs(a.x - b.x) < 0.1
|
||||
and math.abs(a.y - b.y) < 0.1
|
||||
and math.abs(a.w - b.w) < 0.1
|
||||
and math.abs(a.h - b.h) < 0.1
|
||||
end
|
||||
|
||||
-- Some apps and window managers (including Rectangle on some macOS versions)
|
||||
-- coalesce accessibility resize events. Polling the focused frame provides a
|
||||
-- reliable realtime fallback with at most ~33 ms latency.
|
||||
local framePollingWatcher = hs.timer.doEvery(1 / 30, function()
|
||||
if missionControlActive then return end
|
||||
|
||||
local win = hs.window.focusedWindow()
|
||||
local windowId = win and win:id() or nil
|
||||
|
||||
if windowId ~= lastPolledWindowId then
|
||||
lastPolledWindowId = windowId
|
||||
lastPolledFrame = win and win:frame() or nil
|
||||
return
|
||||
end
|
||||
|
||||
if not win then return end
|
||||
|
||||
local frame = win:frame()
|
||||
if sameFrame(frame, lastPolledFrame) then return end
|
||||
|
||||
lastPolledFrame = frame
|
||||
updateHighlight(true)
|
||||
end)
|
||||
|
||||
-- Running Hammerspoon objects must remain reachable after this file finishes.
|
||||
_G.activeWindowHighlightWatchers = {
|
||||
windowFilter = wf,
|
||||
application = applicationWatcher,
|
||||
spaces = spacesWatcher,
|
||||
missionControl = missionControlWatcher,
|
||||
framePolling = framePollingWatcher,
|
||||
-- Keep module state and watchers alive until the next reload.
|
||||
_G.hammerspoonModules = {
|
||||
appNameToast = appNameToast,
|
||||
windowHighlight = windowHighlight,
|
||||
}
|
||||
|
||||
hs.timer.doAfter(0.5, updateHighlight)
|
||||
hs.timer.doAfter(0.5, watchFocusedWindowLive)
|
||||
|
||||
hs.alert.show("Active window highlight loaded")
|
||||
hs.alert.show("Hammerspoon config loaded")
|
||||
@@ -0,0 +1,58 @@
|
||||
local AppNameToast = {}
|
||||
AppNameToast.__index = AppNameToast
|
||||
|
||||
function AppNameToast.new(config)
|
||||
return setmetatable({
|
||||
config = config,
|
||||
alertId = nil,
|
||||
commandKeyDown = false,
|
||||
commandKeyWatcher = nil,
|
||||
shouldShow = nil,
|
||||
}, AppNameToast)
|
||||
end
|
||||
|
||||
function AppNameToast:setShouldShow(predicate)
|
||||
self.shouldShow = predicate
|
||||
end
|
||||
|
||||
function AppNameToast:show()
|
||||
if not self.config.enabled then return end
|
||||
if self.shouldShow and not self.shouldShow() then return end
|
||||
|
||||
local win = hs.window.focusedWindow()
|
||||
local app = win and win:application() or nil
|
||||
if not app then return end
|
||||
|
||||
if self.alertId then
|
||||
hs.alert.closeSpecific(self.alertId, 0)
|
||||
end
|
||||
|
||||
self.alertId = hs.alert.show(
|
||||
app:name(),
|
||||
self.config.style,
|
||||
win:screen(),
|
||||
self.config.duration
|
||||
)
|
||||
end
|
||||
|
||||
function AppNameToast:start()
|
||||
if not self.config.showOnCommand then return self end
|
||||
|
||||
self.commandKeyWatcher = hs.eventtap.new({
|
||||
hs.eventtap.event.types.flagsChanged,
|
||||
}, function(event)
|
||||
local isCommandDown = event:getFlags().cmd == true
|
||||
|
||||
if isCommandDown and not self.commandKeyDown then
|
||||
self:show()
|
||||
end
|
||||
|
||||
self.commandKeyDown = isCommandDown
|
||||
return false
|
||||
end)
|
||||
self.commandKeyWatcher:start()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return AppNameToast
|
||||
@@ -0,0 +1,307 @@
|
||||
local WindowHighlight = {}
|
||||
WindowHighlight.__index = WindowHighlight
|
||||
|
||||
local function sameFrame(a, b)
|
||||
if not a or not b then return false end
|
||||
|
||||
return math.abs(a.x - b.x) < 0.1
|
||||
and math.abs(a.y - b.y) < 0.1
|
||||
and math.abs(a.w - b.w) < 0.1
|
||||
and math.abs(a.h - b.h) < 0.1
|
||||
end
|
||||
|
||||
local function lerp(a, b, t)
|
||||
return a + (b - a) * t
|
||||
end
|
||||
|
||||
local function easeOutCubic(t)
|
||||
return 1 - math.pow(1 - t, 3)
|
||||
end
|
||||
|
||||
function WindowHighlight.new(config, callbacks)
|
||||
local border = hs.drawing.rectangle(hs.geometry.rect(0, 0, 1, 1))
|
||||
border:setFill(false)
|
||||
border:setStroke(true)
|
||||
border:setStrokeWidth(config.borderWidth)
|
||||
border:setStrokeColor(config.color)
|
||||
border:setRoundedRectRadii(config.radius, config.radius)
|
||||
border:setLevel(hs.drawing.windowLevels.screenSaver)
|
||||
border:setBehaviorByLabels({
|
||||
"canJoinAllSpaces",
|
||||
"stationary",
|
||||
"ignoresCycle",
|
||||
"fullScreenAuxiliary",
|
||||
})
|
||||
|
||||
return setmetatable({
|
||||
config = config,
|
||||
callbacks = callbacks or {},
|
||||
border = border,
|
||||
currentFrame = nil,
|
||||
animationTimer = nil,
|
||||
missionControlActive = false,
|
||||
dockElement = nil,
|
||||
liveWindowWatcher = nil,
|
||||
watchedWindowId = nil,
|
||||
lastPolledWindowId = nil,
|
||||
lastPolledFrame = nil,
|
||||
watchers = {},
|
||||
}, WindowHighlight)
|
||||
end
|
||||
|
||||
function WindowHighlight:isMissionControlActive()
|
||||
return self.missionControlActive
|
||||
end
|
||||
|
||||
function WindowHighlight:rectForWindow(win)
|
||||
local frame = win:frame()
|
||||
local screen = win:screen()
|
||||
local screenFrame = screen:fullFrame()
|
||||
local visibleFrame = screen:frame()
|
||||
local pad = self.config.padding
|
||||
|
||||
local left = math.max(frame.x - pad, screenFrame.x)
|
||||
local top = math.max(frame.y - pad, visibleFrame.y)
|
||||
local right = math.min(
|
||||
frame.x + frame.w + pad,
|
||||
screenFrame.x + screenFrame.w
|
||||
)
|
||||
local bottom = math.min(
|
||||
frame.y + frame.h + pad,
|
||||
screenFrame.y + screenFrame.h
|
||||
)
|
||||
|
||||
return {
|
||||
x = left,
|
||||
y = top,
|
||||
w = right - left,
|
||||
h = bottom - top,
|
||||
}
|
||||
end
|
||||
|
||||
function WindowHighlight:isValidWindow(win)
|
||||
if not win then return false end
|
||||
if not win:isStandard() then return false end
|
||||
if self.config.hideOnFullscreen and win:isFullScreen() then return false end
|
||||
|
||||
return win:application() ~= nil
|
||||
end
|
||||
|
||||
function WindowHighlight:hide()
|
||||
if self.animationTimer then
|
||||
self.animationTimer:stop()
|
||||
self.animationTimer = nil
|
||||
end
|
||||
|
||||
self.border:hide()
|
||||
end
|
||||
|
||||
function WindowHighlight:isMissionControlOpen()
|
||||
if not (self.dockElement and self.dockElement:isValid()) then
|
||||
local dockApps = hs.application.applicationsForBundleID("com.apple.dock")
|
||||
local dockApp = dockApps and dockApps[1]
|
||||
|
||||
if not dockApp then return false end
|
||||
self.dockElement = hs.axuielement.applicationElement(dockApp)
|
||||
end
|
||||
|
||||
for _, element in ipairs(self.dockElement) do
|
||||
if element.AXIdentifier == "mc" then return true end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function WindowHighlight:setFrame(frame)
|
||||
self.border:setFrame(frame)
|
||||
self.border:show()
|
||||
end
|
||||
|
||||
function WindowHighlight:animateTo(targetFrame)
|
||||
if self.animationTimer then
|
||||
self.animationTimer:stop()
|
||||
self.animationTimer = nil
|
||||
end
|
||||
|
||||
if not self.config.animation or not self.currentFrame then
|
||||
self.currentFrame = targetFrame
|
||||
self:setFrame(targetFrame)
|
||||
return
|
||||
end
|
||||
|
||||
local startFrame = self.currentFrame
|
||||
local step = 0
|
||||
local steps = self.config.animationSteps
|
||||
local interval = self.config.animationDuration / steps
|
||||
|
||||
self.animationTimer = hs.timer.doEvery(interval, function()
|
||||
step = step + 1
|
||||
local t = easeOutCubic(step / steps)
|
||||
local frame = {
|
||||
x = lerp(startFrame.x, targetFrame.x, t),
|
||||
y = lerp(startFrame.y, targetFrame.y, t),
|
||||
w = lerp(startFrame.w, targetFrame.w, t),
|
||||
h = lerp(startFrame.h, targetFrame.h, t),
|
||||
}
|
||||
|
||||
self.currentFrame = frame
|
||||
self:setFrame(frame)
|
||||
|
||||
if step >= steps then
|
||||
self.animationTimer:stop()
|
||||
self.animationTimer = nil
|
||||
self.currentFrame = targetFrame
|
||||
self:setFrame(targetFrame)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function WindowHighlight:update(immediate)
|
||||
if self.missionControlActive then
|
||||
self:hide()
|
||||
self.currentFrame = nil
|
||||
return
|
||||
end
|
||||
|
||||
local win = hs.window.focusedWindow()
|
||||
if not self:isValidWindow(win) then
|
||||
self:hide()
|
||||
self.currentFrame = nil
|
||||
return
|
||||
end
|
||||
|
||||
local targetFrame = self:rectForWindow(win)
|
||||
if not immediate then
|
||||
self:animateTo(targetFrame)
|
||||
return
|
||||
end
|
||||
|
||||
if self.animationTimer then
|
||||
self.animationTimer:stop()
|
||||
self.animationTimer = nil
|
||||
end
|
||||
|
||||
self.currentFrame = targetFrame
|
||||
self:setFrame(targetFrame)
|
||||
end
|
||||
|
||||
function WindowHighlight:watchFocusedWindowLive()
|
||||
local win = hs.window.focusedWindow()
|
||||
local windowId = win and win:id() or nil
|
||||
|
||||
if self.liveWindowWatcher and self.watchedWindowId == windowId then return end
|
||||
|
||||
if self.liveWindowWatcher then
|
||||
self.liveWindowWatcher:stop()
|
||||
self.liveWindowWatcher = nil
|
||||
end
|
||||
|
||||
self.watchedWindowId = windowId
|
||||
if not win then return end
|
||||
|
||||
self.liveWindowWatcher = win:newWatcher(function(_, event)
|
||||
if event == hs.uielement.watcher.windowMoved
|
||||
or event == hs.uielement.watcher.windowResized then
|
||||
self:update(true)
|
||||
end
|
||||
end)
|
||||
|
||||
self.liveWindowWatcher:start({
|
||||
hs.uielement.watcher.windowMoved,
|
||||
hs.uielement.watcher.windowResized,
|
||||
hs.uielement.watcher.elementDestroyed,
|
||||
})
|
||||
end
|
||||
|
||||
function WindowHighlight:start()
|
||||
local windowFilter = hs.window.filter.new(nil)
|
||||
windowFilter:subscribe({
|
||||
hs.window.filter.windowFocused,
|
||||
hs.window.filter.windowMoved,
|
||||
hs.window.filter.windowTitleChanged,
|
||||
hs.window.filter.windowUnfocused,
|
||||
}, function()
|
||||
self:update(false)
|
||||
end)
|
||||
|
||||
windowFilter:subscribe(hs.window.filter.windowFocused, function()
|
||||
self:watchFocusedWindowLive()
|
||||
if self.callbacks.onWindowFocused then
|
||||
self.callbacks.onWindowFocused()
|
||||
end
|
||||
end)
|
||||
|
||||
local applicationWatcher = hs.application.watcher.new(function()
|
||||
hs.timer.doAfter(0.05, function()
|
||||
self:watchFocusedWindowLive()
|
||||
self:update(false)
|
||||
end)
|
||||
end)
|
||||
applicationWatcher:start()
|
||||
|
||||
local spacesWatcher = hs.spaces.watcher.new(function()
|
||||
hs.timer.doAfter(0.15, function()
|
||||
self:update(false)
|
||||
end)
|
||||
end)
|
||||
spacesWatcher:start()
|
||||
|
||||
local missionControlWatcher = hs.timer.doEvery(0.1, function()
|
||||
local ok, isOpen = pcall(function()
|
||||
return self:isMissionControlOpen()
|
||||
end)
|
||||
if not ok or isOpen == self.missionControlActive then return end
|
||||
|
||||
self.missionControlActive = isOpen
|
||||
self.currentFrame = nil
|
||||
|
||||
if self.missionControlActive then
|
||||
self:hide()
|
||||
else
|
||||
hs.timer.doAfter(0.15, function()
|
||||
self:update(false)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
local framePollingWatcher = hs.timer.doEvery(
|
||||
1 / self.config.framePollingFps,
|
||||
function()
|
||||
if self.missionControlActive then return end
|
||||
|
||||
local win = hs.window.focusedWindow()
|
||||
local windowId = win and win:id() or nil
|
||||
|
||||
if windowId ~= self.lastPolledWindowId then
|
||||
self.lastPolledWindowId = windowId
|
||||
self.lastPolledFrame = win and win:frame() or nil
|
||||
return
|
||||
end
|
||||
|
||||
if not win then return end
|
||||
|
||||
local frame = win:frame()
|
||||
if sameFrame(frame, self.lastPolledFrame) then return end
|
||||
|
||||
self.lastPolledFrame = frame
|
||||
self:update(true)
|
||||
end
|
||||
)
|
||||
|
||||
self.watchers = {
|
||||
windowFilter = windowFilter,
|
||||
application = applicationWatcher,
|
||||
spaces = spacesWatcher,
|
||||
missionControl = missionControlWatcher,
|
||||
framePolling = framePollingWatcher,
|
||||
}
|
||||
|
||||
hs.timer.doAfter(0.5, function()
|
||||
self:update(false)
|
||||
self:watchFocusedWindowLive()
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return WindowHighlight
|
||||
Reference in new issue
Block a user