304 lines
13 KiB
Lua
304 lines
13 KiB
Lua
--- === hs.window.highlight ===
|
|
---
|
|
--- Highlight the focused window
|
|
---
|
|
--- This module can be useful to spatially keep track of windows if you have large and/or multiple screens, and are
|
|
--- therefore likely to have several windows visible at any given time.
|
|
--- It highlights the currently focused window by covering other windows and the desktop with either a subtle
|
|
--- ("overlay" mode) or opaque ("isolate" mode) overlay; additionally it can highlight windows as they're shown
|
|
--- or hidden via a brief flash, to help determine their location intuitively (to avoid having to studiously scan
|
|
--- all your screens when, for example, you know you triggered a dialog but it didn't show up where you expected it).
|
|
---
|
|
--- By default, overlay mode is disabled - you can enable it with `hs.window.highlight.ui.overlay=true` - and so are
|
|
--- the window shown/hidden flashes - enable those with `hs.window.highlight.ui.flashDuration=0.3` (or whatever duration
|
|
--- you prefer). Isolate mode is always available and can be toggled manually via `hs.window.highlight.toggleIsolate()`
|
|
--- or automatically by passing an appropriate windowfilter (or a list of apps) to `hs.window.highlight.start()`.
|
|
|
|
local screen=require'hs.screen'
|
|
local timer=require'hs.timer'
|
|
local window=require'hs.window'
|
|
local windowfilter=require'hs.window.filter'
|
|
local redshift=require'hs.redshift'
|
|
local settings=require'hs.settings'
|
|
local log=hs.logger.new('highlight')
|
|
local drrect = hs.drawing.rectangle
|
|
local type,ipairs=type,ipairs
|
|
|
|
local highlight={setLogLevel=log.setLogLevel,getLogLevel=log.getLogLevel}
|
|
|
|
--local frames={} -- cached
|
|
local focusedWin,sf,frame,rt,rl,rb,rr,rflash
|
|
local BEHAVIOR=9
|
|
local SETTING_ISOLATE_OVERRIDE='hs.window.highlight.isolate.override'
|
|
|
|
--- hs.window.highlight.ui
|
|
--- Variable
|
|
--- Allows customization of the highlight overlays and behaviour.
|
|
---
|
|
--- The default values are shown in the right hand side of the assignements below.
|
|
---
|
|
--- To represent color values, you can use:
|
|
--- * a table {red=redN, green=greenN, blue=blueN, alpha=alphaN}
|
|
--- * a table {redN,greenN,blueN[,alphaN]} - if omitted alphaN defaults to 1.0
|
|
--- where redN, greenN etc. are the desired value for the color component between 0.0 and 1.0
|
|
---
|
|
--- Color inversion is governed by the module `hs.redshift`. See the relevant documentation for more information.
|
|
---
|
|
--- * `hs.window.highlight.ui.overlay = false` - draw overlay over the area of the screen(s) that isn't occupied by the focused window
|
|
--- * `hs.window.highlight.ui.overlayColor = {0.2,0.05,0,0.25}` - overlay color
|
|
--- * `hs.window.highlight.ui.overlayColorInverted = {0.8,0.9,1,0.3}` - overlay color when colors are inverted
|
|
--- * `hs.window.highlight.ui.isolateColor = {0,0,0,0.95}` - overlay color for isolate mode
|
|
--- * `hs.window.highlight.ui.isolateColorInverted = {1,1,1,0.95}` - overlay color for isolate mode when colors are inverted
|
|
--- * `hs.window.highlight.ui.frameWidth = 10` - draw a frame around the focused window in overlay mode; 0 to disable
|
|
--- * `hs.window.highlight.ui.frameColor = {0,0.6,1,0.5}` - frame color
|
|
--- * `hs.window.highlight.ui.frameColorInvert = {1,0.4,0,0.5}`
|
|
--- * `hs.window.highlight.ui.flashDuration = 0` - duration in seconds of a brief flash over windows as they're shown/hidden;
|
|
--- disabled if 0; if desired, 0.3 is a good value
|
|
--- * `hs.window.highlight.ui.windowShownFlashColor = {0,1,0,0.8}` - flash color when a window is shown (created or unhidden)
|
|
--- * `hs.window.highlight.ui.windowHiddenFlashColor = {1,0,0,0.8}` - flash color when a window is hidden (destroyed or hidden)
|
|
--- * `hs.window.highlight.ui.windowShownFlashColorInvert = {1,0,1,0.8}`
|
|
--- * `hs.window.highlight.ui.windowHiddenFlashColorInvert = {0,1,1,0.8}`
|
|
local uiGlobal = {
|
|
frameColor={0,0.6,1,0.5},
|
|
frameColorInvert={1,0.4,0,0.5},
|
|
frameWidth=0,
|
|
overlay=false, --disabled
|
|
overlayColor={0.2,0.05,0,0.25},
|
|
overlayColorInvert={0.8,0.9,1,0.3},
|
|
isolateColor={0,0,0,0.95},
|
|
isolateColorInvert={1,1,1,0.95},
|
|
windowShownFlashColor={0,1,0,0.8},
|
|
windowHiddenFlashColor={1,0,0,0.8},
|
|
windowShownFlashColorInvert={1,0,1,0.8},
|
|
windowHiddenFlashColorInvert={0,1,1,0.8},
|
|
flashDuration=0, -- disabled
|
|
}
|
|
local function getColor(t) if type(t)~='table' or t.red or not t[1] then return t else return {red=t[1] or 0,green=t[2] or 0,blue=t[3] or 0,alpha=t[4] or 1} end end
|
|
|
|
local function getScreens()
|
|
local screens=screen.allScreens()
|
|
if #screens==0 then log.w('Cannot get current screens') return end
|
|
sf=screens[1]:fullFrame()
|
|
for i=2,#screens do
|
|
local fr=screens[i]:frame()
|
|
if fr.x<sf.x then sf.x=fr.x end
|
|
if fr.y<sf.y then sf.y=fr.y end
|
|
if fr.x2>sf.x2 then sf.x2=fr.x2 end
|
|
if fr.y2>sf.y2 then sf.y2=fr.y2 end
|
|
end
|
|
end
|
|
|
|
local hasFrame,hasOverlay,hasFlash
|
|
|
|
local tflash=timer.delayed.new(0.3,function()rflash:hide()end)
|
|
|
|
|
|
local function hideFrame()
|
|
frame:hide() rt:hide() rl:hide() rb:hide() rr:hide()
|
|
-- for _,r in ipairs{rt,rl,rb,rr} do if r then r:hide() end end
|
|
end
|
|
|
|
local invert,isolateAuto,isolateUser,isolate
|
|
local function drawFrame() -- draw an overlay around a window
|
|
if not focusedWin then return end
|
|
local f=focusedWin:frame() -- frames[focusedWin:id()]=f
|
|
if not isolate and hasFrame then frame:setFrame(f):show() end
|
|
-- decided against leaving a passive-aggressive comment mentioning the lack of
|
|
-- hs.geometry wrapping for :setFrame(), because yay performance!
|
|
rt:setFrame{x=sf.x,y=sf.y,w=f.x+f.w-sf.x,h=f.y-sf.y}:show()
|
|
rl:setFrame{x=sf.x,y=f.y,w=f.x-sf.x,h=sf.h-f.y+sf.y}:show()
|
|
rb:setFrame{x=f.x,y=f.y+f.h,w=sf.w-f.x,h=sf.h-f.y-f.h+sf.y}:show()
|
|
rr:setFrame{x=f.x+f.w,y=sf.y,w=sf.w-f.x-f.w,h=f.y+f.h-sf.y}:show()
|
|
end
|
|
|
|
local function flash(win,shown)
|
|
local k='window'..(shown and 'Shown' or 'Hidden')..'FlashColor'..(invert and 'Invert' or '')
|
|
local f=win:frame()
|
|
rflash:setFrame(f):setFillColor(highlight.ui[k]):show()
|
|
tflash:start()
|
|
end
|
|
|
|
local wfFlash,wfOverlay,wfIsolate,modulewfIsolate
|
|
local flashsubs={
|
|
[windowfilter.windowVisible]=function(w)flash(w,true) end,
|
|
[windowfilter.windowNotVisible]=function(w)flash(w,false) end,
|
|
}
|
|
local focusedsubs={
|
|
[windowfilter.windowFocused]=function(w)focusedWin=w return drawFrame() end,
|
|
[windowfilter.windowMoved]=function(w)if w==focusedWin then return drawFrame() end end,
|
|
[windowfilter.windowUnfocused]=function()focusedWin=nil return hideFrame() end,
|
|
}
|
|
local running
|
|
|
|
local function setMode()
|
|
if not running then return end
|
|
hideFrame()
|
|
local over,overinv=highlight.ui.overlayColor,highlight.ui.overlayColorInvert
|
|
local isol,isolinv=highlight.ui.isolateColor,highlight.ui.isolateColorInvert
|
|
for _,r in ipairs{rt,rl,rb,rr} do
|
|
r:setFillColor(invert and (isolate and isolinv or overinv) or (isolate and isol or over))
|
|
end
|
|
frame:setStrokeColor(invert and highlight.ui.frameColorInvert or highlight.ui.frameColor)
|
|
if isolate or hasOverlay then
|
|
wfOverlay:subscribe(focusedsubs)
|
|
local w=window.focusedWindow()
|
|
if w then focusedsubs[windowfilter.windowFocused](w) end
|
|
else wfOverlay:unsubscribe(focusedsubs) end
|
|
if hasFlash then wfFlash:subscribe(flashsubs)
|
|
else wfFlash:unsubscribe(flashsubs) end
|
|
end
|
|
|
|
local function setInvert(v)
|
|
if invert~=v then log.f('inverted mode %s',v and 'on' or 'off') end
|
|
invert=v return setMode()
|
|
end
|
|
|
|
local function setIsolate()
|
|
local v=isolateUser
|
|
if v==nil then v=isolateAuto end
|
|
if isolate~=v then
|
|
log.f('isolate mode %s',v and 'on' or 'off')
|
|
isolate=v return setMode()
|
|
end
|
|
end
|
|
|
|
local isolatesubs={
|
|
[windowfilter.windowFocused]=function(w)focusedWin=w isolateAuto=true setIsolate() return drawFrame() end,
|
|
[windowfilter.windowMoved]=function(w)if w==focusedWin then return drawFrame() end end,
|
|
[windowfilter.windowUnfocused]=function()focusedWin=nil isolateAuto=nil setIsolate() return hideFrame() end,
|
|
}
|
|
|
|
local function setUiPrefs()
|
|
local prevOverlay,prevFlash=hasOverlay,hasFlash
|
|
if frame then
|
|
if next(frame) ~= nil then
|
|
frame:delete() rt:delete() rl:delete() rb:delete() rr:delete() rflash:delete()
|
|
end
|
|
end
|
|
local ui=highlight.ui
|
|
local f={x=-5,y=0,w=1,h=1}
|
|
frame=drrect(f):setFill(false):setStroke(true):setStrokeWidth(ui.frameWidth):setBehavior(BEHAVIOR)
|
|
local function make() return drrect(f):setFill(true):setStroke(false):setBehavior(BEHAVIOR) end
|
|
rt,rl,rb,rr=make(),make(),make(),make()
|
|
rflash=drrect(f):setFill(true):setStroke(false):setBehavior(BEHAVIOR)
|
|
hasFrame=ui.frameColor.alpha>0 and ui.frameWidth>0
|
|
hasOverlay=ui.overlay and ui.overlayColor.alpha>0
|
|
hasFlash=ui.flashDuration>0 and (ui.windowShownFlashColor.alpha>0 or ui.windowHiddenFlashColor.alpha>0)
|
|
if hasFlash then tflash:setDelay(ui.flashDuration) end
|
|
if hasOverlay~=prevOverlay then log.i('overlay mode',hasOverlay and 'enabled' or 'disabled') end
|
|
if hasFlash~=prevFlash then log.i('flash',hasFlash and 'enabled' or 'disabled') end
|
|
return setMode()
|
|
end
|
|
highlight.ui=setmetatable({},{
|
|
__newindex=function(_,k,v) uiGlobal[k]=getColor(v) setUiPrefs() end,
|
|
__index=function(_,k)return getColor(uiGlobal[k])end,
|
|
})
|
|
|
|
|
|
--- hs.window.highlight.toggleIsolate([v])
|
|
--- Function
|
|
--- Sets or clears the user override for "isolate" mode.
|
|
---
|
|
--- Parameters:
|
|
--- * v - (optional) a boolean; if true, enable isolate mode; if false, disable isolate mode,
|
|
--- even when `windowfilterIsolate` passed to `.start()` would otherwise enable it; if omitted or nil,
|
|
--- toggle the override, i.e. clear it if it's currently enforced, or set it to the opposite of the current
|
|
--- isolate mode status otherwise.
|
|
---
|
|
--- Returns:
|
|
--- * None
|
|
---
|
|
--- Notes:
|
|
--- * This function should be bound to a hotkey, e.g.: `hs.hotkey.bind('ctrl-cmd','\','Isolate',hs.window.highlight.toggleIsolate)`
|
|
function highlight.toggleIsolate(v)
|
|
if not running then return end
|
|
if v==nil and isolateUser==nil then v=not isolate end
|
|
isolateUser=v
|
|
log.f('isolate user override%s',v==true and ': isolated' or (v==false and ': not isolated' or ' cancelled'))
|
|
if v==nil then settings.clear(SETTING_ISOLATE_OVERRIDE)
|
|
else settings.set(SETTING_ISOLATE_OVERRIDE,v) end
|
|
return setIsolate()
|
|
end
|
|
|
|
|
|
local screenWatcher
|
|
--- hs.window.highlight.start([windowfilterIsolate[, windowfilterOverlay]])
|
|
--- Function
|
|
--- Starts the module
|
|
---
|
|
--- Parameters:
|
|
--- * windowfilterIsolate - (optional) an `hs.window.filter` instance that automatically enable "isolate" mode
|
|
--- whenever one of the allowed windows is focused; alternatively, you can just provide a list of application
|
|
--- names and a windowfilter will be created for you that enables isolate mode whenever one of these apps is focused;
|
|
--- if omitted or nil, isolate mode won't be toggled automatically, but you can still toggle it manually via
|
|
--- `hs.window.higlight.toggleIsolate()`
|
|
--- * windowfilterOverlay - (optional) an `hs.window.filter` instance that determines which windows to consider
|
|
--- for "overlay" mode when focused; if omitted or nil, the default windowfilter will be used
|
|
---
|
|
--- Returns:
|
|
--- * None
|
|
---
|
|
--- Notes:
|
|
--- * overlay mode is disabled by default - see `hs.window.highlight.ui.overlayColor`
|
|
function highlight.start(wfis,wfov)
|
|
highlight.stop()
|
|
running=true
|
|
if wfov==nil then wfov=windowfilter.default else wfov=windowfilter.new(wfov) end
|
|
|
|
wfFlash=wfov
|
|
wfOverlay=windowfilter.copy(wfov,'wf-highlight'):setOverrideFilter{focused=true}
|
|
screenWatcher=screen.watcher.new(getScreens):start()
|
|
log.i'started'
|
|
getScreens()
|
|
isolateUser=settings.get(SETTING_ISOLATE_OVERRIDE)
|
|
setIsolate()
|
|
setUiPrefs()
|
|
redshift.invertSubscribe(setInvert)
|
|
if wfis~=nil then
|
|
if windowfilter.iswf(wfis) then wfIsolate=wfis
|
|
else
|
|
wfIsolate=windowfilter.new(wfis,'wf-redshift-isolate',log.getLogLevel())
|
|
modulewfIsolate=wfIsolate
|
|
if type(wfis=='table') then
|
|
local isAppList=true
|
|
for k,v in pairs(wfis) do
|
|
if type(k)~='number' or type(v)~='string' then isAppList=false break end
|
|
end
|
|
if isAppList then wfIsolate:setOverrideFilter{focused=true} end
|
|
end
|
|
end
|
|
wfIsolate:subscribe(isolatesubs,true)
|
|
end
|
|
end
|
|
|
|
|
|
--- hs.window.highlight.stop()
|
|
--- Function
|
|
--- Stops the module and disables focused window highlighting (both "overlay" and "isolate" mode)
|
|
---
|
|
--- Parameters:
|
|
--- * None
|
|
---
|
|
--- Returns:
|
|
--- * None
|
|
function highlight.stop()
|
|
if not running then return end
|
|
log.i'stopped'
|
|
if frame then frame:delete() rt:delete() rl:delete() rb:delete() rr:delete() rflash:delete() end
|
|
running=nil
|
|
wfOverlay:delete() wfOverlay=nil
|
|
wfFlash:unsubscribe(flashsubs)
|
|
redshift.invertUnsubscribe(setInvert)
|
|
|
|
if wfIsolate then
|
|
if modulewfIsolate then modulewfIsolate:delete() modulewfIsolate=nil
|
|
else wfIsolate:unsubscribe(isolatesubs) end
|
|
wfIsolate=nil
|
|
end
|
|
|
|
screenWatcher:stop()
|
|
-- focusedwf:delete()
|
|
end
|
|
--return highlight
|
|
return setmetatable(highlight,{__gc=highlight.stop})
|