Tango colors in the Linux console
I enjoy using zsh as my shell. Not mainly because of its advanced features (bash made large strides here in the last years to catch up!), but rather due to its community ecosystem
with tools such as oh my zsh that make extending your .zshrc
and including nice tools integrating with your dev tools really easy.
OK, I admit. I just installed it because I like the themes. But I quickly found that on the physical Linux console (tty) the default colors don’t have enough contrast. Dark blue on black (which is a staple in many themes) is just hard to look at.
So naturally I tried to figure out how to get the colors I know from the terminal (I use Tango colors in xfce-terminal) to the console. Turns out: It’s possible using ANSI escape codes which I found on this page here.
What is an ANSI escape code?
This is a bit of a history lesson, which you can skip if you don’t really care about that kind of thing.
In the really early days of computing, there was no standardized encoding for even the most basic characters like A-Z. Fortunately for us, in 1968, the American Standard Code for Information Interchange (ASCII) was created, which is still around today (e.g. UTF-8 is a superset of ASCII.)
The ASCII standard consisted of 7 Bit character codes. (As I said it was the stone age of computing, people hadn’t really settled on bytes having 8 bits.) Of those 128 characters, the first 32 were reserved control characters used for in-band signalling.
Among those were the NUL
character (today used as a universal record seperator), the BEL
character (which causes the computer too beep when encountered),
and the well-known carriage-return and line-feed characters (CR
, LF
) which nowadays indicate in platform-dependent combinations that a new line should be started.
Unfortunately 128 characters were simply not enough, especially in an international context. Fortunately, the world quickly moved to 8-bit characters, so people made use of that additional bit to extend ASCII with different kinds of code pages.
One family of such code pages was the ISO 8859 standard family, with its most commonly used ISO 8859-1 code page, also known as isolatin1 which implemented special characters found in West/Middle-European languages like French, Swedish or German.
But those extended code pages not only added additional characters, the people creating these standards also considered the 32 Control Characters in the ASCII standard to be not enough.
So ISO 8859-1 added 32 more control characters (hex 0x80
to 0x9F
) to allow for more complex in-band signalling. We call those the C1 control characters.
People generally considered having more control characters to be a good idea, but chose another implementation proposed by the ANSI (American National Standards Institute) in ANSI standard X3.64 (1983)
We take the ASCII symbol ESC
(hex 0x1B
) and add another character to specify one of the new escape characters.
So our escapes took up another character, but the escape sequences were also usable on 7 bit systems!
And on top of that your code page did not have to reserve the C1 control character range
(which later would have been used by the Windows Codepage 1252 that extended ISO 8859-1 with even more printable characters, by using this specific range.)
The first terminals to implement what was to become ANSI X3.64 were the DEC VT100 terminals, which were highly successful and led to rapid proliferation of the escape standard.
Nowadays, our Linux terminals generally are still largely VT100 compatible and support those ANSI escape sequences. Which is probably why you can still easily find a PDF scan of the VT100 Technical Manual from 1980 in surprisingly high quality.
ANSI escape sequences today
As written before, the ANSI escape sequences are an in-band signalling mechanism. This means that the sequences are part of the data we enter. We merely have to be able to print arbitrary character codes and can access the entire feature set.
Fortunately, our shells support this behaviour. For example, a POSIX shell allows you to use the hex-code echo "\x7"
to print out the BEL
character (hex 0x07
) causing your terminal to beep.
(Note the double quote ""
, if we use single quotes ''
the hex code will not be evaluated by the shell, and we would have to use echo -e
to let echo
do the same job.
So to print an ESC
character we can just write echo "\x1b"
or even the convenience alias echo "\e"
that is as well offered by the shell.
So… What can we use them for?
Very common is the Escape code for coloring text (in this case red):
<ESC>[0;31m
Where <ESC>
is the escape code hex 0x1B
and the rest are normal printable ASCII characters.
The standard calls <ESC>[
the Control Sequence Intro (CSI
, equivalent to the 1 byte CSI
character in C1 escape codes, unrelated to modern digital image enhancement methods) which signals to the terminal
that the following text should be interpreted as an implementation-defined control code.
The general invokation of a CSI command is as follows.
<ESC>[Ps;Ps;...X
Where each Ps
specifies a so-called selective parameter which invokes a “sub-function” for the specified escape code.
There may be an arbitrary number of selective parameters. Note that this is not really necessary as a chain of commands with only one selective parameter is generally equivalent.
You may just as well write:
<ESC>[PsX<ESC>[PsX...
for each Ps, and get the same result with only 2 characters overhead, and simplify the entire control logic. But hey, it was the 80s. Those bytes don’t grow on trees and it’s not like saving two characters per record ever caused disproportionally expensive problems down the road.
The final character X
specifies the actual command that should be executed. In our case X
= m
which means that this escape sequence affects the character attributes,
i.e. how the single printable characters following this sequence should from then on be printed.
In our case, we have to selective arguments 0
and 31
. The 0
means that all character attributes should be reset,
and the 31
means that we set the character foreground color to “red”.
You can find all of the color codes on the Wikipedia page for ANSI escape sequences.
And now we get to the point. The color is “red”, but how is “red” defined?
It turns out that indeed the color is indeed defined to refer to a “foreground red” (probably for historical reasons), but what “foreground red” (or any other color) is, is completely up to you!
For example if you want to set “background black” to 0xFF0000
, to feel like you are entering nuclear launch codes, nobody will stop you!
(From setting the colors that way, I mean, not from actually trying to launch nuclear weapons, I think there are laws against doing that.)
To define what “red” or whatever other color means we can use another escape:
<ESC>]PXRRGGBB
Note that this one is not a CSI
command (the bracket faces the other way.) which is why it does not have arguments delimited by semicolons or a final character.
Instead the sequence <ESC>]P
already defines that this is a color definition command, and therefore expects exactly 7 bytes as arguments.
The arguments mean:
X
: Definition of the color to modify as hex (0
-7
: foreground 0-7,8
-f
background 0-7)RR
,GG
,BB
: A hexadecimal 24 Bit RGB code as you would expect. Feel free to use one of the color picker tools on the web to find the hex code.
When I found this out, this immediately made clear why some legacy quirks are still in place, even though Linux now uses framebuffers for the terminal and could in theory display 24 bit Full Color. Yes, you could maybe add another escape code, that works with more than 16 colors, but this is not worth the effort, and if you break anything, it happens at such a low level.
Anyways, the only thing left now is to figure out the correct codes from the Tango scheme. I could have looked them up in the source to my graphical terminal, but instead I just looked at the config dialog and copied the pre-defined hex values there.
OK, got it, so give me color now!
Now that you sat through my lesson of history and weird low-level signalling, you may get your reward.
This is the list of commands to initialize a Tango color map and how they map to the color scheme as defined by VT100:
#!/bin/sh
echo -en "\e]P0000000" # black
echo -en "\e]P8555753" # dark gray ("light black")
echo -en "\e]P1CC0000" # dark red
echo -en "\e]P9EF2929" # red
echo -en "\e]P24E9A06" # dark green
echo -en "\e]PA8AE234" # green
echo -en "\e]P3C4A000" # brown ("dark yellow")
echo -en "\e]PBFCE94F" # yellow
echo -en "\e]P43465A4" # darkblue
echo -en "\e]PC739FCF" # blue
echo -en "\e]P575507B" # dark magenta
echo -en "\e]PDAD7FA8" # magenta
echo -en "\e]P606989A" # dark cyan
echo -en "\e]PE34E2E2" # cyan
echo -en "\e]P7D3D7CF" # light gray ("dark white")
echo -en "\e]PFEEEEEC" # white
clear
echo
Note the clear
and echo
on the bottom. The clear command actually prints the appropriate ANSI escape sequences to clear the terminal.
You may have configured getty to do that for you automatically and may be able to leave it out. The echo
just introduces a new line which looks neater.
This is a script, as it is easiest to work with those pesky escapes when you can just dump them to files, and not have the shell interpreting them.
The following instructions assume you saved it to set-terminal-colors.sh
.
We want the values to be printed to the terminal whenever the terminal is initialized.
On a Linux system this task is performed by the getty
daemon. Fortunately, the getty
daemon will always print
the contents of /etc/issue
which usually contains a set of escape sequences to clear the terminal and some string identifying
the system like Arch Linux \r (\l)
(btw, did i mention i use arch)
We do not want to have the shell interpreting the special characters, so we do a bit of a dance to dump everything directly into files:
bash set-terminal-colors.sh > /etc/issue.new
mv /etc/issue /etc/issue.old
cat /etc/issue.new /etc/issue.old > /etc/issue
Et voila! Just clean up the intermediate files or leave them to unbork your system if something did not work 😃