I’m starting to try using HammerSpoon, which is driven by Lua, for automatically arranging my applications depending on application scenarios.
I have 3 screens, including a MacBook Pro at Retina native resolution and a Seiki 4K display. I grabbed the names of the monitors from Display Menu‘s menu bar menu.
I initially made the mistake of looking for “Outlook”, “OneNote”, and “Chrome”, but all three applications require their respective company’s names to be included in the application name.
applications = { "Messages", "iTunes", "Skype for Business", "iTerm2", "Microsoft Outlook", "HipChat", "Microsoft OneNote", "MacVim", "RubyMine", "Firefox" } | |
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "pageup", function() | |
for _k, app_name in pairs(applications) do | |
hs.application.launchOrFocus(app_name) | |
print(app_name) | |
app = hs.application.find(app_name) | |
if app then | |
hs.application.unhide(app) | |
end | |
end | |
local lgUltra="LG Ultra HD" | |
local windowLayout = { | |
— upper right quadrant | |
{"Firefox", nil, lgUltra, hs.geometry.rect(0,0,0.5,0.5), nil, nil}, | |
— upper left quadrant | |
{"Microsoft Teams", nil, lgUltra, hs.geometry.rect(0.5,0,0.5,0.5), nil, nil}, | |
— lower right quadrant | |
{"Skype for Business", nil, lgUltra, hs.geometry.rect(0.5,0.5,0.5,0.5), nil, nil}, | |
— lower left quadrant | |
{"iTerm2", nil, lgUltra, hs.geometry.rect(0,0.5,0.5,0.5), nil, nil} | |
} | |
hs.layout.apply(windowLayout) | |
local laptopScreen="Color LCD" | |
local windowLayout = { | |
{"Microsoft Outlook", nil, laptopScreen, hs.geometry.rect(0.5,0,0.5,0.5), nil, nil}, | |
{"HipChat", nil, laptopScreen, hs.geometry.rect(0.5,0.5,0.5,0.5), nil, nil}, | |
{"Microsoft OneNote", nil, laptopScreen, hs.geometry.rect(0,0.5,0.5,0.5), nil, nil}, | |
{"Notes", nil, laptopScreen, hs.geometry.rect(0,0,0.5,0.5), nil, nil} | |
} | |
hs.layout.apply(windowLayout) | |
local seiki4KScreen="SE39UY04" | |
local windowLayout = { | |
{"MacVim", nil, seiki4KScreen, hs.geometry.rect(0,0,0.5,0.5), nil, nil}, | |
{"Emacs", nil, seiki4KScreen, hs.geometry.rect(0.5,0,0.5,0.5), nil, nil}, | |
{"RubyMine-EAP", nil, seiki4KScreen, hs.geometry.rect(0,0.5,0.5,0.5), nil, nil}, | |
{"iTerm2", nil, seiki4KScreen, hs.geometry.rect(0.5,0.5,0.5,0.5), nil, nil} | |
} | |
hs.layout.apply(windowLayout) | |
end) | |
— Convert a lua table into a lua syntactically correct string | |
function table_to_string(tbl) | |
local result = "{" | |
for k, v in pairs(tbl) do | |
— Check the key type (ignore any numerical keys – assume its an array) | |
if type(k) == "string" then | |
result = result.."[\""..k.."\"]".."=" | |
end | |
— Check the value type | |
if type(v) == "table" then | |
result = result..table_to_string(v) | |
elseif type(v) == "boolean" then | |
result = result..tostring(v) | |
else | |
result = result.."\""..v.."\"" | |
end | |
result = result.."," | |
end | |
— Remove leading commas from the result | |
if result ~= "" then | |
result = result:sub(1, result:len()–1) | |
end | |
return result.."}" | |
end | |
hs.hotkey.bind({}, "f13", function() | |
hs.location.start() | |
locationTable = hs.location.get() | |
hs.eventtap.keyStrokes(locationTable["latitude"] .. "," .. locationTable["longitude"]) | |
print(table_to_string(locationTable)) | |
end) | |
function move_to_third(r,c) | |
local win = hs.window.focusedWindow() | |
local f = win:frame() | |
local screen = win:screen() | |
local extents = screen:frame() | |
f.x = extents.x + (extents.w / 3) * c | |
f.y = extents.y + (extents.h / 3) * r | |
f.w = extents.w / 3 | |
f.h = extents.h / 3 | |
win:setFrame(f) | |
end | |
function move_to_two_third_horz(c) | |
local win = hs.window.focusedWindow() | |
local f = win:frame() | |
local screen = win:screen() | |
local extents = screen:frame() | |
f.x = extents.x + (extents.w / 3) * c | |
f.y = extents.y | |
f.w = extents.w / 3 * 2 | |
f.h = extents.h | |
win:setFrame(f) | |
end | |
function move_to_two_third_vert(r) | |
local win = hs.window.focusedWindow() | |
local f = win:frame() | |
local screen = win:screen() | |
local extents = screen:frame() | |
f.x = extents.x | |
f.y = extents.y + (extents.h / 3) * r | |
f.w = extents.w | |
f.h = extents.h / 3 * 2 | |
win:setFrame(f) | |
end | |
function move_to_four_ninths(r,c) | |
local win = hs.window.focusedWindow() | |
local f = win:frame() | |
local screen = win:screen() | |
local extents = screen:frame() | |
f.x = extents.x + (extents.w / 3) * c | |
f.y = extents.y + (extents.h / 3) * r | |
f.w = extents.w / 3 * 2 | |
f.h = extents.h / 3 * 2 | |
win:setFrame(f) | |
end | |
for i=1,9 do | |
hs.hotkey.bind({"cmd", "alt", "ctrl"}, tostring(i), function() | |
move_to_third(math.floor((i–1)/3),(i–1)%3) | |
end) | |
end | |
for i=1,9 do | |
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "pad"..tostring(i), function() | |
move_to_third(math.floor((9–i)/3),(i–1)%3) | |
end) | |
end | |
for i=1,9 do | |
hs.hotkey.bind({"cmd", "alt", "ctrl", "shift"}, "pad"..tostring(i), function() | |
if i == 1 then move_to_four_ninths(1,0) | |
elseif i == 3 then move_to_four_ninths(1,1) | |
elseif i == 7 then move_to_four_ninths(0,0) | |
elseif i == 9 then move_to_four_ninths(0,1) | |
elseif i == 2 then move_to_two_third_vert(1) | |
elseif i == 4 then move_to_two_third_horz(0) | |
elseif i == 6 then move_to_two_third_horz(1) | |
elseif i == 8 then move_to_two_third_vert(0) | |
end | |
end) | |
end | |
— https://www.hammerspoon.org/docs/hs.application.watcher.html | |
— https://www.lua.org/pil/9.1.html | |
— https://stackoverflow.com/questions/16984540/how-to-pass-a-function-as-a-parameter-in-lua | |
— hs.application.watcher.new() | |
— TODO Plan: | |
— Create application watcher | |
— | |
currentApp = "" | |
function applicationWatcher(appName, eventType, appObject) | |
if(eventType == hs.application.watcher.activated) then | |
if (appName == "HipChat") then | |
currentApp = "HipChat" | |
end | |
if (appName == "Google Chrome") then | |
currentApp = "Chrome" | |
end | |
end | |
end | |
function alertCurrent() | |
hs.alert(currentApp) | |
end | |
—hs.timer.doEvery(5, alertCurrent) | |
appWatcher = hs.application.watcher.new(applicationWatcher) | |
appWatcher:start() | |
— Dispatch application coroutine on activate | |
— Pause coroutine on deactivate | |
— Application-specific coroutine tracks exactly the important info for that app. | |
{"HipChat", nil, laptopScreen, hs.geometry.rect(0.5,0.5,0.5,0.5), nil, nil}, | |
{"Microsoft OneNote", nil, laptopScreen, hs.geometry.rect(0,0.5,0.5,0.5), nil, nil} | |
} | |
hs.layout.apply(windowLayout) | |
local seiki4KScreen="SE39UY04" | |
local windowLayout = { | |
{"MacVim", nil, seiki4KScreen, hs.geometry.rect(0,0,0.5,0.5), nil, nil}, | |
{"RubyMine", nil, seiki4KScreen, hs.geometry.rect(0.5,0,0.5,1.0), nil, nil}, | |
{"Google Chrome", nil, seiki4KScreen, hs.geometry.rect(0,0.5,0.5,0.5), nil, nil} | |
} | |
hs.layout.apply(windowLayout) | |
end) |