Set up a Hyper Key with Hammerspoon on macOS

'I will just move my active window to the right, using ⌥⌘→. Wait, why did I move to the next tab in my editor?'

This has happened to me on multiple occasions. Every application has their own shortcuts, making it very likely that some of them overlap. For some tasks this can be acceptable, but there is definitely some functionality that needs to work in all contexts, like resizing windows, opening certain applications, or locking your screen.

In order to make sure that we can always access this functionality we need to bind it to global shortcuts, of which we know that they are not used by other applications. One way to do this is using all modifier keys for these shortcuts, since most applications don't use all modifier keys at once. However, the reason that most applications don't do this is because it is highly impractical to press ⌥⌃⌘⇧ at the same time. To overcome these issues, we will be setting up a so-called Hyper key, a special key reserved only for global shortcuts.

The Hyper Key

A Hyper key is a special key that is reserved only for these global shortcuts, to keep it completely separate from our regular environment and applications. By setting up such a key, we make sure that we can always count on our global shortcuts to work, no matter what application we're using.

But how do we do this? All keys on the keyboard have their use, and we can't really add any new ones. The best solution to this is using a key that has relatively little use in day to day life, and rebind this as a Hyper key. In this guide we will be rebinding the CAPS LOCK key, since this is one of the least used and most redundant keys on the keyboard.

Setting up a Hyper Key using Hammerspoon

Let me start by saying that there are multiple ways to set up a Hyper key, and our Hammerspoon approach is not the most simple one. However, Hammerspoon gives you a lot of power in terms of automation and customisation tools. This is why I recommend the Hammerspoon approach if you have programming or technical experience. If you don't, a simpler approach using only Karabiner Elements might be better.

In our approach we will also be using Karabiner Elements for the initial key remapping, but we will be extending this functionality using Hammerspoon.

0. Prerequisites

First download Karabiner Elements and Hammerspoon from their pages or by using Homebrew:

brew cask install hammerspoon
brew cask install karabiner-elements

1. Remapping CAPS LOCK with Karabiner Elements

Open up Karabiner Elements and add the following rule to Simple Modifications:

Alternatively, you can add the following to the "simple modifications" section in your ~/.config/karabiner/karabiner/karabiner.json file, which can help if you want to add the configuration to your dotfiles.

{
    "from": {
        "key_code": "caps_lock"
    },
    "to": {
        "key_code": "f18"
    }
}

This maps the CAPS LOCK to a virtual F18 key. We map it to this virtual key so that this will never get in the way of other keys, while we can still use it in our Hammerspoon configuration.

2. Enabling Hyper Mode using Hammerspoon

Now that CAPS LOCK is bound to this F18 key, we can use the F18 key to set up a Hyper key in Hammerspoon:

-- A global variable for the Hyper Mode
hyper = hs.hotkey.modal.new({}, 'F17')

-- Enter Hyper Mode when F18 (Hyper/Capslock) is pressed
function enterHyperMode()
  hyper.triggered = false
  hyper:enter()
end

-- Leave Hyper Mode when F18 (Hyper/Capslock) is pressed,
-- send ESCAPE if no other keys are pressed.
function exitHyperMode()
  hyper:exit()
  if not hyper.triggered then
    hs.eventtap.keyStroke({}, 'ESCAPE')
  end
end

-- Bind the Hyper key
f18 = hs.hotkey.bind({}, 'F18', enterHyperMode, exitHyperMode)

A few things are happening in the snippet above. First, a new modal is created for Hyper Mode, during which different shortcuts can have different meanings than they do outside of this mode. Next, we define what happens when this Hyper Mode is entered and exited. In our case, we use the hyper.triggered variable to keep track of whether any shortcuts were triggered during Hyper Mode. If there weren't, we use Hammerspoon to trigger the ESCAPE key. Finally, we bind the virtual F18 key to entering and exiting Hyper Mode when it is pressed or released.

By adding this snippet to the top of your ~/.hammerspoon/init.lua, you can use Hyper Mode anywhere in the rest of your Hammerspoon configuration. Which brings us to the next step.

3. Binding shortcuts in Hyper Mode

3.1 Existing shortcuts

Now that we have bound CAPS LOCK to Hyper Mode, we can use it to unleash the power of Hammerspoon to our day to day life. The easiest ways to do so is by starting to bind existing global shortcuts to it.

For instance, I use Unclutter, for which I set up ⌥⌃⌘⇧+U as the shortcut. Using Hammerspoon we just need to add the following snippet to our Hammerspoon configuration:

hyper:bind({}, 'u', function()
  hs.eventtap.keyStroke({'cmd','alt','shift','ctrl'}, 'u')
  hyper.triggered = true
end)

Which tells Hammerspoon to send out the corresponding keystroke when pressing Hyper + U.

3.2 Applescript

Many macOS applications export part of their functionality with Applescript. With Hammerspoon it is incredibly easy to bind Hyper Mode shortcuts to Applescript files (a feature I contributed to 🎉). Executing an Applescript file is as easy as adding the following to your configuration:

hyper:bind({}, "e", function()
  hs.osascript.applescriptFromFile("togglevpn.applescript")
  hyper.triggered = true
end)

This binds Hyper + E to opening the following Applescript file, which toggles my VPN connection, by using Viscosity's Applescript features.

tell application "Viscosity"
    if the state of the first connection is "Connected" then
        disconnect the first connection
    else
        connect the first connection
    end if
end tell

3.3 Built in Hammerspoon functionality

Besides sending keystrokes or executing applescript code, Hammerspoon has a lot of built in functionality that can help with controlling various aspects of your Mac. For example, hs.caffeinate can be used control the state of your machine, so it allows you to turn your machine off, lock it or log out of the current user session. I have this snippet in my configuration to quickly lock my MacBook with Hyper + L:

hyper:bind({}, "l", function()
  hs.caffeinate.lockScreen()
  hyper.triggered = true
end)

And a snippet such as the following can be used to toggle fullscreen for the frontmost application with Hyper + ↩:

hyper:bind({}, "return", function()
  local win = hs.window.frontmostWindow()
  win:setFullscreen(not win:isFullscreen())
  hyper.triggered = true
end)

Conclusion

We can set up a Hyper key by going through several steps using Karabiner Elements and Hammerspoon. Functionality can be added to this Hyper key in multiple ways; by binding existing shortcuts to it, by executing Applescript code, and by using built in Hammerspoon functions.

Examples are included for each of tese methods to get you started on your own configuration. If you would like to go beyond the examples that I've shown here, you can check out my full configuration, take a look at some of the configurations of others, or dive into the Hammerspoon documentation.

References


I hope you enjoyed this article. If you did, please consider sharing this on Facebook, Twitter or LinkedIn. If this got you started on your own setup, or if you just want to share, show me your own Hammerspoon configurations in the comments below!