For years, I wanted a way to launch my most-used apps with single-key shortcuts. While I knew tools like Hammerspoon could make this possible, I never found a key combination that felt right. I’d always end up disabling the setup, missing the functionality of keys like right control, caps lock, or escape.
Everything changed when I discovered Karabiner Elements’ ability to create distinct mappings for different key events, such as short presses and long presses. The setup I’m about to share lets you launch any mapped app by pressing caps lock plus a specific letter. The best part? You don’t lose the caps lock functionality—it still works as normal when pressed on its own.
Prerequisites
This setup uses two tools: Karabiner Elements for setting up a hyper key binding and Hammerspoon for setting up the key mapping for specific apps. There’s a multitude of solutions to this problem, but I’ve found this one to work for me. It’s fairly simple and easy to set up in minutes.
- Install Karabiner Elements
- Install Hammerspoon
Setting up Karabiner Elements
Navigate to the Complex Modifications tab and install this rule. This rule maps the caps lock key to hyper or escape when pressed. You can modify it to your liking.
Setting up Hammerspoon
Paste the below into your Hammerspoon config and reload it.
hs.loadSpoon("AppLauncher")
local hyper = {"control", "option", "cmd", "shift"}
spoon.AppLauncher.modifiers = hyper
spoon.AppLauncher:bindHotkeys(
{
c = "Terminal",
x = "Xcode",
f = "Firefox",
s = "Slack",
m = "Music",
v = "Visual Studio Code",
t = "Tower",
p = "Proxyman",
o = "Obsidian",
// ...
}
)
This config uses the AppLauncher spoon to set up hotkeys for specific apps. You can adjust them to your liking.
Notes
Single Tool for the Job
I know it is technically possible to achieve the same result using Hammerspoon alone, but it didn’t seem like the right tool for the job to me. If that’s not an issue for you, you might want to use this as your starting point:
local hyper = {"ctrl", "alt", "cmd", "shift"}
local sendEscape = false
local lastMods = {}
-- Function to check if only caps lock was pressed
local function onlyCapsWasPressed()
for k, v in pairs(lastMods) do
if v then return false end
end
return true
end
-- Create and start the eventtap
eventtap = hs.eventtap.new({hs.eventtap.event.types.flagsChanged, hs.eventtap.event.types.keyDown, hs.eventtap.event.types.keyUp}, function(event)
local newMods = event:getFlags()
if event:getKeyCode() == 57 then -- Caps Lock key
-- Check if fn is pressed
if newMods.fn then
-- Allow normal caps lock behavior when fn is pressed
return false
end
-- On key down
if not lastMods.capslock and newMods.capslock then
sendEscape = onlyCapsWasPressed()
-- Set Hyper key
hs.eventtap.keyStroke(hyper, "")
return true
-- On key up
elseif lastMods.capslock and not newMods.capslock then
if sendEscape then
hs.eventtap.keyStroke({}, "escape")
end
return true
end
end
lastMods = newMods
return false
end)
eventtap:start()
Running out of Keys
Ideally, I’d want this setup to support two-letter combinations, but I couldn’t find a straightforward solution. One idea would be to completely ditch the AppLauncher and use something like the following:
local hyper = {"cmd", "alt", "ctrl", "shift"}
local appShortcuts = {
te = "Terminal",
to = "Tower"
}
local lastKeyPress = nil
local lastKeyPressTime = 0
hs.hotkey.bind(hyper, "t", function()
local now = hs.timer.secondsSinceEpoch()
if lastKeyPress and (now - lastKeyPressTime) < 0.5 then
local combo = "t" .. lastKeyPress
if appShortcuts[combo] then
hs.application.launchOrFocus(appShortcuts[combo])
end
lastKeyPress = nil
else
lastKeyPress = "t"
end
lastKeyPressTime = now
end)
for k, _ in pairs(appShortcuts) do
local secondKey = string.sub(k, 2, 2)
hs.hotkey.bind(hyper, secondKey, function()
local now = hs.timer.secondsSinceEpoch()
if lastKeyPress and (now - lastKeyPressTime) < 0.5 then
local combo = lastKeyPress .. secondKey
if appShortcuts[combo] then
hs.application.launchOrFocus(appShortcuts[combo])
end
end
lastKeyPress = nil
end)
end