Create emacs-style keychains keybindings in hammerspoon? I discovered Hammerspoon today, a keybinding and desktop automation tool reminiscent of i3wm or awesonewm for linux, but combined with something like xdotool.
I am an emacs user, and find emacs's (and similarly vim's) solution of using &quot;prefix&quot; keys to support more keybindings useful.
In emacs (and vim) you can support &quot;prefix&quot; keys, which once pressed select a different map of keybindings. So for example:
C-x  followed by  ,   followed by  v could be bound to a single keybinding which is different from v.
Is there any way to acheive this in hammerspoon (short of writing my own library that toggles bindings on and off as I press keys)?
Research

This script seems to implement a special case of this sort of logic in a handcode way
This native component, hs.hotkey.modal, looks to provide &quot;modes&quot; similar to i3.
https://github.com/Hammerspoon/hammerspoon/issues/208
https://www.hammerspoon.org/Spoons/RecursiveBinder.html
If you're still wondering how to do this, then the RecursiveBinder Spoon for Hammerspoon will do the trick. I wrote a Medium article about how to use it to make a leader key for macOS.
Here is a minimal lua snippet on how to use it.
hs.loadSpoon(&quot;RecursiveBinder&quot;)  -- Load the spoon 

spoon.RecursiveBinder.escapeKey = {{}, 'escape'}  -- Press escape to abort

-- singleKey is a convenience function provided in the Spoon
local singleKey = spoon.RecursiveBinder.singleKey

local keyMap = {
  [singleKey('b', 'browser')] = function() hs.application.launchOrFocus(&quot;Firefox&quot;) end,  -- 'b' is the keybinding, 'browser' is the text shown in the onscreen helper text
  [singleKey('t', 'terminal')] = function() hs.application.launchOrFocus(&quot;Terminal&quot;) end,
  [singleKey('d', 'domain+')] = {  -- Separate layer
    [singleKey('g', 'github')] = function() hs.urlevent.openURL(&quot;github.com&quot;) end,
    [singleKey('y', 'youtube')] = function() hs.urlevent.openURL(&quot;youtube.com&quot;) end
  }
}

-- Calling recursiveBind returns the activation function, and here, it is bound to option+space
hs.hotkey.bind({'option'}, 'space', spoon.RecursiveBinder.recursiveBind(keyMap))  

Here, pressing option+space activates the first layer with b, t, d (browser, terminal, domain respectively)
Pressing d will activate the layer with g and y.
At each level, a helper text is shown, like which-key in emacs.