Created
October 1, 2018 14:57
-
-
Save 23maverick23/b7dce3d1baff2cf2b4dad7a0a8a750b9 to your computer and use it in GitHub Desktop.
OSX: Hammerspoon scripts
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
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 |
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
-- 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 |
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
--[[ | |
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() |
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
-- 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