Anykey: my first native macOS app
Recently, my system automation journey took me on an unexpected excursion as I found myself building a native macOS app. Now, mere two weeks later, it’s finished and I’ve integrated it into my main setup. It’s called Anykey, and it’s a tool for binding arbitrary shell commands to system-wide or app-specific hotkeys. It’s configured via a text file that can be stored in Git or synced to Dropbox. It’s free (as in GPLv3) and open-source, so anyone curious can download it from GitHub.
It all started when I finally conceded that some of my most used automation scripts could benefit from dedicated keyboard shortcuts, as I got tired of invoking them by name every single time. To be fair, I’d already set up a few hotkeys in my Alfred workflow, but, aside from having to hook each one up using an awkward graph GUI, those can be pretty cumbersome to maintain across multiple machines. For security reasons, Alfred doesn’t export concrete hotkeys bindings for its workflows.
I believed there must be “a better way”, so I began to investigate what others are doing. A popular suggestion is to use Automator to create a simple system service. The service can then be assigned a hotkey in System Preferences. This works well if you only have a few automations and don’t mind repeating the process on every machine that you work on. I briefly considered writing a script to build macOS services programmatically, but that still wouldn’t solve the need to manually edit the settings. Overall, hardly an improvement over Alfred.
The conversation threads unveiled by my web searches kept mentioning tools like Keyboard Maestro, but neither it nor its kin supported text configs. So I turned to Hammerspoon, which advertised the ability to assign shortcuts to automations. Ultimately, I wasn’t too thrilled about the Lua config and I was skeptical that it’d be able to create app-scoped hotkeys (this hunch later proved to be correct), given how the API is structured. So it was about at that time when I thought to myself: “hell, I know some Swift, maybe I could build this thing in a couple of days”.
Unsurprisingly, I soon became the living illustration of the tortured JFK’s saying: “we chose to do this not because it is easy, but because we thought it was going to be easy”. In my mind, this whole process looked something like: generate a “status bar app” in Xcode (nope, not an actual starter template, but Gabriel Theodoropoulos’ epic writeup helps), then crank up the views in the much-vaunted SwiftUI (turns out, there’s still a ton of stuff that exists exclusively in AppKit/Cocoa), then hook up this promising Hotkey lib (oops, creates exclusive shortcuts that cannot be scoped to a particular app) to a JSON config (Elm has made me forget how cumbersome and error-prone dealing with JSON is in statically typed languages), PROFIT?
So, after the first few nights of frantically putting this thing together, my enthusiasm noticeably waned. So much that I considered giving up on the idea. When the Hotkey lib couldn’t do what I needed, I felt at an impasse. At the lowest point, I found myself studying an obscure PDF describing Apple’s deprecated Carbon APIs, then forking Hotkey to try to apply what I learned to work around some of the issues I was seeing. No luck: any keyboard shortcut declared by Hotkey immediately stopped working in all other apps. Having virtually no experience developing for the Apple platform, I felt out of my depth. Finally a forum comment somewhere pointed me towards the Quartz Events Services, which, at a first glance, seemed like another obscure macOS API. But searching GitHub for usage examples (a criminally under-appreciated way of studying APIs), I found a bunch of apps successfully using it for similar use cases (whole-OS Vim emulators, keyboard cleaners, etc.). In hindsight, this was the key to the puzzle, and once I got it to work, everything just kind of snapped together.
Integrating Anykey into my automation setup went even smoother than I expected: I updated Process to expose an external trigger for my script launcher workflow. Anykey then invokes a simple AppleScript that “pulls” that trigger with a specified argument. The whole changeset is less than one hundred lines of code and I can now easily assign a hotkey to any AppleScript in my collection and my status bar is adorned by this (cliché, I know) looped square that I chose for the Anykey logo.
From the feedback to my low-key Twitter announcement of Anykey, I learned that Karabiner-Elements, which I’d only known as a “fancy keyboard remapping tool“ also has the ability to run shell commands, and can even scope assignments to individual apps. Had I known this, I would’ve likely deemed it “good enough” and went with it. So I’m thankful for my ignorance in this instance. Karabiner feels a lot more heavyweight and it doesn’t auto-reload its configuration when it changes on the disk, so I’m going to stick with Anykey. I also have another unique feature in mind that, if it works out, would put Anykey miles ahead of Karabiner for this particular use case. Stay tuned!