Making the Runtime, Funtime with Hammerspoon

What is Hammerspoon and what can it do for me?

How often have you wanted a little something extra out of macOS, or it’s desktop environment, but felt intimidated digging into the unwieldy system APIs? Well, fret no more!

Today we will build the neat little utility illustrated in the gif above and, hopefully, inspire you to build something yourself. To do this, we will be using Hammerspoon, an open-source project, which aims to bring staggeringly powerful macOS desktop automation into the Lua scripting language.

This allows you to quickly and easily write Lua code which interacts with the otherwise complicated macOS APIs, such as those for applications, windows, mouse pointers, filesystem objects, audio devices, batteries, screens, low-level keyboard/mouse events, clipboards, location services, wifi, and more.

Having been around for a few years, it is encouraging to know that there is a vibrant community developing Hammerspoon — with features and fixes being merged nearly every day! There is also a handy collection of user submitted snippets, known as “spoons”, which you can easily begin adding to your own configuration. You’ll soon find yourself building up a personalised arsenal of productivity tools, there are few I’ve found particularly helpful:

  • Seal: pluggable launch bar – a viable alternative to Alfred;
  • Caffeine: temporarily prevent the screen from going to sleep;
  • HeadphoneAutoPause: play/pause music players when headphones are connected/disconnected. The reason as to why this isn’t the default behaviour is beyond me…

Getting started with Hammerspoon

If you use brew cask, you can install Hammerspoon in seconds by running the command: brew cask install hammerspoon. If you don’t use brew cask (you really should), you can download the latest release from GitHub then drag the application over to your /Applications/ folder. Afterwards, launch and enable accessability.

Hopefully, by now you’re convinced about how powerful Hammerspoon can be. So, let’s give you a taste of how it works and dive into a code example. Having been inspired from a post I saw on /r/unixporn, we shall be creating a quick spoon which allows the user to draw a rectangle on top of the screen only to transform into a terminal window.

Create a rectangle which overlays on top of the screen, to indicate the size of the incoming terminal window:

local rectanglePreviewColor = '#81ecec'
local rectanglePreview = hs.drawing.rectangle(
  hs.geometry.rect(0, 0, 0, 0)
rectanglePreview:setStrokeColor({ hex=rectanglePreviewColor, alpha=1 })
rectanglePreview:setFillColor({ hex=rectanglePreviewColor, alpha=0.5 })
rectanglePreview:setRoundedRectRadii(2, 2)

One of the really cool things about Hammerspoon is its ability to work alongside Open Scripting Architecture (OSA) languages, such as AppleScript. We’ll be using this to create our new terminal window, with the desired position and size:

local function openIterm()
  local frame = rectanglePreview:frame()
  local createItermWithBounds = string.format([[
    if application "iTerm" is not running then
      activate application "iTerm"
    end if
    tell application "iTerm"
      set newWindow to (create window with default profile)
      set the bounds of newWindow to {%i, %i, %i, %i}
    end tell
  ]], frame.x, frame.y, frame.x + frame.w, frame.y + frame.h)

Listen for when the user moves their mouse, so we can move and resize our rectanglePreview:

local fromPoint = nil

local drag_event =
  { hs.eventtap.event.types.mouseMoved },
    toPoint = hs.mouse.getAbsolutePosition()
    local newFrame ={
      ["x1"] = fromPoint.x,
      ["y1"] = fromPoint.y,
      ["x2"] = toPoint.x,
      ["y2"] = toPoint.y,

    return nil

Begin to capture the rectangle drawn by the user, as they hold ctrl + shift. Once released, cease capture, hide the rectangle and then open up our iTerm instance:

  local flags_event
  { hs.eventtap.event.types.flagsChanged },
    local flags = e:getFlags()
    if flags.ctrl and flags.shift then
      fromPoint = hs.mouse.getAbsolutePosition()
      local newFrame = hs.geometry.rect(fromPoint.x, fromPoint.y, 0, 0)
    elseif fromPoint ~= nil then
      fromPoint = nil
    return nil

And that’s all it takes!

Stepping into the future

Feel free to check out my Hammerspoon config on GitHub, where you can find the coalesced version of the example above, along with my (upcoming) other spoons.

If you fancy giving a shot at writing your own spoons, here are a couple ideas to help get your creativity flowing:

Fun fact: the name Hammerspoon is derived from itself being a “fork” of its lightweight predecessor Mjölnir (that being the name of Thor’s hammer 🔨).

You liked this article? You'd probably be a good match for our ever-growing tech team at Theodo.

Join Us