We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Copacetic keybindings for my TUI on MacOS
I have a personal TUI text-editor app thingy that runs in a terminal emulator.
One of my main ambitions for this program was that it should be intuitive to use, for my personal value of intuitive. Ideally, the editor should work like this on my Mac:
-
Ctrl-Enter
orCmd-Enter
to “submit” a thing -
Ctrl-X/C/V
orCmd-X/C/V
to copy/cut/paste text - Arrow keys to move the cursor
-
Option-Left/Right Arrow
to move by a word (whereOption
is AKAAlt
) -
<Some modifier>-Left/Right Arrow
for Home and End
Some of this fights terminal-emulator norms.
Because I put the cart before the horse (vibe coding), I bumped up against constraints by trial and error, which left me with only a vague understanding and a bunch of superfluous code.
I am back to nail down what’s copacetic and what needs a workaround. This stuff is fascinating to me because there’s so much of it that I’ve never really thought about, which means there may be errors.
Why are there any constraints?
Because terminal emulation isn’t skin deep; it’s the outcome of continuous evolution from the 1960s and ‘70s when hardware terminals and computers communicated by a limited set of character codes.
In 1971, it was possible to type commands like ls
, cat
, chmod
, and rm
on a Teletype Model 33 to execute them on a DEC PDP-7 minicomputer running the first edition of Unix.
Somehow, after decades of nerdery and market forces, the fingerprints of that Teletype remain all over the terminal emulator you use to SSH into a Linux VM and run ls
, cat
, chmod
, and rm
.
And, somehow, Unix paradigms underlie most of our OSes, including MacOS, where I’m running my program.
Unix’s TTY subsystem let programs trade ASCII bytestreams with a Teletype or video terminal. The kernel would interface with your user-space program (the sh
command interpreter; the ed
editor) at one “end”, and at the other end it would drive hardware to shuttle ASCII to and from serial current or voltage signals.
4.2BSD (a Unixy OS) introduced the pseudoterminal (PTY) driver in 1983 to let user-space programs interface with both ends of the TTY subsystem. Now you could replace your hardware terminal with a software terminal emulator, running on the same computer as your shell.
And that’s basically what happens under MacOS in 2025. My terminal emulator and my shell (or my TUI blogging program) communicate by passing bytes back and forth through the kernel TTY subsystem, via PTY pseudo-devices.
As a regular GUI program, the emulator can decide which bytes, if any, to push when it receives an input event from the OS. By default, it’ll map key events the way that my shell, SSH, and that remote Linux VM expect, and that won’t match my GUI-centric keybinding preferences.
My TUI program squats in the terminal emulator because I like a text-based interface. Because it’ll reconstitute my little bit of logic into a whole lightweight UI, no platform-specific development required. The fastidiously tended compatibility is something I sometimes have to work around.
Ctrl-Enter
doesn’t work
The 7-bit ASCII character set took most of the 1960s to stabilise. The initial standard, ASA X3.4-1963, had some unassigned codes. By 1968, they’d given in and used most of them for lowercase letters.
Of the 128 ASCII codes, 95 represent printable, or “graphic”, characters—“text”. The other 33 are control codes for stuff like coordinating communication and manoeuvring print heads and paper.
The ASCII table was painstakingly arranged so that you could get to 32 of the control codes by zeroing the 6th and 7th bits on 32 of the printable characters. Which is what the CTRL
modifier key did on the Teletype 33, and on video terminals like DEC’s VT series (starting with the 1970 VT05) and the 1976 Lear Siegler ADM-3A.
For completeness: the 33rd control code, DEL
, lives in the last slot on the 7-bit ASCII chart, which is 1111111
—good for deleting a character on paper tape by going back to its position and punching all the holes.
Back to Ctrl-Enter
.
There were a few control codes you’d commonly want to send from the terminal, and it became normal on video terminals to have keys for them: HT
(horizontal tab), BS
(backspace), LF
(line feed), DEL
(delete), and CR
(carriage return), emitted by the RETURN
or ENTER
key (or both, as the case may be).
So ENTER
is already a control code; CTRL-ENTER
would be like CTRL-CTRL-M
, which is not, historically, a thing. If you mask the top two bits of CR
you get CR
again, which is how Ctrl-Enter
acts in iTerm2 by default.
Again, in 2025 we’re allowed way more key events than just ASCII codes, and I can get around this by setting a custom keybinding on an iTerm2 profile; I checked. I’m still loath to do it.
Cmd-Enter
doesn’t work either
Or, I should say: Cmd-Return
doesn’t work either.
In principle it could, since it generates a key event and the command
key on my Apple computer isn’t mentioned in any ANSI standard from 45 years ago.
But equally, combinations that aren’t spoken for by any ASCII code are in danger of being claimed by MacOS and terminal emulator programs.
Cmd-Return
‘s default behaviour in both GhosTTY and iTerm2 is to toggle full-screen mode, so I’d have to change that setting (if possible) and then add the custom keybinding.
I admitted defeat and set Ctrl-S
as my shortcut for submitting changes to a text entry.
Arrow keys work fine, thanks to escape sequences
All the ASCII control codes were used up by the time video terminals needed a way to move a cursor around on a screen.
The VT05 “solved” this by just commandeering some control codes for arrow keys.
Key | Code | ASCII control |
---|---|---|
up |
CTRL-Z |
SUB (SUBSTITUTE ) |
down |
CTRL-K |
VT (VERTICAL TABULATION ) |
right |
CTRL-X |
CAN (CANCEL ) |
left |
CTRL-H |
BS (BACKSPACE ) |
If your terminal and your program agreed, this worked. These codes weren’t otherwise relevant to the VT05.
I had a quick look on the webs, and I’m not sure what this was actually good for (aside from BACKSPACE
). It seems there wasn’t a lot of software that used this kind of cursor movement in the very early 1970s, and you’d have to write your software specifically to understand the control codes in this custom way.
You couldn’t really standardise on this approach, because other devices still existed that did use the canonical meanings.
But ESC
, sitting in one of those 128 precious ASCII slots, offers a more elegant option: it’s a prefix affecting the interpretation of a limited number of contiguously following characters.
That is, it starts an escape sequence. Escape sequences vastly increase the number of possible signals by letting us bundle multiple ASCII codes together.
There was a standard for the purpose and general form of escape sequences in 1971, but which sequence of codes to use for what was still freeform.
The 1975 VT52 used ESC A
to move up one line; ESC C
to move right one position, and so on.
A standard for specific escape sequences appeared in 1976: ECMA-48; followed in 1979 by a second edition and the nearly identical ANSI X3.64.
It seems the VT52 guessed wrong.
In 1978, the DEC VT100 was released: the first terminal compatible with the upcoming ANSI standard. Its arrow keys emitted standard escape codes:
Key | Code | ANSI control |
---|---|---|
up |
ESC [ A |
CUU (CURSOR UP ) |
down |
ESC [ B |
CUD (CURSOR DOWN ) |
right |
ESC [ C |
CUF (CURSOR FORWARD ) |
left |
ESC [ D |
CUB (CURSOR BACKWARD ) |
ESC [
is the Control Sequence Introducer for 7-bit ASCII.
If I do cat -v
in a terminal emulator today and press arrow keys, I get the same things, albeit in a different notation: ^[[A
, ^[[B
, ^[[C
, ^[[D
.
^[
for Ctrl-[
reflects that ESC
is itself a control code.
I can rebind Ctrl-X/C/V
. Why?
These are all control codes with canonical meanings: CTRL-X
is CAN
(cancel), CTRL-C
is ETX
(end of text), and CTRL-V
is SYN
(synchronous idle). I want to use them for “cut”, “copy”, and “paste”—and this works fine.
We told video terminals they couldn’t repurpose control codes, but this is just a contract between a single program and its users; not nearly as big a deal, as long as the connected device doesn’t need these control codes for something.
The device is a terminal emulator, and it doesn’t absolutely need any of them.
There is another slightly more modern question, though: doesn’t Ctrl-C
cause a SIGINT
on Unix-like systems?
When I talked about the kernel TTY subsystem earlier, I didn’t mention the line discipline layer. The line discipline used to be really handy for buffering your command until you hit ENTER
, and providing basic editing to fix typos. At some point the line discipline started also taking Ctrl-C
to mean “send a SIGINT
“.
That’s its behaviour in its default mode, called the canonical or cooked mode. But the line discipline also has a raw mode. In raw mode it passes the ASCII/UTF-8 codes along to the program, and the program decides whether Ctrl-C
means it should exit, or copy selected characters, or something else. A user-space program can specify which mode to use.
In other words, it’s condoned for a program to repurpose control codes away from the standards, so that’s what I do.
What about Cmd-X/C/V
?
I already know I can’t rely on Cmd-<anything>
in a CLI or TUI program, and I don’t generally want to use Ctrl
for some things and Cmd
for others.
But I might make an exception for clipboard operations. Cmd-X/C/V
are the standard Apple shortcuts for these. I often want to copy and paste things between applications, so I’m switching between shortcuts there for what my muscles think is the same thing.
I notice that both iTerm2 and GhosTTY use Cmd-C
and Cmd-V
to copy and paste out of the box. Cmd-X
seems unclaimed. This may work!
As far as my specific app is concerned, I have to tease out where I’m using tui-textarea’s internal yank buffer vs. Arboard for interacting with the system clipboard (vibe coding), but the Cmd
shortcuts may actually be fine.
Option-Left/Right Arrow
to jump by a word
Option-Left/Right Arrow
to jump the cursor by one word has been a thing on Macs since MacWrite (1985).
My stubbornness about changing terminal profiles has hit a snag: my emulators don’t have matching defaults.
In both Terminal.app and GhosTTY, Option-Left Arrow
/ Option-Right Arrow
send ^[b
and ^[f
: the Emacs shortcuts ESC b
and ESC f
in disguise. Zsh and tui-textarea both happily agree that these are for moving back or forward by one word.
iTerm2 sends xterm-style escape sequences instead.
ANSI X3.64 and friends don’t know about the Alt
modifier, which is what iTerm2 is identifying the Option
key as. xterm added support for modifier-key parameters in “function-key” escape sequences in 1999, so that Option-Right Arrow
can emit ^[[1;3C
, a modified CURSOR FORWARD
where the parameter 3
flags that the Alt
key is depressed.
Sounds reasonable! xterm established a lot of de facto standards in terminal-emulator land, including escape sequences for 256 text colours, and mouse events.
But empirically, no program I’ve met seems to be interpreting ^[[1;3C
as “hop by a word” or anything else.
My easy way out is to go into iTerm2’s Settings -> Keys -> Key Bindings -> Presets
and choose “Natural Text Editing” or “Terminal.app Compatibility”.
This made line editing in my shell and Claude Code a lot nicer too, so I could argue that I’m not doing anything to my terminal profile specifically for one program.
Just to check that I’m not bending over too far backward to avoid the happy path here, let’s check out oldey-timey terminalish ways to do jumping by a word.
There’s the Vim way: get into (checks notes) Normal
mode and hit b
or w
(or e
). I’m just not ready to embrace modal editing.
There’s the Emacs way: M-b
or M-f
(where M
is Meta
, which can map to the Alt
modifier or Esc
prefix). Esc b
and Esc f
work in just this way on all the terminals I tried!
It’s a bit inconvenient to hit this combination repeatedly, though, and while Option-Left Arrow
/ Option-Right Arrow
are available to my program, MacOS snags a lot of Option
combinations, including Option-b
and Option-f
, to print special characters.
I haven’t dug around to find out how Emacs users on Mac deal with this, but I’m reassured that “switch to Emacs keybindings” is not a superior solution to slapping a popular preset on my iTerm2 profile.
Cmd-Left/Right Arrow
for Home and End
In other apps on my Mac I’m seeing Home
and End
functionality provided by either fn-Left/Right Arrow
or Cmd-Left/Right Arrow
(or both). The Cmd-<arrow>
shortcut again dates back to MacWrite of 40 years ago.
iTerm2 claims Cmd-Left/Right Arrow
for switching tabs. But with the “Natural Text Editing” keybinding preset and only one iTerm2 tab, it’ll emit the same as Terminal.app and GhosTTY: ^A
and ^E
. More Emacs conventions in the shell! And they’re the right ones for Home
and End
.
Funny story, though, iTerm settings aside: in my application code, I treat Ctrl-A
as “select all text in the editor”.
fn-Left/Right Arrow
for Home and End
fn-Left Arrow
and fn-Right Arrow
in both GhosTTY and iTerm2 give ^[[H
and ^[[F
, escape sequences now understood reliably as “go to the start of the line” and “go to the end of the line”.
Those aren’t the ANSI x3.64 / ECMA-48 meanings of these escape sequences; the convention seems to have grown out of DEC’s 1993 VT510 terminal having a mode to emulate a console for SCO Unix.
The xterm changelog has an entry for a patch in 2000 with the text “add logic to implement SCO function-keys.”
Terminal.app doesn’t pass anything along that I can see in cat -v
, and the effect in zsh or bash looks like a true home and end here; but within my TUI app, for whatever reason either (app code or tui-textarea) is giving my desired behaviour.
So I’m treating this one as reliable.
Roundup
I know. This is a long post. Here’s a barebones look at the various key combos and their practical quirks:
-
Ctrl-Enter
-
Enter
is, itself, an ASCII control code, so there’s no standardised way to handleCtrl-Enter
(Ctrl-Ctrl-M
). Might get it to work with terminal emulator options.I substituted
Ctrl-S
.
-
Cmd-Enter
-
Inconveniently claimed for toggling fullscreen mode in iTerm2 and GhosTTY.
-
Ctrl-X/C/V
-
OK! With the TTY line discipline in raw mode, ASCII control codes are passed in and the program can interpret them as needed.
-
Cmd-X/C/V
-
May be fine for cut/copy/paste!
Cmd-C
andCmd-V
copy and paste out of the box in iTerm2 and GhosTTY.My particular app has to decide how it’s interacting with the system clipboard and tui-textarea’s internal buffer.
-
Option-Left Arrow
/Option-Right Arrow
-
These do what I expect—jump by one word—by default in Terminal.app and GhosTTY.
iTerm2 needs custom keybindings to enable this.
-
Cmd-Left Arrow
/Cmd-Right Arrow
-
In Terminal.app and GhosTTY:
^A
and^E
. Emacs keybindings for “go to the start of the line” and “go to the end of the line”; understood as such by zsh and tui-textarea.iTerm2 uses these to switch tabs by default.
Bigger conundrum: a clash in my app, which binds
Ctrl-A
to “select all text in my editor”.
-
fn-Left/Right Arrow
-
These do what I expect in my TUI editor.
In iTerm2 and GhosTTY:
^[[H
and^[[F
. Escape sequences understood by zsh and tui-textarea as “go to the start of the line” and “go to the end of the line”.Terminal.app is doing something different, but in my app it’s working as desired.
I think I haven’t discovered maximum copacetic…ity…in keybindings for editing text in a TUI on Mac. That being said, my app is now reasonably intuitive, and I use it.
There’s one thing, though. It hurts my little finger.
All shortcuts with the control
key are bad on Mac
Apple likes Cmd
for most of the things I’d use a shortcut for in an editor, so they’ve happily squished Ctrl
over by one spot, where no finger can easily reach it, to fit a fn
key at the start of the row. And because Cmd
isn’t a 1970s terminal modifier, the OS and the terminal emulator frequently claim it for their own shortcuts.
You know when you go against the flow and it kicks your ass repeatedly onto the rocky riverbed until you start to understand the attraction of Vim?
Yeah.