How to Choose Colors for Your CLI Applications (2023)
68 points by equeue
68 points by equeue
Also worth mentioning: if you care about your CLI tool's accessibility (as those of us who read articles like this with interest probably do), please consider implementing support for NO_COLOR. Some of us have long given up on tweaking color themes, for various reasons including those highlighted in TFA.
NO_FOO settings are terrible. It should've been COLOR=false because then you have actual expressivity for user intent (COLOR=false, COLOR=true, COLOR=nauseating) and simpler logic for implementers.
Better for implementers to style their terminal output semantically, which GNU libtextstyle takes a stab at.
I think the color portability issues outlined in the essay make it kind of pointless to attempt writing settings like nauseating or muted or whatever. A color/no-color dichotomy seems good enough so far for what most terminal apps actually intend to accomplish. It also makes the default option obvious. If you haven't set NO_COLOR, then you have color.
Different communities expect different behavior 0 is false in C but both 0 and "0" are "truthy" in Ruby. Someone might expect COLOR=yes or COLOR=on to work and unless there is strict validation and errors/warnings they get no feedback why their intended value isn't working as they expect.
For that reason I prefer for env vars values to either be exhaustive and known (can raise an error when unexpected input) or any values "on" and unset is "off" and since color is on by default it makes sense to me that NO_COLOR is a feature to be turned on.
I personally always treat empty-string the same as unset in my programs because it's extremely common to write VAR=$OTHER_VAR or whatever, but yes. When I added some flag variables to a program at work I had it recognize yes/on/1/true, no/off/0/false, and the empty string, and it would throw on anything else.
An early version of the no color web page did allow "set to anything => true". I also disagree with the update. I believe it came from the shell community where ':' forms of ${var:=unsetOrEmpty} seem more well known than non-':' forms ${var=unset}.
Honestly I think what this drives home to me is that picking colors for terminal applications is completely impossible. The Solarized behavior in particular strikes me as so annoying that I would personally just close it any issues from it as "wontfix, pick a theme where the colors actually correspond to what they're supposed to be".
This is why my custom terminal emulator... well actually one of the big reasons why it exists at all, but more specifically, why its palette is adaptive.
If an application requests yellow on white, the terminal emulator applies a different yellow than if the applications requests the same yellow on black (or if the application just asks for yellow but the user bg is set to white vs black). Its palette is not a fixed set of colors, it prioritizes readability. Even if I detach/attach a session from a dark bg to a light bg, the palette adjusts since it is a decision at gui draw time, not in the terminal guts.
I think all terminals should do this! Terminal text is meant to be read, so you can make decisions to maximize readability.
alas, for the application author, unless it is me making stuff for myself, they can't rely on this behavior either since most don't do this, and there's no reliable standard way of even knowing if the bg is light or dark.
ghostty has a minimum-contrast option that works pretty well for me
It seems ghostty uses WCAG 2 for contrast? Counter examples over at https://github.com/tattoy-org/contrast-experiments made that seem inadequate. Maybe adam's emulator does something better?
iTerm2 also supports setting the minimum contrast, though its default value is 0. With iTerm’s Light Background color preset, I found that a minimum contrast of 35 struck the best balance between readability and color accuracy.
This! It irritates me a little bit because it makes my prompt look wrong when the background becomes the foreground - e.g. because there is a little triangle shape at the end of text block and then the colors don't match - but this is still way better than not being able to read things due to theming issues.
If an application requests yellow on white, the terminal emulator applies a different yellow than if the applications requests the same yellow on black (or if the application just asks for yellow but the user bg is set to white vs black). Its palette is not a fixed set of colors, it prioritizes readability.
That’s interesting. Does it do this for true colour as well?
My TE doesn't support true color (I implemented it early then later changed my mind and remove that support... it wasn't worth keeping, ate kinda a lot of memory (which is cuz my implementation was poor) but like a terminal is for doing low resolution text work, i don't need a billion colors there)..... but if an application tries to do true color, it maps it to the closest palette entry then still does some adjustment, so the end result still works ok most the time. There's some cases where it still sucks, but then I just don't use those applications.
tbh I think so many programs base their ui on what looks fancy on github readme screenshots rather than what is practically beneficial for day-to-day users. But that's another rant for another day (and tbh to be fair like with zero marketing you can end up with zero users but still).
Sounds like the algorithm iterm2 and ghostty use would handle true color better than me, but since I'm meh on true color anyway i say good enough. But they should enable it by default!
The solution to this is to have color0 and color7 mean bg and fg, not black and white. Every color from 1 to 6 should be readable on the background color0. It’s a travesty how unreadable so many colors are on the bg on default palettes in terminals. Extra palette colors if you need more then 1-6 must be shoved elsewhere in the 256 not in the bright colors because applications expect them to be bright versions of color0-7 (exception: color8 “bright bg” works as dim fg, and color15 “bright fg” works as emphasis). But not all terminals let you configure the 256 palette that easily (and doing it through escape codes only works in one shell not through ssh tmux etc). Base16 gets this right but unfortunately did not catch on because it is trying to do tons of other stuff besides just solving the terminal problem. Tinted theming tried to be a successor but it has a similar large scope. 24 bit colors are not a solution because you lose the level of indirection offered by a palette.
If anything, mentioning Oklab is a plus in my book. I use it for all my Neovim color schemes and it was a corner stone for designing Neovim's default color scheme. Too bad it is somewhat in the middle and is too easy to miss.
Linking to a color picker that I highly suggest to use is also a big plus.
Even without accessibility constraints, I mostly avoid color (nvi, plain output). Color is fine as a hint, but tools should never depend on it. NO_COLOR is a simple and pragmatic default.
This is why iTerm's "minimum contrast" slider is great. Perhaps not every color in the theme will look quite as vibrant as originally intended, but at least you'll be able to read the text.
...I actually kinda dig the 256-color Solarized variant? It's not something I would actually use, the contrast could be better, but it also looks kinda pleasant.
Isn’t there a way to query the terminal for the actual RGB values of the palette entries? Or I think there’s a way to use actual RGB values in modern terminals, am I right?
You can say "give me this 24-bit color", which has fairly wide though not universal support, and the 216-color palette which is basically universal at this point is generally not customized to this extent so it's safe. The problem is you can't know anything about the contrast unless you also set the background color. For a full-blown TUI application like cmus it's okay to just override that, but for something like build output you can't.
No. And even then, the 16 colors won't tell you much. There's no support for anything remotely "context-based" like textmate grammars do. Certainly not to report the palette of the terminal, light/dark background, even support for 256/truecolor.
Would have made Chalk.js a lot easier to maintain.
"standard" terminals won't tell you what the values of the palette are. "modern" terminals will let you set some or all palette entries, and may let you specify colors directly.
The question, as always:
Do you know better than the user?
The only way to resolve this question is to design your application to work well with no colors at all, then to accept a complete palette from your user, preferably in a file, preferably in a file which can be separated/included from the main config file.
Then everyone can be satisfied at the cost of everyone having to think about the palette.
Others have mentioned terminal-specific minimum-contrast settings. I actually made a terminal agnostic tool, that amongst many other visual features, supports automatic contrast adjustment: https://tattoy.sh
I’m working on a CLI tool to decompose a URL into its parts and pretty print it. So this post is timed perfectly.
All modern terminals support 24-bit colour, so perhaps the right approach these days is to stop using legacy 16/256-colors entirely.
Last year I was working on some patches to modify the way that colour was handled in a terminal application. The maintainer was keen that the colours would be consistent for all the users of the application. Which meant using true colour I think. For awhile I was exploring what the fallback should be if that wasn’t available to the user and whether we could fall back to 256 colour or 16 colour for example. I never did finish that work, or decide whether having the colours be precisely defined by default was a good thing from an accessibility point of view.