Hammerspooning OSX

Being a big fan of using the keyboard for everything and not touching the mouse more than necessary, I went through different OSX helper applications for helping to move windows around, aligning them on screen, etc. Lately I've been using Magnet (formerly known as Window Magnet, App Store, $4.99), which is pretty nice but I felt it is not using the concept of moving stuff around with the keyboard to the full extent: While you can surely put windows in one quadrant of the screen or something, there's no option to e.g. nudge a window a bit to the right.

Hammerspoon logo

Enter Hammerspoon: First and foremost, Hammerspoon provides you with scriptable access to OSX's accessibility API -- and whatever you do with that, you can bind to a hotkey. Not only can you access that single API, but also check for USB devices, Wifi names, attached screens and so on - all controlable with small Lua scripts. Just some examples of what you can do with Hammerspoon:

  • switch sound on or off, depending on the available wifi networks
  • unmount USB devices upon switching to battery power
  • watch the name and number of screens connected and position windows accordingly (e.g. if Thunderbolt Display connected, put Xcode on that one and Mail on the internal one)
  • move windows on a grid on the screen
  • move windows in general
  • ...

Phrased differently:

Unlimited power for OSX Automation!

Configuring Hammerspoon

The following code snippets have been heavily inspired (read: copied) by philipalexander's, tstirrat's and cmsj's Hammerspoon configs on Github and also by Tristan Hume's post on configuring Mjolnir, of which Hammerspoon is a fork.

The configuration (it basically comes with no defaults) can be a bit daunting in the beginning - in fact, I had stumbled upon Hammerspoon already some time ago but didn't invest the effort back then.

However, if you have about 30min at hand, you can already hammer out a nice configuration for the things that are most important for you to automate or hotkey. For me, these are arranging windows grid-like on screen and have e.g. the screen locked with a keypress. So let's have a look at that:

First, Hammerspoon's config resides in the file ~/.hammerspoon/init.lua. I use a set of modifier keys for all of the hotkeys, let's call that set hyper. I also define that I want a window grid size of 2x2, with no margins:

local hyper = {"⌘", "⌥", "⌃", "⇧"}

-- definitions
hs.grid.MARGINX = 0
hs.grid.MARGINY = 0
hs.grid.GRIDWIDTH = 2
hs.grid.GRIDHEIGHT = 2

This combination of keys might also be mapped to a single key by virtue of Karabiner, a cool program allowing arbitrary key remappings and much much. How to achieve that thou is left as an exercise for the interested reader ;-)

Easy bits first -- application launching

I want to automate a few things via hotkeys. Stuff I regularly want to do is get an iTerm open or locking the screen. These two actions can be bound to keys via:

-- locking
hs.hotkey.bind(hyper, 'x', function()
    os.execute("/System/Library/CoreServices/Menu\\ Extras/User.menu/Contents/Resources/CGSession -suspend")
end)

-- Applications
hs.hotkey.bind(hyper, 'i', function()
    os.execute("open /Applications/iTerm.app")
end)

Configuring Window Movement Hotkeys

-- a helper function that returns another function that resizes the current window
-- to a certain grid size.
local gridset = function(x, y, w, h)
    return function()
        cur_window = hs.window.focusedWindow()
        hs.grid.set(
            cur_window,
            {x=x, y=y, w=w, h=h},
            cur_window:screen()
        )
    end
end

-- function to move window one screen left or back if it's already on the
-- leftmost
local toNextScreen = function()
    return function()
        currentWindow = hs.window.focusedWindow()
        s = hs.screen{x=1,y=0}
        
        if s == currentWindow:screen() then
            s = hs.screen{x=0,y=0}
            currentWindow:moveToScreen(s)
        else
            currentWindow:moveToScreen(s)
        end
    end
end

-- movement keys
hs.hotkey.bind(hyper, 'j', toNextScreen())
hs.hotkey.bind(hyper, 'h', gridset(0, 0, 1, 2)) -- left half
hs.hotkey.bind(hyper, 'k', hs.grid.maximizeWindow)
hs.hotkey.bind(hyper, 'l', gridset(1, 0, 1, 2)) -- right half

Auto-reloading of config

Hammerspoon also supports reloading its configuration files upon detecting changes. Just add the following at the very end of init.lua:

-- watch config for changes and reload when they occur
function reloadConfig(files)
    doReload = false
    for _,file in pairs(files) do
        if file:sub(-4) == ".lua" then
            doReload = true
        end
    end
    if doReload then
        hs.reload()
    end
end

hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", reloadConfig):start()
hs.alert.show("Config reloaded")

Hammerspoon is free & open source software (MIT licensed) - the source code is on Github: https://github.com/Hammerspoon/hammerspoon

My full Hammerspoon config is also available on Github, have a look at https://github.com/skalarproduktraum/hammerspoon-config/