macOS Terminal - still missing the mark Apple!

Terminal app icon 

(Note: I'm resurrecting this dormant blog after a long period of inactivity.  But this thing bothered me so much that I felt I needed to write a blog about it.)

TLDR: The macOS Terminal.app is still buggy and not recommended. Instead consider switching to a 3rd party emulator.  The rest of this post describes the faults in far more detail.

One of the more lauded features in macOS 26 is the updated Terminal.app.

As some of you may know, I'm the author of a very popular library for interacting with terminals and terminal emulators, at least within the Go ecosystem.

I was excited to hear about the new Terminal.app. Finally, we would get support for 24-bit color, years after everyone else has had it. (Even Microsoft beat Apple to the punch here, leading by something like half a decade.)

So yes, the good news is that macOS Terminal.app in does support 24-bit color.

But in nearly every other interesting case it is far inferior.

The rest of this is probably only interesting to terminal nerds, but for those of you who care about these things, the new Terminal.app is a major disappointment.

Identification Woes

The first and most egregious failing is that there is no good way to remotely identify the macOS terminal, or even what features it might support.

Normally we would like to be able to query and get useful feedback via one of the following mechanisms:

  1. Primary Device Attributes (obtained via CSI c): Terminal.app reports that it is a vanilla VT100 with no extra features, it does not claim to be a VT220 or similar.

  2. 2. Extended Device Attributes (obained via CSI > q): Terminal.app reports nothing here. Most modern terminal emulators store their application name and version here. While I dislike hardcoding capabilities for a specific terminal emulator (so-called "quirks"), this isn't even an option because we can't know that this is Terminal.app.

  3. 3. DECRQM - DEC mode queries.  This is supported officially starting with VT320 and up, but pretty much every other software emulator supports it. This can be used to query whether a given DEC mode (including things like mouse support and features, automatic margin wrap, etc.) are present and modifiable.  It has been used for new things like synchronized output (to fix tearing during a resize) and to identify support for grapheme clusters (very important in a Unicode world).  Terminal.app does not support these queries, but it even gets it wrong in a worse way. This is a CSI sequence, so the reasonable behavior would be to parse to the end of the sequence, and discard it if you don't support it. Terminal.app instead aborts the parsing early, emitting the final character of the sequence on the screen. So during early startup we see these mysterious "p" characters show up as Tcell tries to query for support.

Ok, this is just about identification.  What about TERM?  Well Terminal.app identifies as xterm-256color - although it clearly is not compatible with genuine xterm.  What about $TERM_PROGRAM?  Well, yes, we could use that.  But note that this environment variable is not passed through ssh by default, which makes it useful only for locally running applications.

Unicode Woes

Modern applications and users expect to be able to use things like skin-tone modifiers for emoji, national flags, and similar "characters" (well graphemes technically, which is just another way of saying a user-perceived character). In Unicode, these graphemes are composed by combining multiple Unicode "characters" together. 

This approach is also used with certain languages, such as Arabic, or even in Western languages where a character might be modified to include the diaeresis using two code points joined by a special character called the "zero-width-joiner". (For example, in Bengali, র্য (No, I do not read or speak Bengali.)

How does Terminal.app fare here?  Well its mixed results. For some, such as 👨‍🚒 and the Bengali example, it works reasonably well.  But for others, like the flag emoji 🇨🇭 it miscalculates the width.  (In this case it calculates the width of the flag as just one cell, instead of two, even though it renders it using two cells!).

This behavior could be detected by an application emitting the composed sequence, then querying the resulting position using CSI 6 n.

It would be easier, and better if Apple would implement the DECRQM and advertise mode 2027.  It could be hard coded to 3 (forced on) in this case, unless Apple were prepared to add the complexity to support both legacy and modern grapheme support.

Hashimoto also has something to say about Terminal.app and grapheme clustering: 

🤡 Living in its own cursed little world

(Apparently it miscalculated the width a joined sequence as 6, probably because it considered the ZWJ as occupying two cells.)

Broken State Machine


So modern terminals, which includes everything starting from VT100 or claiming to support ANSI X3.64 or ECMA 48, are expected to fully parse (but may not implement) certain classes of escape sequences.  For example,  consider the sequence emitted by this command:

printf "\x1b]something\x1b\\"

This is a OSC (operating system command).  The "\x1b" values are escape codes, and the command is "something".  Now no terminal (probably) has a command "something".  (Usually OSC sequences begin with a number followed by a semicolon, followed by arguments, but this isn't required.)

Every reasonable implementation should parse the entire string, using a simple state machine that recognizes "\x1b]" is the start of OSC sequence, which is terminated by either "\x1b\\" (formally the ST sequence in ECMA-48), or "\x07" (due to historical accident.)  Note that ST can also be encoded in a single eight bit character as "\x9c", but 8-bit encodings are uncommon due to potential confusion with other national encodings (but not UTF-8, or ISO-8859, or any EUC or ISO-2022 encoding).

So what does Terminal.app do when it sees this sequence? It prints "omething" on screen.  (It consumed the leading "s".)  One possible interpretation is that they implement all OSC commands by printing the entirety of the command minus the first character, to the screen.  Is this a legal interpretation?  Technically, yes.  Is it a reasonable interpretation?

I believe this is an egregious violation of Postel's Law. (By the way, if you work in software, you should be well familiar with Postel's Law, which was first invoked by the late Jon Postel, whose name is listed as an author of most of the foundational RFCs that define the Internet.)

More importantly, this interpretation means that you cannot safely use OSC sequences at all on Terminal.app, although you can reasonably do this for real xterm. But Terminal.app is not a real xterm, so that should be fine -- except that Terminal.app does nothing to make itself discoverable. It doesn't define its own value of $TERM, it doesn't answer any of the standard queries to test for functionality, nor does it answer the extended attributes to help a user program identify that it should avoid any of the normal things it might expect to be able to do with a real implementation of xterm.

Btw, this is not limited to just the OSC sequences.   A similar, but slightly different behavior occurs with DCS sequences (device control sequences).  It just emits the entire string (including the first character).

What about APC, SOS and PM (which are generally unused by modern terminals)? They behave as DCS. The content goes straight to screen.

Then there is that strange case of CSI sequences that it does not understand. Remember DECRQM above? That is a valid CSI sequence:

CSI ? Pm $ p

(The final character here is "p" and the Pm is a numeric mode number such as 7 for automatic margin wrapping.) As we have seen, the parser in Terminal.app does not swallow these, but acts as if "$" were the legal character.  (Technically "$" is considered an "intermediate byte" in the ECMA specification.)

Modern Key Events


Modern terminal applications are looking for ways to make better use of the keyboard, for things like control-keys for short cuts.  Historically, it was impossible to discriminate between certain keys.  For example, Ctrl-I is the same as TAB, and Control-M is the same as Enter. (This goes all the way back to decisions made in the 1970s.)

In answer to this, there are a few standards available.  The most widely popular one is a protocol designed by the author of kitty, and lets terminal applications fully discriminate different key sequences and can even report key repeat and release events.  (Xterm has its own way to do this, which is a subset of the functionality, and Windows Terminal invented its own scheme called win32-input-mode, which offers the full richness but has some drawbacks in efficiency and safety -- you cannot recover from this mode if you turn it on by mistake in a shell.)

Of course, Terminal.app has none of these, and is stuck in the 1970s for key reporting.

So What To Do?


With all these problems with Terminal.app, one might wonder what recourse there is. Fortunately, I have good news.

There are a plethora of excellent options available which are all far superior to Terminal.app, and which are free. (There are non-free options as well.)

Personally, I use the excellent Ghostty, but in the past I had great results with the old stand by iTerm2 as well as kitty.  The up and coming Rio is also pretty excellent. The Alacritty program is popular but I found it inferior to these other options (no grapheme clustering, and no advanced keyboard support). I found WezTerm to be brittle (it would occasionally hang), so I don't recommend it personally, but it has many adherents.

What About Windows Users?


For Windows users, Rio might be the best option, although for advanced keyboard handling the modern Windows 11 Terminal is quite excellent.  It still lacks some things I'd like to have, like support for CSI > q identification, and better support for grapheme clusters, but for most applications it behaves quite well. One I specifically do not recommend is ConEmu, as it has many different ways in which it is broken.  It also appears to be abandonware, in spite of its once great popularity.

Comments

Popular posts from this blog

The Hand May Be Forced

GNU grep - A Cautionary Tale About GPLv3

MacOS X 10.10.3 Update is *TOXIC*