Hold caps (as esc) for vim arrows with Hammerspoon

▪️

One of the coolest aspects of building a custom keyboard is the level of customizability—not just in hardware, but also in software. My custom keyboard features QMK/VIA support, which allows me to configure any key to perform exactly as I want.

One of my favorite customizations is repurposing the Caps Lock key to function as Escape. The Caps Lock key is conveniently located, but I rarely use it, so I switched it to Escape. However, that still didn’t feel quite useful enough. I also configured my Caps Escape key to toggle a layer when held, mapping “hjkl” to the Vim arrow keys. It was a game changer.

I quickly developed muscle memory for this setup, and whenever I used my MacBook away from my desk, I found myself missing it! Recently, I discovered that I could achieve a similar functionality on my MacBook keyboard using Hammerspoon.

First, install Hammerspoon. Then, add the following to ~/.hammerspoon/CapsEscArrows.lua:

local function pressFn(mods, key)
	if key == nil then
		key = mods
		mods = {}
	end

	return function() hs.eventtap.keyStroke(mods, key, 1000) end
end

-- Create event tap to monitor modifier key changes
local escHeld = false
local keyMap = {
	h = 'left',
	j = 'down',
	k = 'up',
	l = 'right',
	d = 'pagedown',
	u = 'pageup'
}

-- Create event tap for key down events
local keyDownTap = hs.eventtap.new({ hs.eventtap.event.types.keyDown }, function(event)
	if not escHeld then return end
	
	local keyChar = hs.keycodes.map[event:getKeyCode()]
	local flags = event:getFlags()
	
	if keyMap[keyChar] then
		local mods = {}
		if flags.cmd then table.insert(mods, 'cmd') end
		if flags.alt then table.insert(mods, 'alt') end
		if flags.shift then table.insert(mods, 'shift') end
		
		pressFn(mods, keyMap[keyChar])()
		return true -- Prevent original key event
	end
end)

-- Create flags changed event tap
local flagsTap = hs.eventtap.new({ hs.eventtap.event.types.flagsChanged }, function(event)
	local flags = event:getFlags()
	
	-- Check if escape key is pressed or released
	if flags.fn then
		if not escHeld then
			escHeld = true
			keyDownTap:start()
		end
	else
		if escHeld then
			escHeld = false
			keyDownTap:stop()
		end
	end
end)

-- Start the flags event tap
flagsTap:start()

Next, require this file in Hammerspoon’s init.lua:

local CapsEscArrows = require('CapsEscArrows')

Next, configure your modifier keys in “System Settings > Keyboard > Keyboard Shortcuts > Modifier Keys”, and change the “Caps Lock” action to “Escape”.

Finally, don’t forget to reload your Hammerspoon config.

Now when you hold Caps Lock / Esc, h,j,k, and l become vim arrow keys. Also as a bonus d becomes pg down, and u becomes pg up.

I hope this makes you as happy as it made me!

GitHub Gist with CapsEscArrows.lua

Amor Kumar Avatar

👋🏽 Hi! I’m Amor – yes as in love!

I’m a creative front end engineer crafting user experiences with WordPress.
Currently, I’m working with enterprise clients at WebDevStudios.