So you want to render colors in your terminal
📖 tl;dr: Most terminal emulators lie about which color space they support. That's why most detection libraries hard code color support when a certain combination of platforms and terminal emulator is used.
If you've been writing command line tools for other developers to use, there will come a time where the clarity of the output can be enhanced through colors. What appears to be simple on the surface gets a bit messy as soon as you dive into the details on how colors are supported in various terminal emulators. Funnily, much of this complexity is due to historical reasons which have been kept alive to this date.
Humble beginnings
In the early days of computing there were no terminal colors. Everything was rendered in either black or white. Demand grew for more complex rendering in the terminal and that's how ANSI escape codes were born. They represent a special set of character sequences that control cursor location, color, background color, font styling, and other options.
// This will print the string "foobar" in green to the terminal
console.log("\u001b[32mfoobar\u001b[39m");
...which looks like this in the terminal:
There is a catch though and that is that only a total of 16 colors are supported. Nearly half of them are lighter shades of existing colors, so the true perceived amount of colors feels even more limited. Despite those limitations, they are usually enough for most apps.
One cool aspect about this color palette is that pretty much every terminal emulator allows you to change the color values. This opens up the door for theming and styling the terminal to your likings. You can see a good overview of the default palette of various terminal emulators on Wikipedia.
The full list of supported colors is:
- black
- white
- gray + light gray
- red + light red
- green + light green
- yellow + light yellow
- blue + light blue
- magenta + light magenta
- cyan + light cyan
All these colors can be used as foreground colors or as background colors. This is how they all render in my terminal.
Supporting all the colors
Over time computers advanced, and so did the richness of colors that developers wanted to use in their terminals. This led to the addition of an enhanced 8-bit color space which supports a whopping 256 colors. Suddenly, there wasn't only green and dark green anymore. You could now display shades of green!
But why stop there? Fast forward a couple of years and with the introduction of graphic cards, demand for even more colors grew. It became common for applications to render 16 or 24-bits of colors. It didn't take long for terminal emulators to follow suit. They jumped right to 24-bit colors which is often referred to as "true color" support. I'll spare you a screenshot since that would be too big for this post. Let me just say that the jump from 256 colors to 16.7 million colors is pretty big.
In summary, we ended up with 4 different color spaces:
- black & white
- Ansi, 16 colors
- Ansi 256 colors
- 24-bit True Color
Detecting color support
This is the bit where it gets messy, because every terminal emulator does it slightly differently. There is no standardized way to detect which color space is supported. It's not just terminal emulators either, because today's developers expect the CI logs to be colored too. Most environments straight up don't tell you what kind of color space they support.
The most common way to detect color support is by checking the TERM
and COLORTERM
environment variables. CI systems can be detected by checking for the existence of the CI
environment variable. Combine that with the knowledge about which operating system the program is running on, and we have a decent enough way to detect colors.
If COLORTERM
is 24bit
or truecolor
, then you know for certain that 24-bit True Colors are supported. Detecting ANSI 256 is usually done by checking if $TERM
ends with 256
or 256color
. If True Colors are supported than ANSI 256 support is a given. Same is true for the basic ANSI escape codes. Again, this detection logic is neither perfect nor elegant, but it works pretty reliable in practice. The Windows Terminal on the other hand doesn't give you anything. Both environment variables are not set there. Therfore everyone simply assumes that the terminal supports full 24-bit colors, which is the case since Windows 10 revision 14931.
Name | OS | ANSI | ANSI 256 | True Color | $TERM | $COLORTERM | $CI |
---|---|---|---|---|---|---|---|
Terminal.app | macOS | ✅ | ✅ | - | xterm-256color |
- | - |
iTerm | macOS | ✅ | ✅ | ✅ | xterm-256color |
truecolor |
- |
Windows Terminal | Windows | ✅ | ✅ | ✅ | - | - | - |
PowerShell | Windows | ✅ | ✅ | ✅ | - | - | - |
GitHub Actions | Linux (Ubuntu) | ✅ | ✅ | ✅ | dumb |
- | true |
The crazy bit is that the only common terminal emulator that I came across that didn't support True Colors at the time of this writing was macOS's built in Terminal.app
. It only supports up to ANSI 256.
CI systems are the real boss battle here, because they often advertise themselves as dumb
terminals with no support for colors. Since developers expect the logs to contain colors, there is no other way than to ignore both TERM
and COLORTERM
variables. Instead color support is inferred by virtue of detecting that the code runs inside the CI.
Color conversions
The missing piece when it comes to colors is converting one color space to another. When a developer uses the notation for True Colors to print something to the terminal, we should at least be able to show some colors.
Here is what it looks like if you try to render True Colors in a terminal emulator that doesn't support them.
And here is the same colors converted down to the more limited ANSI 256 color space.
Sure, the colors are slightly different, but it's better than nothing. It's a "good enough" compromise that keeps the original intentions intact. As far as I know this is only needed for macOS's Terminal.app
, which unfortunately is the one I happen to use the most. For some reason I haven't switched to another emulator so far.
Can we do better?
Despite all those advancements, it feels a little weird to have to be aware of ANSI escape codes as a developer. Most developers just want to set the text color or background color after all. You know browsers allow you to style console.log
messages via plain CSS. What if we leveraged the same thing on the server? That's exactly what deno does. They got it right. They allow you to use the same API to print colors on the server.
console.log(
"%cThis text is lime green %cand %cthis text has a red background",
"color: #86efac",
"",
"background-color: red; color: white"
);
Rendered in Chrome's browser console:
The same code executed on the server with deno:
Conclusion
Sometimes it's all about getting the details right. For me colors have always been a huge part in making CLI output more readable. They can introduce an additional visual hierarchy that's not possible with mere character shapes. Fixing color detection support for some projects was a fun little investigation. Luckily, most of the complexity is already solved by existing libraries in the ecosystem, so that you don't have to work this out yourself.