Last active
November 15, 2020 22:10
-
-
Save Skinner927/5231cacbd1eb25c7dd239b3ee557becd to your computer and use it in GitHub Desktop.
Hammerspoon auto screen position script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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