Custom Window Management using Hammerspoon

September 17, 2020

One of my favorite programming quotes is:

“Every program attempts to expand until it can read mail. Those programs which cannot so expand are replaced by ones which can.” - jwz

My corollary:

“Every productivity improvement I make attempts to expand until I write a window management script” - me

COVID accelerated this; In the Physical Days pre-March 2020, I used a laptop in the office - no external displays of any kind. But now most of my day is now spent in front of two monitors.

Just Use Some Existing App?

I wanted:

  1. Keyboard driven. Small number of keystrokes to a desired layout outcome.

  2. Real-time layout flexibility of apps. No pre-defined app layouts as I tend to mix and match quite often.

  3. Low cognitive load to shift a window across physical displays - ideally the same set of hotkeys can spatially move windows across displays.

There’s a huge variety of Mac window management tools out there already 1, and Spectacle was probably the closest off the shelf, but:

  • It is no longer maintained
  • I didn’t want symmetry between the up/down keys and right/left keys, as I use vertical space and horizontal space very differently
  • It additionally has a separate hotkey for moving windows between displays - I wanted a single set that could move across displays naturally.

So I built my own.

Slots and the SlotChain on Hammerspoon

My new system is built on Hammerspoon. Hammerspoon has a straightforward set of window control functions you can access through Lua. It was easy to get right to prototyping.

Two key concepts I landed on:

  • A slot is a rectangular region on a particular physical display. Any window from any app can “fit” into a slot.
  • A slot chain is a linked set of slots. Each slot has a next and previous neighbor slot.

My approach was then to define slot chains, and then bind two hotkeys (a “next slot” and “previous” slot hotkey to move the focused window through the slot chain.

Sample Code in my init.lua:

--- Here is the actual slot and connection definitions
--- This goes in your init.lua

local leftRightArrowSlotChain = {}
--- makeSlot takes (xPosition, yPosition, width of window, height of window, screen)
--- A nice set of "slots" for a landscape monitor that is wide. 
--- Slots: Left third, Left half, Left 2/3, Right half, Right third
leftRightArrowSlotChain[0] = k2win.makeSlot(0.0, 0.0, 0.333, 1.0, 1)
leftRightArrowSlotChain[1] = k2win.makeSlot(0.0, 0.0, 0.5, 1.0, 1)
leftRightArrowSlotChain[2] = k2win.makeSlot(0.0, 0.0, 0.666, 1.0, 1)
leftRightArrowSlotChain[3] = k2win.makeSlot(0.5, 0.0, 0.5, 1.0, 1)
leftRightArrowSlotChain[4] = k2win.makeSlot(0.6666, 0.0, 0.33333, 1.0, 1)6

--- Connect the slots in order so a "next" arrow key goes from slot 0 -> slot 1 -> etc.
k2win.setSlotConnection(leftRightArrowSlotChain, 0, 1)
k2win.setSlotConnection(leftRightArrowSlotChain, 1, 2)
k2win.setSlotConnection(leftRightArrowSlotChain, 2, 3)
k2win.setSlotConnection(leftRightArrowSlotChain, 3, 4)

--- Bind the k2window management functions to the hotkeys you want
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "Left", function() k2win.moveFocusedWindowToNextSlot(leftRightArrowSlotChain, 0) end)
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "Right", function() k2win.moveFocusedWindowToNextSlot(leftRightArrowSlotChain, 1) end)

 

The code figures out which slot your window is in (or closest to), finds the next or previous slot in the slot chain and then moves and resizes the window to fit the target slot.

As I hit Command-Control-Option Right/Left, the window moves through the slots:

Window Management Example

Thus far, in practice, I use 2 slot chains, with the right/left arrows controlling horizontal proportions, and the up/down arrows controlling vertical proportions but also bridging across my 2 displays which are arranged vertically.


  1. Divvy, Moom, Spectacle, and a bunch of in-built Hammerspoon packages are seem pretty good but didn’t quite do what I was looking for. ↩︎