Skip to content

Instantly share code, notes, and snippets.

@23maverick23
Created October 1, 2018 14:57
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 23maverick23/b7dce3d1baff2cf2b4dad7a0a8a750b9 to your computer and use it in GitHub Desktop.
Save 23maverick23/b7dce3d1baff2cf2b4dad7a0a8a750b9 to your computer and use it in GitHub Desktop.
OSX: Hammerspoon scripts
apps = {}
apps.launchers = {
{
name="Home - Coding",
description="Launch apps for coding.",
apps={
"Sublime Text",
"iTerm",
"Dash"
}
},
{
name="Work - Essential",
description="Launch bare minimum apps (for single screen).",
apps={
"Firefox",
"Fantastical 2"
}
},
{
name="Work - Full Spread",
description="Launch apps used in multi-monitor setup.",
apps={
"Firefox",
"Fantastical 2",
"Sublime Text"
}
},
{
name="Work - Coding",
description="Launch apps for coding projects at work.",
apps={
"Sublime Text",
"iTerm"
}
}
}
function apps.launchApps(launcher)
print('Launcher ' .. launcher.name .. ' selected')
for _, app in ipairs(launcher.apps) do
if app then
hs.application.launchOrFocus(app)
end
end
end
apps.launcher = hs.chooser.new(function(selection)
if not selection then return end
apps.launchApps(apps.launchers[selection.uuid])
end)
local i = 0
local choices = hs.fnutils.imap(apps.launchers, function(launcher)
i = i + 1
return {
uuid=i,
text=launcher.name,
subText=launcher.description
}
end)
apps.launcher:choices(choices)
apps.launcher:searchSubText(true)
apps.launcher:rows(#choices)
apps.launcher:width(20)
return apps
-- Main grid methods
local grid = {}
local window = hs.window
fullScreen = false
function applicationWatcher(appName, eventType, appObject)
if (eventType == hs.application.watcher.activated) then
fullScreen = false
if (appObject) then
local focusedWindow = appObject:focusedWindow()
if focusedWindow then
local focusedScreen = focusedWindow:screen()
if (focusedScreen) then
local screenName = focusedScreen:name()
print('Screen name = ' .. tostring(screenName))
if (screenName == "VG2439 Series") then
fullScreen = true
else
fullScreen = false
end
end
end
end
end
end
appWatcher = hs.application.watcher.new(applicationWatcher)
appWatcher:start()
function grid.snap(win, x, y, w, h)
local newframe = {
x = x,
y = y,
w = w,
h = h,
}
win:setFrame(newframe, 0)
end
function grid.getAllValidWindows()
local allWindows = hs.window.allWindows()
local windows = {}
local index = 1
for i = 1, #allWindows do
local w = allWindows[i]
if w:screen() then
windows[index] = w
index = index + 1
end
end
return windows
end
function grid.screenFrameSize(win)
local s = nil
print('Full screen = ' .. tostring(fullScreen))
if (fullScreen == true) then
s = win:screen():fullFrame()
else
s = win:screen():frame()
end
return s
end
-- +------------------+
-- | +--------------+ +
-- | | | |
-- | | HERE | |
-- | | | |
-- | +--------------+ |
-- +------------------+
function grid.center_fullscreen()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x, s.y, s.w, s.h)
end
-- +-----------------+
-- | HERE |
-- +-----------------+
-- | |
-- +-----------------+
function grid.snap_north()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x, 0, s.w, s.h/2)
end
-- +-----------------+
-- | |
-- +-----------------+
-- | HERE |
-- +-----------------+
function grid.snap_south()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x, s.y+(s.h/2), s.w, s.h/2)
end
-- +-----------------+
-- | | |
-- | | HERE |
-- | | |
-- +-----------------+
function grid.snap_east()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x+s.w/2, 0, s.w/2, s.h)
end
-- +-----------------+
-- | | |
-- | HERE | |
-- | | |
-- +-----------------+
function grid.snap_west()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x, 0, s.w/2, s.h)
end
-- +-----------------+
-- | HERE | |
-- +--------+ |
-- | |
-- +-----------------+
function grid.snap_northwest()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x, 0, s.w/2, s.h/2)
end
-- +-----------------+
-- | | HERE |
-- | +--------|
-- | |
-- +-----------------+
function grid.snap_northeast()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x+s.w/2, 0, s.w/2, s.h/2)
end
-- +-----------------+
-- | |
-- +--------+ |
-- | HERE | |
-- +-----------------+
function grid.snap_southwest()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x, s.y+(s.h/2), s.w/2, s.h/2)
end
-- +-----------------+
-- | |
-- | +--------|
-- | | HERE |
-- +-----------------+
function grid.snap_southeast()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x+s.w/2, s.y+(s.h/2), s.w/2, s.h/2)
end
-- +-----------------+
-- | | |
-- | H | |
-- | | |
-- +-----------------+
function grid.snap_west_one_third()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x, 0, s.w/3, s.h)
end
-- +-----------------+
-- | | |
-- | HERE | |
-- | | |
-- +-----------------+
function grid.snap_west_two_thirds()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x, 0, s.w*(2/3), s.h)
end
-- +-----------------+
-- | | |
-- | | H |
-- | | |
-- +-----------------+
function grid.snap_east_one_third()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x+s.w*(2/3), 0, s.w/3, s.h)
end
-- +-----------------+
-- | | |
-- | | HERE |
-- | | |
-- +-----------------+
function grid.snap_east_two_thirds()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x+s.w/3, 0, s.w*(2/3), s.h)
end
-- +-----------------+
-- | HERE |
-- +-----------------+
-- | |
-- | |
-- +-----------------+
function grid.snap_north_one_third()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x, 0, s.w, s.h/3)
end
-- +-----------------+
-- | |
-- | HERE |
-- +-----------------+
-- | |
-- +-----------------+
function grid.snap_north_two_thirds()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x, 0, s.w, s.h*(2/3))
end
-- +-----------------+
-- | |
-- | |
-- +-----------------+
-- | HERE |
-- +-----------------+
function grid.snap_south_one_third()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x, s.y+(s.h*(2/3)), s.w, s.h/3)
end
-- +-----------------+
-- | |
-- +-----------------+
-- | HERE |
-- | |
-- +-----------------+
function grid.snap_south_two_thirds()
local win = window.focusedWindow()
local s = grid.screenFrameSize(win)
grid.snap(win, s.x, s.y+(s.h/3), s.w, s.h*(2/3))
end
return grid
--[[
IMPORTS
--]]
local grid = require "modules.grid"
local layouts = require "modules.layouts"
local apps = require "modules.apps"
-- local menubar = require "modules.menubar"
-- local pomodoro = require "modules.pomodoro"
--[[
MAIN
--]]
-- Capture the hostname, so we can make this config behave differently across my Macs
hostname = hs.host.localizedName()
-- Ensure the IPC command line client is available
hs.ipc.cliInstall()
-- Watchers and other useful objects
configFileWatcher = nil
wifiWatcher = nil
screenWatcher = nil
usbWatcher = nil
appWatcher = nil
-- window hints
hs.hints.showTitleThresh = 0
-- Define monitor names for layout purposes
local display_home = "f.lux profile"
local display_laptop = "Color LCD"
local display_monitor = "Thunderbolt Display"
local display_monitor2 = "HD 709-A"
-- USB hub
local docking_station = "BRCM20702 Hub"
-- Define keyboard names for USB events
local microsoft_keyboard = "Natural® Ergonomic Keyboard 4000"
-- Defines for WiFi watcher
homeSSID = "The LAN Before Time" -- My 2.5 home WiFi SSID
homeSSID5 = "The LAN Before Time+" -- My 5.0 home WiFi SSID
lastSSID = hs.wifi.currentNetwork()
-- Defines for screen watcher
lastNumberOfScreens = #hs.screen.allScreens()
print('Last number of screens: ' .. lastNumberOfScreens)
-- Set grid options
hs.grid.MARGINX = 0
hs.grid.MARGINY = 0
hs.grid.ui.textSize = 50
hs.grid.HINTS = {
{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'},
{'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'},
{'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';'},
{'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/'}
}
-- Set window animation off. It's much smoother.
hs.window.animationDuration = 0
-- Defines for window grid
if (hostname == "Bessie") then
hs.grid.GRIDWIDTH = 6
hs.grid.GRIDHEIGHT = 6
else
hs.grid.GRIDWIDTH = 12
hs.grid.GRIDHEIGHT = 12
end
--[[
KEYBINDINGS
--]]
local mash = {"cmd", "alt", "ctrl"}
local mashshift = {"cmd", "alt", "ctrl", "shift"}
hs.hotkey.bind(mash, 'G', function() hs.hints.windowHints(grid.getAllValidWindows()) end)
hs.hotkey.bind(mashshift, 'G', function() hs.grid.show() end)
hs.hotkey.bind(mash, ';', function() hs.grid.snap(hs.window.focusedWindow()) end)
hs.hotkey.bind(mash, "'", function() hs.fnutils.map(hs.window.visibleWindows(), hs.grid.snap) end)
hs.hotkey.bind(mash, '=', function() hs.grid.resizeWindowWider() end)
hs.hotkey.bind(mash, '-', function() hs.grid.resizeWindowThinner() end)
hs.hotkey.bind(mashshift, '=', function() hs.grid.resizeWindowTaller() end)
hs.hotkey.bind(mashshift, '-', function() hs.grid.resizeWindowShorter() end)
hs.hotkey.bind(mash, 'left', function() hs.grid.pushWindowLeft() end)
hs.hotkey.bind(mash, 'right', function() hs.grid.pushWindowRight() end)
hs.hotkey.bind(mash, 'up', function() hs.grid.pushWindowUp() end)
hs.hotkey.bind(mash, 'down', function() hs.grid.pushWindowDown() end)
hs.hotkey.bind(mashshift, 'left', function() hs.window.focusedWindow():focusWindowWest() end)
hs.hotkey.bind(mashshift, 'right', function() hs.window.focusedWindow():focusWindowEast() end)
hs.hotkey.bind(mashshift, 'up', function() hs.window.focusedWindow():focusWindowNorth() end)
hs.hotkey.bind(mashshift, 'down', function() hs.window.focusedWindow():focusWindowSouth() end)
hs.hotkey.bind(mash, 'M', function() hs.grid.maximize_window() end)
hs.hotkey.bind(mash, 'F', function() hs.window.focusedWindow():toggleFullScreen() end)
hs.hotkey.bind(mash, 'C', grid.center_fullscreen)
hs.hotkey.bind(mashshift, 'C', function() hs.window.focusedWindow():centerOnScreen(nil, true) end)
-- Halves
hs.hotkey.bind(mash, 'J', grid.snap_north)
hs.hotkey.bind(mash, 'K', grid.snap_south)
hs.hotkey.bind(mash, 'L', grid.snap_east)
hs.hotkey.bind(mash, 'H', grid.snap_west)
hs.hotkey.bind(mash, 'U', grid.snap_northwest)
hs.hotkey.bind(mash, 'I', grid.snap_northeast)
hs.hotkey.bind(mash, 'O', grid.snap_southwest)
hs.hotkey.bind(mash, 'Y', grid.snap_southeast)
-- Thirds
hs.hotkey.bind(mashshift, 'H', grid.snap_west_one_third)
hs.hotkey.bind(mashshift, 'J', grid.snap_west_two_thirds)
hs.hotkey.bind(mashshift, 'K', grid.snap_east_one_third)
hs.hotkey.bind(mashshift, 'L', grid.snap_east_two_thirds)
hs.hotkey.bind(mashshift, 'Y', grid.snap_north_one_third)
hs.hotkey.bind(mashshift, 'U', grid.snap_north_two_thirds)
hs.hotkey.bind(mashshift, 'I', grid.snap_south_one_third)
hs.hotkey.bind(mashshift, 'O', grid.snap_south_two_thirds)
-- Screens
hs.hotkey.bind(mash, 'N', function() hs.grid.pushWindowNextScreen() end)
hs.hotkey.bind(mash, 'P', function() hs.grid.pushWindowPrevScreen() end)
-- Pomodoro key binding
-- hs.hotkey.bind(mash, '9', pomodoro.pom_enable)
-- hs.hotkey.bind(mash, '0', pomodoro.pom_disable)
-- hs.hotkey.bind(mashshift, '0', pomodoro.pom_reset_work)
-- Apps launcher
hs.hotkey.bind(mashshift, '9', function() apps.launcher:show() end)
-- Layouts chooser
hs.hotkey.bind(mashshift, '0', function() layouts.chooser:show() end)
-- Defines for window maximize toggler
frameCache = {}
-- Toggle a window between its normal size, and being maximized
function toggle_window_maximized()
local win = hs.window.focusedWindow()
if frameCache[win:id()] then
win:setFrame(frameCache[win:id()])
frameCache[win:id()] = nil
else
frameCache[win:id()] = win:frame()
win:maximize()
end
end
--[[
CALLBACKS
--]]
-- Callback function for application events
function applicationWatcher(appName, eventType, appObject)
if (eventType == hs.application.watcher.activated) then
if (appName == "Finder") then
-- Bring all Finder windows forward when one gets activated
appObject:selectMenuItem({"Window", "Bring All to Front"})
end
end
end
-- Callback function for WiFi SSID change events
function ssidChangedCallback()
newSSID = hs.wifi.currentNetwork()
print("ssidChangedCallback: old:"..(lastSSID or "nil").." new:"..(newSSID or "nil"))
if newSSID == homeSSID or newSSID == homeSSID5 and lastSSID ~= homeSSID or lastSSID ~= homeSSID5 then
-- We have gone from something that isn't my home WiFi, to something that is
print('at home')
elseif newSSID ~= homeSSID or newSSID ~= homeSSID5 and lastSSID == homeSSID or lastSSID == homeSSID5 then
-- We have gone from something that is my home WiFi, to something that isn't
print('left home')
end
lastSSID = newSSID
end
-- Callback function for USB device events
hs.execute('/Applications/Karabiner.app/Contents/Library/bin/karabiner select 0')
local usb_table = nil
usb_table = hs.usb.attachedDevices()
for index, usb_device in pairs(usb_table) do
local device = usb_device["productName"]
print(device)
if (device == microsoft_keyboard) then
hs.execute('/Applications/Karabiner.app/Contents/Library/bin/karabiner select 1')
end
end
local usbWatcher = nil
function usbDeviceCallback(data)
local device = data["productName"]
if (device == microsoft_keyboard) then
if (data["eventType"] == "added") then
hs.execute('/Applications/Karabiner.app/Contents/Library/bin/karabiner select 1')
elseif (data["eventType"] == "removed") then
hs.execute('/Applications/Karabiner.app/Contents/Library/bin/karabiner select 0')
end
end
end
-- Callback function for changes in screen layout
function screensChangedCallback()
print("screensChangedCallback")
newNumberOfScreens = #hs.screen.allScreens()
local notificationMessage = ''
-- FIXME: This is awful if we swap primary screen to the external display,
-- all the windows swap around, pointlessly.
-- if lastNumberOfScreens ~= newNumberOfScreens then
-- if newNumberOfScreens == 1 then
-- hs.layout.apply(internal_display)
-- notificationMessage='Internal display layout applied'
-- elseif newNumberOfScreens == 2 then
-- hs.layout.apply(dual_display)
-- notificationMessage='Internal display layout applied'
-- end
-- hs.notify.new({
-- title='Screens Changed',
-- informativeText=notificationMessage
-- }):send()
-- end
lastNumberOfScreens = newNumberOfScreens
end
-- Fancy config reload
function reloadConfig(paths)
doReload = false
for _,file in pairs(paths) do
if file:sub(-4) == ".lua" then
print("A lua file changed, doing reload")
doReload = true
end
end
if not doReload then
print("No lua file changed, skipping reload")
return
end
hs.reload()
end
--[[
WATCHERS
--]]
-- Create and start our callbacks
-- start app launch watcher
appWatcher = hs.application.watcher.new(applicationWatcher)
appWatcher:start()
screenWatcher = hs.screen.watcher.new(screensChangedCallback)
screenWatcher:start()
wifiWatcher = hs.wifi.watcher.new(ssidChangedCallback)
wifiWatcher:start()
usbWatcher = hs.usb.watcher.new(usbDeviceCallback)
usbWatcher:start()
configFileWatcher = hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", reloadConfig)
configFileWatcher:start()
-- Finally, show a notification that we finished loading the config successfully
hs.notify.new({
title='Hammerspoon',
informativeText='Config loaded'
}):send()
-- Main grid methods
local layouts = {}
-- Define monitor names for layout purposes
local display_home = "f.lux profile"
local display_laptop = "Color LCD"
local display_monitor = "Thunderbolt Display"
local display_monitor2 = "VG2439 Series"
-- Define window layouts
-- Format reminder:
-- {"App name", "Window name", "Display Name", "unitrect", "framerect", "fullframerect"},
layouts.layouts = {
{
name="Home Laptop",
description='15" MacBook Pro personal laptop screen'
},
{
name="Work Laptop",
description='13" MacBook Air work laptop screen'
},
{
name="Office Setup",
description="Dual monitor setup at the office",
small={
{"Spark", nil, display_laptop, {hs.layout.maximized}, nil, nil},
{"Fantastical 2", nil, display_laptop, {hs.layout.maximized}, nil, nil},
},
large={
{"Google Chrome", nil, display_monitor, {0, 0, 2/3, 1}, nil, nil},
{"Sublime Text", nil, display_monitor, {1/3, 1/3, 1/3, 2/3}, nil, nil}
}
},
}
function layouts.applyLayout(layout)
print('Layout ' .. layout.name .. ' selected')
if lastNumberOfScreens > 1 then
-- Multiple monitors
print('We have multiple monitors')
hs.layout.apply(layout.large, function(windowTitle, layoutWindowTitle)
return string.sub(windowTitle, 1, string.len(layoutWindowTitle)) == layoutWindowTitle
end)
end
-- Multiple monitors
print('We have a single screen')
hs.layout.apply(layout.small, function(windowTitle, layoutWindowTitle)
return string.sub(windowTitle, 1, string.len(layoutWindowTitle)) == layoutWindowTitle
end)
end
layouts.chooser = hs.chooser.new(function(selection)
if not selection then return end
layouts.applyLayout(layouts.layouts[selection.uuid])
end)
-- chooser:choices(choices)
local i = 0
local choices = hs.fnutils.imap(layouts.layouts, function(layout)
i = i + 1
return {
uuid=i,
text=layout.name,
subText=layout.description
}
end)
layouts.chooser:choices(choices)
layouts.chooser:searchSubText(true)
layouts.chooser:rows(#choices)
layouts.chooser:width(20)
return layouts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment