Skip to content

Instantly share code, notes, and snippets.

@Skinner927
Last active November 15, 2020 22:10
Show Gist options
  • Save Skinner927/5231cacbd1eb25c7dd239b3ee557becd to your computer and use it in GitHub Desktop.
Save Skinner927/5231cacbd1eb25c7dd239b3ee557becd to your computer and use it in GitHub Desktop.
Hammerspoon auto screen position script
hs.urlevent.bind('reload', function(eventName, params)
hs.reload()
end)
hs.urlevent.bind('currentScreenTag', function(eventName, params)
alert(getScreenTag())
end)
hs.urlevent.bind("printLayout", function(eventName, params)
config = {}
for k, app in pairs(hs.application.runningApplications()) do
window = app:mainWindow()
if window ~= nil then
cords = window:topLeft()
size = window:size()
table.insert(config, { app:name(), nil, window:screen():name(), nil, {x=cords.x, y=cords.y, w=size.w, h=size.h}, nil})
end
end
print(table.show(config))
hs.openConsole(true)
end)
hs.urlevent.bind('launchApps', function(eventName, params)
--[[
set key to the name of the application (window name specifically,
see appName option for more info).
set the value to:
- false to do nothing with the window.
- A table of x,y & w,h for width and height,
or center=ScreenToCenterOn to simply center the window on screen,
or fill=ScreenToFill to make the window fill the screen (like maximize).
NOTE: When specifiying a screen to center or fill on, be aware that
the string specified is matched as a pattern. By this, the following
characters have special meaning and should be avoided if possible:
( ) . % + - * ? [ ^ $
You can escape a special character with a %.
For more information see: https://www.lua.org/pil/20.2.html
Optional:
- hideAlerts=true to no display alert if app or window cannot be found
- maxLoops for number of tries to attemp to find app or window.
Each loop fires at 500ms intervals.
- appName if the application name differs from the window name, use
this string to launch the application.
--]]
screenConfigs = {
-- Work
[{'DELL U2415', 'Color LCD', 'DELL U2211H'}] = {
['Google Chrome'] = {fill='DELL U2211H'},
Messages = {x=1926, y=524, h=520, w=735},
Mattermost = {x=2592, y=338, w=1000, h=705},
Slack = {x=1926, y=28, w=966, h=672, maxLoops=20},
PyCharm = {fill='DELL U2415', hideAlerts=true, maxLoops=40, appName='PyCharm CE'},
},
-- Home (old)
[{'Color LCD', 'E275W-1920', 'VS248'}] = {
['Google Chrome'] = {fill='VS248'},
Messages = {x=1102, y=614, w=813, h=520},
Mattermost = {x=898, y=23, w=1013, h=670},
Slack = {x=0, y=486, w=960, h=647, maxLoops=20},
PyCharm = {fill='E275W%-1920', hideAlerts=true, maxLoops=40, appName='PyCharm CE'},
},
-- Home PC
[{'SE2717H/HX', 'Color LCD', 'SE2717H/HX'}] = {
['Google Chrome'] = {fill='-1,0'},
Messages = {x=-975, y=1610, w=735, h=520},
Mattermost = {x=-1920, y=1425, w=1000, h=705},
Slack = {x=-1634, y=1103, w=1152, h=799, maxLoops=20},
PyCharm = {fill='0,0', hideAlerts=true, maxLoops=40, appName='PyCharm CE'},
}
}
--[[
Here's the old ll config, worth saving.
screenConfigs = {
-- Home
[{'Color LCD', 'E275W-1920', 'VS248'}] = {
--['Google Chrome'] = {x=-699, y=-1057, w=1920, h=1057},
['Google Chrome'] = {fill='VS248'},
Messages = {x=1213, y=662, w=813, h=564},
Slack = {x=0, y=486, w=960, h=647},
Mail = {x=1223, y=55, w=813, h=564},
--PhpStorm = {center='E275W', hideAlerts=true, maxLoops=40},
--PhpStorm = {x=1221, y=-1057, w=1920, h=1057, hideAlerts=true, maxLoops=40},
PhpStorm = {center='E275W', hideAlerts=true, maxLoops=40},
},
-- CoWork
[{'SMEX2220', 'ASUS PB278', 'VE198'}] = {
['Google Chrome'] = {x=0, y=23, w=1920, h=1011},
Messages = {x=1782, y=-569, w=813, h=564},
Slack = {x=1172, y=-651, w=960, h=647},
Mail = {x=1530, y=-877, w=1086, h=673},
PhpStorm = {x=1920, y=23, w=2560, h=1417, hideAlerts=true, maxLoops=40},
}
}
--]]
log = hs.logger.new('messenger', 'verbose')
-- We don't VPN anymore
-- Start VPN
-- hs.osascript.applescript([[
-- tell application "Tunnelblick"
-- connect "llVPN"
-- end tell
-- ]])
screenTag = {}
for i, screen in ipairs(hs.screen.allScreens()) do
table.insert(screenTag, screen:name())
end
foundConfig = nil
for key, config in pairs(screenConfigs) do
if compareValues(screenTag, key, true) then
foundConfig = config
break
end
end
foundScreenTag = true
if foundConfig == nil then
msg = 'Cannot find the screen tag: \n' .. getScreenTag() .. "\n\nPress 'Load Apps' to load default apps or 'Cancel' to abort."
if alert(msg, true, {'Load Apps', 'Cancel'}) == 'Load Apps' then
--hs.notify.new({title='Cannot find screen config', informativeText=getScreenTag()}):send()
foundScreenTag = false
-- Fallback to 1
k = next(screenConfigs)
foundConfig = screenConfigs[k]
else
return
end
end
-- Launch each application
for appName, windowConfig in pairs(foundConfig) do
if windowConfig.appName ~= nil then
appName = windowConfig.appName
end
log.i('Launching ' .. appName)
hs.application.open(appName)
end
skipAppCount = {}
skipWindowCount = {}
THRESHOLD = 10
function arrangeWindows()
log.i('starting loop')
for appName, windowConfig in pairs(foundConfig) do
app = nil
window = nil
loopThreshold = windowConfig.maxLoops or THRESHOLD
if windowConfig == nil or windowConfig == false then
-- skip
goto cleanup
end
log.i('Looking for application ' .. appName)
log.d('Loop app:' .. (skipAppCount[appName] or 0) ..
' window:' .. (skipWindowCount[appName] or 0) ..
' of:' .. loopThreshold)
app = hs.application.find(appName)
if app == nil then
table.safeAdd(skipAppCount, appName, 1)
err = 'Cannot find the application: ' .. appName
log.d(err .. ' -- looping')
if skipAppCount[appName] > loopThreshold then
if not windowConfig.hideAlerts then
alert(err, true, {'OK'})
else
err = err .. ' -- hiding alert per config'
end
log.e(err)
goto cleanup
end
goto continue
end
status, window = pcall(function() return app:mainWindow() end)
if not status or window == nil then
table.safeAdd(skipWindowCount, appName, 1)
err = 'Cannot find the application window: ' .. appName
log.d(err .. ' -- looping')
if skipWindowCount[appName] > loopThreshold then
if not windowConfig.hideAlerts then
alert(err, true, {'OK'})
else
err = err .. ' -- hiding alert per config'
end
log.e(err)
goto cleanup
end
goto continue
end
-- All is good if we made it here
if windowConfig.center ~= nil then
-- Simply center the window
window:centerOnScreen(windowConfig.center, true)
elseif windowConfig.fill ~= nil then
-- Make window fill screen
screen = hs.screen.find(windowConfig.fill)
mode = screen:currentMode()
coords = screen:localToAbsolute({x=0, y=0, w=mode.w, h=mode.h})
window:setTopLeft(coords)
window:setSize(coords)
else
-- Position and resize the window
window:setTopLeft(windowConfig)
window:setSize(windowConfig)
end
::cleanup::
foundConfig[appName] = nil
::continue::
end
end
arrangeWindows()
hs.timer.doUntil(
function()
if table.length(foundConfig) == 0 then
log.i('Done!')
return true
else
return false
end
end,
arrangeWindows,
0.5)
end)
-- Compare values of 2 tables in any order
function compareValues(t1, t2)
local t1Count = 0
for k1, v1 in pairs(t1) do
t1Count = t1Count + 1
local found = false
for k2, v2 in pairs(t2) do
if v1 == v2 then
found = true
break
end
end
if found == false then
return false
end
end
-- Everything in t1 matched t2
local t2Count = 0
for k2, v2 in pairs(t2) do
t2Count = t2Count + 1
end
-- If same number of keys we're good
return t1Count == t2Count
end
function deepcompare(t1,t2,ignore_mt)
-- https://web.archive.org/web/20131225070434/http://snippets.luacode.org/snippets/Deep_Comparison_of_Two_Values_3
local ty1 = type(t1)
local ty2 = type(t2)
if ty1 ~= ty2 then return false end
-- non-table types can be directly compared
if ty1 ~= 'table' and ty2 ~= 'table' then return t1 == t2 end
-- as well as tables which have the metamethod __eq
local mt = getmetatable(t1)
if not ignore_mt and mt and mt.__eq then return t1 == t2 end
for k1,v1 in pairs(t1) do
local v2 = t2[k1]
if v2 == nil or not deepcompare(v1,v2) then return false end
end
for k2,v2 in pairs(t2) do
local v1 = t1[k2]
if v1 == nil or not deepcompare(v1,v2) then return false end
end
return true
end
function alert(text, isError, buttons)
--[[
Will return true on 'OK' and false on 'Cancel'.
isError is optional and if true will add the error icon.
Butttons are optional and if array of strings, will return the text value of the
button clicked or false on failure.
--]]
-- Real cheap sub of non-escaped double quote
text = text:gsub('([^\\])"', '%1\\"')
text = string.format('display dialog "%s"', text)
if buttons ~= nil then
txtButtons = '{"' .. table.concat(buttons, '", "') .. '"}'
text = text .. ' buttons ' .. txtButtons
end
if isError then
if isError == true then
text = text .. ' with icon caution'
else
text = text .. ' with icon ' .. isError
end
end
success, result, output = hs.osascript.applescript(text)
if success and buttons ~= nil then
-- Return the text of the winning button
buttonNames = {}
for i, btn in pairs(buttons) do
table.insert(buttonNames, btn)
end
-- longest to shortest
table.sort(buttonNames, function(a,b) return #a > #b end)
-- Try to find match
for i, btn in ipairs(buttonNames) do
if output:find(btn) ~= null then
return btn
end
end
return false
else
return success
end
end
function getScreenTag()
screenTag = ''
for k, screen in ipairs(hs.screen.allScreens()) do
screenTag = screenTag .. screen:name() .. ','
end
if prefix ~= nil and prefix ~= '' then
screenTag = prefix .. screenTag
end
-- Drop trailing comma
return screenTag:sub(1, -2)
end
function table.val_to_str ( v )
if "string" == type( v ) then
v = string.gsub( v, "\n", "\\n" )
if string.match( string.gsub(v,"[^'\"]",""), '^"+$' ) then
return "'" .. v .. "'"
end
return '"' .. string.gsub(v,'"', '\\"' ) .. '"'
else
return "table" == type( v ) and table.tostring( v ) or
tostring( v )
end
end
function table.key_to_str ( k )
if "string" == type( k ) and string.match( k, "^[_%a][_%a%d]*$" ) then
return k
else
return "[" .. table.val_to_str( k ) .. "]"
end
end
function table.tostring( tbl )
local result, done = {}, {}
for k, v in ipairs( tbl ) do
table.insert( result, table.val_to_str( v ) )
done[ k ] = true
end
for k, v in pairs( tbl ) do
if not done[ k ] then
table.insert( result,
table.key_to_str( k ) .. "=" .. table.val_to_str( v ) )
end
end
return "{" .. table.concat( result, "," ) .. "}"
end
function table.show(t, name, indent)
local cart -- a container
local autoref -- for self references
--[[ counts the number of elements in a table
local function tablecount(t)
local n = 0
for _, _ in pairs(t) do n = n+1 end
return n
end
]]
-- (RiciLake) returns true if the table is empty
local function isemptytable(t) return next(t) == nil end
local function basicSerialize (o)
local so = tostring(o)
if type(o) == "function" then
local info = debug.getinfo(o, "S")
-- info.name is nil because o is not a calling level
if info.what == "C" then
return string.format("%q", so .. ", C function")
else
-- the information is defined through lines
return string.format("%q", so .. ", defined in (" ..
info.linedefined .. "-" .. info.lastlinedefined ..
")" .. info.source)
end
elseif type(o) == "number" or type(o) == "boolean" then
return so
else
return string.format("%q", so)
end
end
local function addtocart (value, name, indent, saved, field)
indent = indent or ""
saved = saved or {}
field = field or name
cart = cart .. indent .. field
if type(value) ~= "table" then
cart = cart .. " = " .. basicSerialize(value) .. ";\n"
else
if saved[value] then
cart = cart .. " = {}; -- " .. saved[value]
.. " (self reference)\n"
autoref = autoref .. name .. " = " .. saved[value] .. ";\n"
else
saved[value] = name
--if tablecount(value) == 0 then
if isemptytable(value) then
cart = cart .. " = {};\n"
else
cart = cart .. " = {\n"
for k, v in pairs(value) do
k = basicSerialize(k)
local fname = string.format("%s[%s]", name, k)
field = string.format("[%s]", k)
-- three spaces between levels
addtocart(v, fname, indent .. " ", saved, field)
end
cart = cart .. indent .. "};\n"
end
end
end
end
name = name or "__unnamed__"
if type(t) ~= "table" then
return name .. " = " .. basicSerialize(t)
end
cart, autoref = "", ""
addtocart(t, name, indent)
return cart .. autoref
end
-- Return number of non-nil table entries
function table.length(tbl)
count = 0
for k, v in pairs(tbl) do
if v ~= nil then
count = count + 1
end
end
return count
end
-- Safely add a value to a table's key
function table.safeAdd(tbl, key, value)
if tbl[key] == nil then
tbl[key] = 0
end
tbl[key] = tbl[key] + value
end
hs.urlevent.bind('useSoundBlaster', function(eventName, params)
name = "Sound Blaster Play! 3"
dev = hs.audiodevice.findOutputByName(name)
if dev then
dev:setDefaultOutputDevice()
if not dev:setDefaultInputDevice() then
dev = hs.audiodevice.findInputByName(name)
if dev then
dev:setDefaultInputDevice()
end
end
end
end)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment