Manav B. Ponnekanti

Over-customising with Hammerspoon

Hammerspoon is a free, open-source program that gives you deep access to macOS APIs via Lua scripting. In the Before Times, the barrier to entry would be your ability/willingness to write Lua, but now we have Claude Code as a non-deterministic compiler that turns English into functioning code (h/t yomismoaqui) so a quick read of the docs is sufficient to do whatever you'd like, even if you are only semi-technical like me.

It was incredibly satisfying to replace a few 3rd party dependencies that I kept around for a tiny subset of their features, as well as to go crazy and create some bespoke solutions to very specific problems. You can check out the config file and/or read the longer explanation below.

Hyper key

This is the foundation for all of the rest of the shortcuts – it is a key that you remap to a key or modifier combination unused by the system, usually Ctrl-Cmd-Opt-Shift. This ensures that your new shortcuts will never collide with combinations used by the system or other apps, allowing you to create keybindings for various things with abandon.

In the past I used Karabiner-Elements for this. It uses a driver extension to intercept keystrokes at a very low level and passes them through a virtual keyboard, which is where your remapped keys lie. It works, but it's prone to breakage with macOS updates and is generally a bit janky – it doesn't play nice with the British ISO keyboard layout, does not uninstall cleanly, and has the occasional memory leak. It's also by far the most under-utilised program on my computer. I'd prefer something more stable with a smaller scope, given that I'm just remapping a couple of keys.

Now, I use macOS' native command-line remapping tool, hidutil, which has virtually zero footprint and is very reliable, but unfortunately quite obscure – to persist your remapping across reboots, you need to create a launchd plist. Someone saintly has made a webtool to easily create remapping scripts, which obviates this issue. As this tool only allows one-to-one key swaps, I remap both Caps Lock and Right Command to F19 as I use neither for their regular function but use Hyper a lot, so it's nice to have opposable chords for shortcuts instead of having to contort my hand. This is one advantage of Hammerspoon: F19 is not a true motifier key, but hs.hotkey.modal allows you to pass any key as a modifier for commands within Hammerspoon.

Another advantage of having Hyper be a discrete key instead of a combo is that it unlocks an extra layer of shortcuts when used in conjunction with a true modifier key. As most of my Hyper-[letter] shortcuts are occupied with app toggling and window management, I use Shift-Hyper-[letter] for others.

App toggling

I find it irritating to wrangle with Command-Tab or the trackpad to switch between programs when I'm in flow. You either have to move away from the keyboard or tab your way through a long list of apps on the screen using visual search.

To solve this, I configured Hyper-[letter] hotkeys (e.g. Hyper-B launches my browser) to:

This means I don't really need to do 'window management'. I can just call up and dismiss apps as and when I need them with near-zero switching cost.

Hammerspoon can launch and toggle apps by app name, path, or bundle ID. I took the bundle ID route, as it seemed the most robust. You can find bundle IDs by running osascript -e 'id of app "AppName"'.

There are ready-baked solutions for this, e.g. you can set up direct shortcuts in Alfred or Raycast, but my home-baked version has two extras:

Window management

There are tons of apps that handle window management, but my needs are very minimal: I only ever use windows maximised, in a 50/50 split, or a two-thirds/one-third split. Hammerspoon handles all of these very well, and rolling my own window manager let me set up some additional luxuries:

This integrates nicely with the app toggling shortcuts, as it means I can launch an app and tile it with the same modifier held down.

Toggling Bluetooth devices

My Sony headphones have unreliable multipoint, so I turn it off and manually connect to them from my phone and laptop. For some reason, the 3 seconds it takes me to interact with the bluetooth menu bar icon were incredibly grating, so I configured Cmd-Hyper-x to instantly pair/unpair the headphones by hooking into the blueutil CLI.

Local bindings

As F19 is not a true modifier key, you generally can't use it to toggle universal bindings within apps. To solve this, I get Hammerspoon to pass F19 as Cmd-Opt-Ctrl-Shift for some shortcuts, e.g.

hyper:bind({ "shift" }, "p", function()
        hs.eventtap.keyStroke({ "cmd", "alt", "shift", "ctrl" }, "p")
    end)

This routes Shift-Hyper-P to Cmd-Opt-Ctrl-Shift-P, which is my 1Password Quick Access shortcut. I learned this trick from Evan Travers' blog post on Hammerspoon, of which mine is deeply derivative. He explains a lot of this stuff much better than me, so I recommend giving it a read.