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:
- 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 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:
- Move window focus directionally using the VIM movement keys (HJKL).
- When Spotify begins to play a new song, display an alert with the new song title, artist, etc…
- Inter-process communication and a simple HTTPServer enable you to trigger Hammerspoon functionality from pretty much any environment.
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 🔨).