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.

  1. Install Karabiner Elements
  2. 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.

description

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