Monday, November 12, 2018

Why the Linux console has sixteen colors (SeaGL)

At the 2018 Seattle GNU/Linux Conference after-party, I gave a lightning talk about why the Linux console has only sixteen colors. Lightning talks are short, fun topics. I enjoyed giving the lightning talk, and the audience seemed into it, too. So I thought I'd share my lightning talk here. These are my slides in PNG format, with notes added:
Also, my entire presentation is under the CC-BY:
When you bring up a terminal window, or boot Linux into plain text mode, maybe you've wondered why the Linux console only has sixteen colors. No matter how awesome your graphics card, you only get these sixteen colors for text:
You can have eight background colors, and sixteen foreground colors. But why is that?
Remember that Linux is a PC operating system, so you have to go back to the early days of the IBM PC. Although the rules are the same for any Unix terminal.

The origins go back to CGA, the Color/Graphics Adapter from the earlier PC-compatible computers. This was a step up from the plain monochrome displays; as the name implies, monochrome could only display black or white. CGA could display a limited range of colors.

CGA supported mixing red (R), green (G) and blue (B) colors. In its simplest form, RGB is either "on" or "off." In this case, you can mix the RGB colors in 2×2×2=8 ways. So RGB=100 is Red, and RGB=010 is Green, and RGB=001 is Blue. And you can mix colors, like RGB=011 is cyan. This simple table shows the binary and decimal representations of RGB:
To double the number of colors, CGA added an extra bit called the "intensifier" bit. With the intensifier bit set, the red, green and blue colors would be set to their maximum values. Without the intensifier bit, each RGB value would be set to a "midrange" intensity. Let's represent that intensifier bit as an extra 1 or 0 in the binary color representation, as iRGB:
That means 0100 gives "red" and 1100 (with intensifier bit set) results in "bright red." Also, 0010 is "green" and 1010 is "bright green." And 0000 is "black," but 1000 is "bright black."

Oh wait, there's a problem. "Black" and "bright black" are the same color, because there's no RGB value to intensify.

But we can solve that! CGA actually implemented a modified iRGB definition, using two intermediate values, at about one-third and two-thirds intensity. Most "normal" mode (0–7) colors used values at the two-thirds intensity. Translating from "normal" mode to "bright" mode, convert zero values to the one-third intensity, and two-thirds values to full intensity.

With that, you can represent all the colors in the rainbow: red, orange, yellow, blue, indigo, and violet. You can sort of fake the blue, indigo, and violet with the different "blue" shades.

Oops, we don't have orange! But we can fix that by assigning 0110 yellow a one-third green value that turned the color into orange, although most people saw it as brown.

Here's another iteration of the color table, using 0x0 to 0xF for the color range, with 0x5 and 0xA as the one-third and two-thirds intensities, respectively:
And that's how the Linux console got sixteen text colors! That's also why you'll often see "brown" labeled "yellow" in some references, because it started out as plain "yellow" before the intensifier bit. Similarly, you may also see "gray" represented as "bright black," because "gray" is really "black" with the intensifier bit set.

So let's look at the bit patterns. You have four bits for the foreground color, 0000 black to 1111 bright white:
And you have three bits for the background color, from 000 black to 111 white:
But why not four bits for the background color? That's because the final bit is reserved for a special attribute. With this attribute set, your text could blink on and off. The "Blink" bit was encoded at the end of the foreground and background bit-pattern:
That's a full byte! And that's why the Linux console has only sixteen colors; the Linux console inherits text mode colors from CGA, which encodes colors a full byte at a time.

It turns out the rules are the same for other Unix terminals, which also used eight bits to represent colors. But on other terminals, 0110 really was yellow, not orange or brown.


  1. Awesome write up on the subject. Any ideas why Linux console doesn't support the "new" true colors yet?

  2. I mean, when using framebuffer/drm, Linux doesn't use the graphics card's text rendering but renders them itself, right?

  3. Interesting,I hadn't tried that ANSI escape code before. That's 24-bit color, so each RGB color coordinate ranges from 00000000 (0) to 11111111 (256). BTW, that's equivalent color to the web color space, because 11111111 (binary) is equivalent to FF (hex). In 24-bit color, DC143C is a strong red ("crimson") and B22222 is a deep red ("firebrick") and 8B0000 is a dark red ("darkred") and FF0000 is a bright red ("red").

    My understanding is when Linux is outputting text directly to the screen, it's not doing anything special. It's letting the hardware do the job of drawing the characters. And that's where the CGA history comes in, because those sixteen colors (Bbbbffff) are defined at the hardware level. I suppose you could tell your video card or system BIOS (or UEFI) to use a different set of sixteen colors. But at some level, colors in plain text mode (CGA, but VGA is the same) are set using Bbbbffff.

    Your terminal in GNOME or KDE doesn't use plain text rendering; it uses the X Window environment. But it still has to emulate the sixteen colors. In fact, I had to change the color palette for the sixteen colors in my terminal to make the screenshot on slide 3; otherwise, the screenshot would have used slightly different colors. In GNOME Terminal, go to Preferences, click on the name of your profile (my default was "Unnamed") and click on the Colors tab. You can set the palette of the sixteen colors here (my default was Tango, which uses a yellow for iRGB=0110). You can tweak the colors, but it's still a sixteen color palette because GNOME Terminal emulates plain text mode (the command line).

  4. I have a really vague memory from when the framebuffer came in (around 2.2?) that Linux itself draws the characters.
    Still, I'm fairly sure that 24 bit colour has not been implemented on fbdev but I see no reason it couldn't be .

  5. I wonder what a modern equivalent of VGA text mode would look like?


Note: Only a member of this blog may post a comment.