Skip to content
Logo Theodo

Making the Runtime, Funtime with Hammerspoon

Braden Marshall5 min read

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:

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 Hammerspoon.app 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:setStrokeWidth(2) rectanglePreview:setStrokeColor({ hex=rectanglePreviewColor, alpha=1 }) rectanglePreview:setFillColor({ hex=rectanglePreviewColor, alpha=0.5 }) rectanglePreview:setRoundedRectRadii(2, 2) rectanglePreview:setStroke(true):setFill(true) rectanglePreview:setLevel(floating)

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) hs.osascript.applescript(createItermWithBounds) end

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

local fromPoint = nil

local drag_event = hs.eventtap.new( { hs.eventtap.event.types.mouseMoved }, function(e) toPoint = hs.mouse.getAbsolutePosition() local newFrame = hs.geometry.new({ [x1] = fromPoint.x, [y1] = fromPoint.y, [x2] = toPoint.x, [y2] = toPoint.y, }) rectanglePreview:setFrame(newFrame)

<span style="color: #859900;">return</span> <span style="color: #b58900;">nil</span>

end )

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.new( { hs.eventtap.event.types.flagsChanged }, function(e) 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) rectanglePreview:setFrame(newFrame) drag_event:start() rectanglePreview:show() elseif fromPoint ~= nil then fromPoint = nil drag_event:stop() rectanglePreview:hide() openIterm() end return nil end ) flags_event:start()

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 🔨).

Liked this article?