Scripting iTerm Key Mappings

Jeroen Janssens
Jan 19, 2023 • 13 min read

To improve my iTerm+tmux experience, I’ve set up a whole bunch of key mappings. Rather than defining these manually, I wrote a Python script to generate the corresponding JSON programmatically.

An expressive oil painting of a desk with a couple of keyboards and a monitor showing the unix terminal.
An expressive oil painting of a desk with a couple of keyboards and a monitor showing the unix terminal.

Fixing prefixing

I’ve recently refound my love for tmux. Having multiple terminal panes, windows, and even sessions helps me keep my command-line shenanigans organized. By default, you interact with tmux by pressing a prefix key (Ctrl-B) followed by another key. For instance, Ctrl-B C creates a new window and Ctrl-B " splits the current pane into two. That’s incredibly powerful, but I don’t particularly enjoy pressing prefixes all the time.

Fortunately, iTerm, which is the terminal emulator that I use on macOS, has the ability to define key mappings. This allows you to simulate, say, Ctrl-B J by pressing Cmd-J.[1] Defining these key mappings manually, however, turns out to be a tedious and error-prone task.

Using Python to generate key mappings

Like any real developer, I wrote a script to automate this task. Here’s a screenshot showing a portion of the 116(!) key mappings the script generates:

I currently have 116 key mappings defined in iTerm. Thanks Python!
I currently have 116 key mappings defined in iTerm. Thanks Python!

You can find the complete Python script below or you can download it from GitHub. It generates three types of key mappings:

  1. If you press Cmd-key, iTerm sends Ctrl-B key, for A to Z and then some, except X, C, V, and Q to keep the default cut, copy, paste, and quit keyboard shortcuts.
  2. If you press Cmd-Ctrl-key, iTerm sends Ctrl-B Shift-key for A to Z and then some.[2]
  3. If you press Option-key, iTerm sends Ctrl-B Ctrl-key, for A to Z.

The output of the script is JSON, which looks[3] like this:

$ ./itermkeymap.py | trim 20
{
"Key Mappings": {
"0x31-0x140000": {
"Version": 1,
"Action": 11,
"Text": "0x02 0x21",
"Label": "C-b !"
},
"0x27-0x140000": {
"Version": 1,
"Action": 11,
"Text": "0x02 0x22",
"Label": "C-b \""
},
"0x33-0x140000": {
"Version": 1,
"Action": 11,
"Text": "0x02 0x23",
"Label": "C-b #"
},
… with 680 more lines

This JSON structure is discussed in more detail below.

I haven’t yet found a way to import these keymappings programmatically, so you’ll first have to save the output to a file:

$ ./itermkeymap.py > generated.itermkeymap 

And then import that file by clicking “Presets…” and “Import…” at the bottom of the Key Mappings section of the iTerm preferences.

Key mappings are personal

You’re of course welcome to use this script as it is, but I can imagine you’ll want to adapt it to your personal preferences. For instance, if you use Ctrl-A as your tmux prefix, you’d need to change “b” and “02” to “a” and “01” in the following two lines:

prefix_key = "b" # My prefix key in tmux is Ctrl-B
prefix_hex = "02" # The hex code that iTerm sends (corresponds to Ctrl-B)

Or perhaps you’d like to change which keys and modifiers are used:

keys_upper = string.ascii_uppercase + r'!@#$%^&*()_+{}:"|<>?~'
keys_lower = string.ascii_lowercase + r"1234567890-=[];'\,./`"
keys_exclude = "xcvq" # Don't override cut, copy, paste, and quit shortcuts

mod = {"cmd": "100000",
"cmd+ctrl": "140000",
"option": "80000",
}

If you want to use different modifiers or other special keys (e.g., arrow keys), my advice is to

  1. manually define a key mapping in iTerm,
  2. export the key mapping to a file, and
  3. inspect the JSON using, e.g., jq . export.itermkeymap.

Two useful tools for finding hex codes are the ASCII manual page, c.f. man ascii and making hexdumps using xxd -p.

JSON structure explained

As an example, consider the JSON corresponding to the key mapping Cmd-J sending Ctrl-B J:

{
"Key Mappings": {
"0x6a-0x100000-0x0": {
"Version": 1,
"Action": 11,
"Text": "0x02 0x6a",
"Label": ""
}
}
}

I couldn’t find any documentation about the JSON structure, but here’s what I’ve learned so far:

  • “0x6a-0x100000-0x0”: Refers to the key combination you press to trigger the key mapping, consisting of three parts:
    1. “0x6a”: is the hex code for “j”. See man ascii for the complete ASCII table in hex.
    2. “0x100000”: refers to the Cmd key.
    3. “0x0”: I’m not sure what this does. It can be safely left out when importing a JSON file.
  • “Version”: Is always 1; not interesting.
  • “Action”: “11” stands for “Send Hex Codes”.
  • “Text”: The hex codes being sent. In this case Ctrl-B followed by J.
  • “Label”: Doesn’t seem to be used by iTerm. The script puts the key mapping in plain text here for debugging purposes.

Defining the corresponding tmux key bindings

All these key mappings only make sense when you define the corresponding key bindings in tmux[4]. Here are tree key bindings to illustrate the three types of mappings for the H key:

bind h  show-message "Received Ctrl-B H"        # If you press Cmd-H
bind H show-message "Received Ctrl-B Shift-H" # If you press Cmd-Ctrl-H
bind ^h show-message "Received Ctrl-B Ctrl-H" # If you press Option-H

My own tmux configuration defines, among many others, the following key bindings:

bind h select-pane -L                           # Select pane to the left
bind j select-pane -D # Select pane below
bind k select-pane -U # Select pane above
bind l select-pane -R # Select pane to the right
bind t new-window -c "#{pane_current_path}" # New window
bind w kill-pane # Close current pane

These key bindings enable me to navigate to other panes using Cmd-H, Cmd-J, Cmd-K, and Cmd-L (aka vim style). Moreover, common keyboard shortcuts such as Cmd-T and Cmd-W are now intercepted; they no longer create and close iTerm tabs, but create a new tmux window and close the current tmux pane, respectively. After all, who needs tabs when you’re running tmux?

That’s all for now. May your sessions live long and prosper.

– Jeroen

Appendix: The complete script

Below is the script I use to generate the JSON containing the key mappings. Obviously this script can be improved in many ways, but it gets the job done. You can also download the script from GitHub.

#!/usr/bin/env python3

import json
import string

prefix_key = "b" # My prefix key in tmux is Ctrl-B
prefix_hex = "02" # The hex code that iTerm sends (corresponds to Ctrl-B)

keys_upper = string.ascii_uppercase + r'!@#$%^&*()_+{}:"|<>?~'
keys_lower = string.ascii_lowercase + r"1234567890-=[];'\,./`"
keys_exclude = "xcvq" # Don't override cut, copy, paste, and quit shortcuts


mod = {"cmd": "100000",
"cmd+ctrl": "140000",
"option": "80000",
}

keys_all = "".join(sorted(set(keys_upper +
keys_lower).difference(keys_exclude)))

mappings = {}

# Cmd+<KEY> becomes Prefix <KEY>
# Cmd+Ctrl+<KEY> becomes Prefix Shift+<KEY>
for send_key in keys_all:

send_hex = send_key.encode("utf-8").hex()

if send_key in keys_upper:
press_key = keys_lower[keys_upper.index(send_key)]
press_mod = mod["cmd+ctrl"] # Command+Ctrl modifier
else:
press_key = send_key
press_mod = mod["cmd"] # Command modifier

press_hex = press_key.encode("utf-8").hex()

mappings[f"0x{press_hex}-0x{press_mod}"] = {
"Version": 1,
"Action": 11, # Corresponds to "Send Hex Codes" action
"Text": f"0x{prefix_hex} 0x{send_hex}", # Hex codes to send
"Label": f"C-{prefix_key} {send_key}" # Nice for debugging
}

# Option+<KEY> becomes Prefix Ctrl+<KEY>
# Generate Option+A through Option+Z
for i, send_key in enumerate(string.ascii_lowercase, start=1):
send_hex = hex(i) # returns 0x01 for a, 0x02 for b, etc.

press_mod = mod["option"] # Option modifier
press_hex = send_key.encode("utf-8").hex()

mappings[f"0x{press_hex}-0x{press_mod}"] = {
"Version": 1,
"Action": 11, # Corresponds to "Send Hex Codes" action
"Text": f"0x{prefix_hex} {send_hex}", # Hex codes to send
"Label": f"C-{prefix_key} C-{send_key}" # Nice for debugging
}

result = {"Key Mappings": mappings}
print(json.dumps(result, indent=4))

  1. Although you cannot bind to the Cmd key in tmux, you can still use it in an iTerm key mapping to simulate pressing other key combinations. ↩︎

  2. For some reason, I’m having trouble mapping Cmd-Shift so I’m currently using Cmd-Ctrl. ↩︎

  3. In case you were wondering about trim, it can be found in my dsutils repository. ↩︎

  4. The tmux configuration is usually located at ~/.tmux.conf or ~/.config/tmux/tmux.conf. ↩︎


Would you like to receive an email whenever I have a new blog post, organize an event, or have an important announcement to make? Sign up to my newsletter:
© 2013–2024  Jeroen Janssens