My external keyboard has a hyper key (altshiftctrlcmd) that I use for global shortcuts like launching or switching between my regularly used apps (like a faster command tab) and running some automations.

I have now configured my caps lock to behave as the hyper key while pressed or esc when tapped.

This post shows you how to configure your laptop to do the same.

Why#

You probably use Shift for capitalization, while caps-lock just sits there taking up valuable real estate on your keyboard.

Esc#

caps lock is easier to reach then esc on the Macbook Pro keyboard!

Hyper#

For instance I have the following shortcuts setup:

  1. No more constant Cmd+tabbing. Instead for switching between constantly used apps I have:
    1. caps locki -> launch/switch to IntelliJ IDEA
    2. caps lockt -> launch/switch to iTerm
    3. caps lockb -> launch/switch to browser (currently Firefox or Vivaldi)
    4. caps lockg -> launch/switch to Spotify (g is closer to the hyper key on my external keyboard)
    5. caps locks -> switch to Slack
    6. caps lock0 -> switch to Obsidian
  2. Automating tasks:
    1. caps lock14 -> update Slack status
    2. caps lock5 -> put computer to sleep

How#

Hammerspoon#

Hammerspoon is a powerful automation utility for macOS with configuration written in Lua. You don’t need to know Lua to understand the instructions in this post.

Install Hammerspoon#

First, install Hammerspoon if you don’t already have it. You can follow the “Setup” instructions outlined in their Getting Started guide.

Once installed, open the configuration file by clicking on the Hammerspoon icon in your menubar and selecting the “Open config” option. The config file is located at ~/.hammerspoon/init.lua by default if you want to open it in another editor.

Remapping#

A common way to accomplish this is via Karabiner-Elements, but I have found that to be buggy 1, or it might not be on your organization’s allowed list of apps. Additionally, I would prefer to not have another app using my compute resources for such a minor utility.

Instead, a better and easier method is to use hidutil – a key remapping tool built into MacOS.

You can call the hidutil command right from the Hammerspoon config by adding the following to the top of your file

-- Remap Caps Lock to escape using hidutil
local remap_output, remap_status = hs.execute [["/usr/bin/hidutil" "property" "--set" "{\"UserKeyMapping\":[{\"HIDKeyboardModifierMappingSrc\":0x700000039,\"HIDKeyboardModifierMappingDst\":0x70000006D}]}"]]

Save your configuration file and reload it in Hammerspoon by clicking on the Hammerspoon icon in your menubar and selecting the “Reload Config” option.

If you tap caps lock now, the LED indicator should no longer light up.

Configuring shortcuts with Hammerspoon#

Now let’s configure Hammerspoon to trigger caps lock as esc or hyper.

Configure hyper mode#

First, let’s setup the “hyper” mode.

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

-- 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
    -- BONUS! caps-lock acts as esc with a short tap
    hs.eventtap.keyStroke({}, 'ESCAPE')
  end
end

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

This gets you setup to enter hyper mode while caps-lock is pressed and exit it when it is released.

Shortcuts#

Next, lets setup some shortcuts to switch between regularly used apps. As an example, let’s start with IntelliJ

hyper:bind({}, 'i', function()
  -- trigger the hyper key
  hs.eventtap.keyStroke({ "alt", "shift", "ctrl", "cmd" }, 'i')
  hyper.triggered = true
end)
-- Use the hyber key combo to configure the actual actions
-- so the external keyboard can take advantage of it as well
hs.hotkey.bind({ "alt", "shift", "ctrl", "cmd" }, "i", function()
  hs.application.launchOrFocus("IntelliJ IDEA Ultimate")
end)

The first line in this code block defines what to do when i is pressed while hyper mode is active. Here we tell Hammerspoon to simulate tapping the hyper key 2 along with i.

The next block of code tells Hammerspoon to launch or focus to IntelliJ when it detects the hyper key combo is pressed with i. I am not doing this in the previous block of code because I use another programmable keyboard with a standalone hyper key – and I want this configuration to work with that keyboard.

You can replace this block with a simpler loop to setup these shortcuts for multiple apps like so

-- Shortcuts to launch or switch focus to specific apps
keyApps = {
	['i'] = 'Intellij IDEA Ultimate',
	['b'] = 'Firefox', -- replace with your favorite browser
	['t'] = 'iTerm',
	['m'] = 'Telegram',
	['s'] = 'Slack',
	['g'] = 'Spotify',
	['o'] = 'Obsidian'
}

for key, app in pairs(keyApps) do
	-- First, bind the `key` when pressed in `hyper` mode to simulate tapping the hyper key
	hyper:bind({}, key, function()
		hs.eventtap.keyStroke({ "alt", "shift", "ctrl", "cmd" }, key)
		hyper.triggered = true
	end)
	-- Then setup the app to launch or focus when configured key is pressed along with hyper key
	-- This is useful if your programmable keyboard has a standalone hyper key
	hs.hotkey.bind({ "alt", "shift", "ctrl", "cmd" }, key, function ()
		hs.application.launchOrFocus(app)
	end)
end

The keyApps table contains mappings for keys to their respective apps. We then loop over these mappings to setup appropriate bindings like before.

Completing the configuration#

At the end your Hammerspoon file should look like this

-- Remap Caps Lock to escape using hidutil
local remap_output, remap_status = hs.execute [["/usr/bin/hidutil" "property" "--set" "{\"UserKeyMapping\":[{\"HIDKeyboardModifierMappingSrc\":0x700000039,\"HIDKeyboardModifierMappingDst\":0x70000006D}]}"]]

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

-- 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
	-- BONUS! caps-lock acts as esc with a short tap
    hs.eventtap.keyStroke({}, 'ESCAPE')
  end
end

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


-- Shortcuts to launch or switch focus to specific apps
keyApps = {
	['i'] = 'Intellij IDEA Ultimate',
	['b'] = 'Firefox', -- replace with your favorite browser
	['t'] = 'iTerm',
	['m'] = 'Telegram',
	['s'] = 'Slack',
	['g'] = 'Spotify',
	['o'] = 'Obsidian'
}

for key, app in pairs(keyApps) do
	-- First, bind the `key` when pressed in `hyper` mode to simulate tapping the hyper key
	hyper:bind({}, key, function()
		hs.eventtap.keyStroke({ "alt", "shift", "ctrl", "cmd" }, key)
		hyper.triggered = true
	end)
	-- Then setup the app to launch or focus when configured key is pressed along with hyper key
	-- This is useful if your programmable keyboard has a standalone hyper key
	hs.hotkey.bind({ "alt", "shift", "ctrl", "cmd" }, key, function ()
		hs.application.launchOrFocus(app)
	end)
end

Save this file and then reload your configuration from Hammerspoon’s menu bar icon. You should be able to launch or switch apps when you hit your configured keys while holding down caps lock. Just tapping caps lock will trigger esc.

I haven’t covered the use case of automating tasks here – I plan to do that in a future post.


  1. After waking the computer from sleep, my keyboard is completely unresponsive until I restart Karabiner-Elements – regardless of how I setup my computer. ↩︎

  2. Mentioned earlier in the post the hyper key is a combination of “alt”, “shift”, “ctrl”, “cmd”. This can be used for global shortcuts because no other application is likely to shortcuts that use all the modifier keys. ↩︎