WezTerm 2: Twenty Leader Shortcuts

In short. In part 1 I made Ctrl+U the prefix key and put splits, movement and tabs behind it. Use it for a while and the next problem arrives. Past twenty keys, what your hands remember and what the config file says drift apart. This post puts the key definitions in one table. That table produces both the real bindings and the help screen. It also covers the price of having a prefix key at all.

Dotori Bluetooth Keyboard - Your phone as a keyboard for PC and TV

A free app I built myself. Give it a try.

Put the key definitions in one table

Line up leader("w", ...) calls the way part 1 did and there is nowhere to write the description. So bundle key, description and action into one three-column entry. The bindings are generated from that list. The description becomes data rather than a comment.

local leader_bindings = {
  { "w", "Split left/right (Width)", act.SplitHorizontal({ domain = "CurrentPaneDomain" }) },
  { "v", "Split top/bottom (Vertical)", act.SplitVertical({ domain = "CurrentPaneDomain" }) },
  { "h", "Move to the left pane", act.ActivatePaneDirection("Left") },
  { "/", "Command palette", act.ActivateCommandPalette },
}

for _, b in ipairs(leader_bindings) do
  table.insert(keys, { mods = "LEADER", key = b[1], action = b[3] })
end

Now put the same list on screen. Bind act.InputSelector to LEADER + . and the entries appear as an overlay, running the action of whichever one you pick. Add a key and the help grows with it, so the two cannot drift.

act.InputSelector({
  title = "LEADER (Ctrl+U) shortcuts",
  fuzzy = true,
  fuzzy_description = "Search shortcuts (↑↓ move · Enter run · Esc close) : ",
  choices = cheatsheet_choices,   -- { id = key, label = "key   description" }
  action = wezterm.action_callback(function(win, p, id)
    for _, b in ipairs(leader_bindings) do
      if b[1] == id then win:perform_action(b[3], p) end
    end
  end),
})

Some keys do not fit in the table. Tab numbers are generated by a for i = 0, 9 loop, which would add ten entries. Those belong in the help as a single informational line and must be kept out of the executable set. Give them an empty id and filter that out in the callback. They show in the list, but pressing them does nothing. Pad the columns with string.format and the keys and descriptions line up vertically.

table.insert(choices, { id = "", label = string.format("%-12s%s", "0 - 9", "Tab by number (info only)") })
-- first line of the callback
if not id or id == "" then return end

Two ways the help screen caused misfires

Do not leave out fuzzy = true. The default mode of InputSelector attaches a quick-select character to each row. That character comes in order from the default alphabet field, 1234567890abc…. It has nothing to do with the shortcut printed on the screen.

In my list the fourth entry was "close pane". Press 4 to jump to tab 4 and the pane closes. The help screen was producing misfires, so I pinned it to search mode.

The second one is when choices is built. Build that list inside the key handler and every press of LEADER + . registers a new event handler through action_callback, and they pile up. The list is fixed anyway, so build it once at config load time and keep it in a variable.

For what you will not memorise, there is the command palette

You have to decide what gets a shortcut and what does not. Attach every action you use a few times a day to the leader and the list passes twenty again. I handed the rest to the command palette and kept a single LEADER + /.

The palette is a modal overlay that searches every command WezTerm knows. Typing narrows it with a fuzzy match, and what you used often floats to the top. Its default key is Ctrl+Shift+P, so it does not need to go on the leader either. Some things already exist in the defaults, such as Ctrl+Shift+Z for zooming a pane to full screen. I left those where they were.

A combination spent on the leader is lost in the shell

Once you pick a prefix key, that combination never reaches the shell. The moment you claim Ctrl+U, the Ctrl+U that deleted everything before the cursor in bash is gone. Pick your leader from combinations your hands reach for less often in the shell. Pick it at the start, because changing it later is hard.

While the leader is armed, even keys that are not bound get swallowed. The official documentation puts it the same way: they are not passed through to the terminal. That is why the ripple indicator in part 1 is not decoration. Without it, the key you press to check whether you are in the waiting state simply disappears. After two seconds the wait clears on its own.

Summary

Put key, description and action in one table and the bindings and the help read the same source. Pin InputSelector to fuzzy = true and build the list once at load time. Leave commands you rarely use to the command palette rather than the leader.

Is your key list from part 1 already past ten lines? Move those lines into a three-column table and bind LEADER + ..