hammerspoon/extensions/tabs/tabs.lua

205 lines
6.0 KiB
Lua

--- === hs.tabs ===
---
--- Place the windows of an application into tabs drawn on its titlebar
local tabs = {}
local drawing = require "hs.drawing"
local uielement = require "hs.uielement"
local watcher = uielement.watcher
local fnutils = require "hs.fnutils"
local application = require "hs.application"
local appwatcher = application.watcher
tabs.leftPad = 10
tabs.topPad = 2
tabs.tabPad = 2
tabs.tabWidth = 80
tabs.tabHeight = 17
tabs.tabRound = 4
tabs.textLeftPad = 2
tabs.textTopPad = 2
tabs.textSize = 10
tabs.fillColor = {red = 1.0, green = 1.0, blue = 1.0, alpha = 0.5}
tabs.selectedColor = {red = .9, green = .9, blue = .9, alpha = 0.5}
tabs.strokeColor = {red = 0.0, green = 0.0, blue = 0.0, alpha = 0.7}
tabs.textColor = {red = 0.0, green = 0.0, blue = 0.0, alpha = 0.6}
tabs.maxTitle = 11
local function realWindow(win)
-- AXScrollArea is weird role of special finder desktop window
return (win:isStandard() and win:role() ~= "AXScrollArea")
end
--- hs.tabs.tabWindows(app)
--- Function
--- Gets a list of the tabs of a window
---
--- Parameters:
--- * app - An `hs.application` object
---
--- Returns:
--- * An array of the tabbed windows of an app in the same order as they would be tabbed
---
--- Notes:
--- * This function can be used when writing tab switchers
function tabs.tabWindows(app)
local tabWins = fnutils.filter(app:allWindows(),realWindow)
table.sort(tabWins, function(a,b) return a:title() < b:title() end)
return tabWins
end
local drawTable = {}
local function trashTabs(pid)
local tab = drawTable[pid]
if not tab then return end
for _,obj in ipairs(tab) do
obj:delete()
end
end
local function drawTabs(app)
local pid = app:pid()
trashTabs(pid)
drawTable[pid] = {}
local proto = app:focusedWindow()
if not proto or not app:isFrontmost() then return end
local geom = app:focusedWindow():frame()
local tabWins = tabs.tabWindows(app)
local pt = {x = geom.x+geom.w-tabs.leftPad, y = geom.y+tabs.topPad}
local objs = drawTable[pid]
-- iterate in reverse order because we draw right to left
local numTabs = #tabWins
for i=0,(numTabs-1) do
local win = tabWins[numTabs-i]
pt.x = pt.x - tabs.tabWidth - tabs.tabPad
local r = drawing.rectangle({x=pt.x,y=pt.y,w=tabs.tabWidth,h=tabs.tabHeight})
r:setClickCallback(nil, function() tabs.focusTab(app, #tabs.tabWindows(app) - i) end)
r:setFill(true)
if win == proto then
r:setFillColor(tabs.selectedColor)
else
r:setFillColor(tabs.fillColor)
end
r:setStrokeColor(tabs.strokeColor)
r:setRoundedRectRadii(tabs.tabRound,tabs.tabRound)
r:bringToFront()
r:show()
table.insert(objs,r)
local tabText = win:title():sub(1,tabs.maxTitle)
local t = drawing.text({x=pt.x+tabs.textLeftPad,y=pt.y+tabs.textTopPad,
w=tabs.tabWidth,h=tabs.tabHeight},tabText)
t:setTextSize(tabs.textSize)
t:setTextColor(tabs.textColor)
t:show()
table.insert(objs,t)
end
end
local function reshuffle(app)
local proto = app:focusedWindow()
if not proto then return end
local geom = app:focusedWindow():frame()
for _,win in ipairs(app:allWindows()) do
if win:isStandard() then
win:setFrame(geom)
end
end
drawTabs(app)
end
local function manageWindow(win, app)
if not win:isStandard() then return end
-- only trigger on focused window movements otherwise the reshuffling triggers itself
local newWatch = win:newWatcher(function(el) if el == app:focusedWindow() then reshuffle(app) end end)
newWatch:start({watcher.windowMoved, watcher.windowResized, watcher.elementDestroyed})
local redrawWatch = win:newWatcher(function () drawTabs(app) end)
redrawWatch:start({watcher.elementDestroyed, watcher.titleChanged})
-- resize this window to match possible others
local notThis = fnutils.filter(app:allWindows(), function(x) return (x ~= win and realWindow(x)) end)
local protoWindow = notThis[1]
if protoWindow then
print("Prototyping to '" .. protoWindow:title() .. "'")
win:setFrame(protoWindow:frame())
end
end
local function watchApp(app)
-- print("Enabling tabs for " .. app:title())
for _,win in ipairs(app:allWindows()) do
manageWindow(win,app)
end
local winWatch = app:newWatcher(function(el,_,_,appl) manageWindow(el,appl) end,app)
winWatch:start({watcher.windowCreated})
local redrawWatch = app:newWatcher(function () drawTabs(app) end)
redrawWatch:start({watcher.applicationActivated, watcher.applicationDeactivated,
watcher.applicationHidden, watcher.focusedWindowChanged})
reshuffle(app)
end
local appWatcherStarted = false
local appWatches = {}
--- hs.tabs.enableForApp(app)
--- Function
--- Places all the windows of an app into one place and tab them
---
--- Parameters:
--- * app - An `hs.application` object or the app title
---
--- Returns:
--- * None
function tabs.enableForApp(app)
if type(app) == "string" then
appWatches[app] = true
app = application.get(app)
end
-- might already be running
if app then
appWatches[app:title()] = true
watchApp(app)
end
-- set up a watcher to catch any watched app launching or terminating
if appWatcherStarted then return end
appWatcherStarted = true
local watch = appwatcher.new(function(name,event,theApp)
-- print("Event from " .. name)
if event == appwatcher.launched and appWatches[name] then
watchApp(theApp)
elseif event == appwatcher.terminated then
trashTabs(theApp:pid())
end
end)
watch:start()
end
--- hs.tabs.focusTab(app, num)
--- Function
--- Focuses a specific tab of an app
---
--- Parameters:
--- * app - An `hs.application` object previously enabled for tabbing
--- * num - A tab number to switch to
---
--- Returns:
--- * None
---
--- Notes:
--- * If num is higher than the number of tabs, the last tab will be focussed
function tabs.focusTab(app,num)
if not app or not appWatches[app:title()] then return end
local theTabs = tabs.tabWindows(app)
local bounded = num
--print(hs.inspect(tabs))
if num > #theTabs then
bounded = #theTabs
end
theTabs[bounded]:focus()
end
return tabs