Rich color in terminals is nothing new, and its use by the general public probably peaked in the mid-90’s with the use of dial-up bulletin board systems. Consider this screenshot of TradeWars 2002, where it’s easy to see key figures and messages:

TradeWars 2002

Even the intro screens looked gorgeous in the terminal:

TradeWars 2002 Intro

So why do most modern Linux shell sessions end up looking like this:

Boring Linux Shell

It’s like we’ve gone back in time to the 70’s when everyone was in front of a monochrome terminal!

To be fair, color is slowly starting to creep back in. The ‘dmesg’ command for example now uses color on a lot of Linux distributions to highlight important messages. Systemd uses color to show whether services are running or considered secure. There are whole communities dedicated to making Linux look great. But despite all of this, we’re nowhere near the amount of color that was used back in the BBS days. Why?

I think a big part has to do with how surprisingly difficult it is to casually use color in a modern shell. Most guides for colorizing bash ask you to do something like this:


# Reset
Color_Off='\033[0m'       # Text Reset

# Regular
Black='\033[0;30m'        # Black
Red='\033[0;31m'          # Red
Green='\033[0;32m'        # Green
Yellow='\033[0;33m'       # Yellow
Blue='\033[0;34m'         # Blue
Purple='\033[0;35m'       # Purple
Cyan='\033[0;36m'         # Cyan
White='\033[0;37m'        # White

# Bright
BBlack='\033[1;30m'       # Black
BRed='\033[1;31m'         # Red
BGreen='\033[1;32m'       # Green
BYellow='\033[1;33m'      # Yellow
BBlue='\033[1;34m'        # Blue
BPurple='\033[1;35m'      # Purple
BCyan='\033[1;36m'        # Cyan
BWhite='\033[1;37m'       # White

So, if you want to print “Hello world!” in red, you need to do something like this:

echo “${Red}Hello world!${Color_Off}”

That doesn’t look too bad, until you need to start combining a lot of different color combinations. Consider some of the messages from TradeWars 2002 we saw earlier. One line would look like this:

echo “${Green}You have ${BYellow}300${Green} credits and ${BYellow}20${Green} empty cargo holds.${Color_Off}”

You could make this a little easier to read by using ‘printf’ instead of ‘echo’:

printf “${Green}You have ${BYellow}%d${Green} credits and ${BYellow}%s${Green} empty cargo holds.${Color_Off}\n” “$credits” “$cargo_holds”

But this is very difficult for the mind to process. Different colors are varying lengths (‘red’ vs ‘yellow’), and it’s hard to visualize what the final string will actually look like once it’s rendered.

The EPIC IRC client has had a solution for this for a long time. It uses the concept of formatting codes similar to printf. Unlike printf, it actually has a lot of formatting codes for colors. Here’s what the formatting codes look like, taken directly from the EPIC documentation:


%k Black foreground
%K Grey foreground
%r Red foreground
%R Bright red foreground
%g Green foreground
%G Bright green foreground
%y Brown foreground
%Y Yellow foreground
%b Blue foreground
%B Bright blue foreground
%m / %p Magenta foreground
%M / %P Bright magenta foreground
%c Cyan foreground
%C Bright cyan foreground
%w White foreground
%W Bright white foreground
%F Turn blinking on
%f Turn blinking off
%n All colors turned off
%N Dont clear colors at EOS

If we were to use the EPIC formatting codes on the example TradeWars 2002 string, it gets simplified down to this:

%gYou have %Y300%g credits and %Y20%g empty cargo holds.%n

That’s much easier to read, and it’s easier to imagine what the final rendered string will look like in color.

Unfortunately, we can’t use codes like this with printf because a lot of those are in use already. For example, %c, which EPIC uses for ‘cyan’ is used by printf to designate a character.

To work around this, I’ve created a some new formatting code prefixes in printf:

  • \C for foreground color.
  • \B for background color.
  • \A to control terminal attributes.

From there, the codes follow the EPIC convention. Using the TradeWars 2002 string as an example again, it can now be formatted like this:

printf “\CgYou have \CY%d\Cg credits and \CY%d\Cg empty cargo holds.\Cn\n” “$credits” “$cargo_holds”

And it’s rendered like this:

Example Color Formatting with printf

Now it’s easy to format colors in bash with the built-in printf command. The strings in code are easy to read, are all of a uniform length, use a minimal amount of space, and imagining the rendered output isn’t too difficult. Here’s the patch:


--- bash/builtins/printf.def	2016-05-05 16:12:16.791135821 -0700
+++ bash/builtins/printf.def	2016-05-05 17:08:21.067931336 -0700
@@ -891,6 +891,72 @@
 	      *lenp = temp;
 	  }
 	break;
+
+	case 'C': // Foreground Color
+	        switch(*p) {
+		                case '0':
+		                case 't':
+		                case 'n': strcpy(cp, "\e[0m"); *lenp = 4; break; // Terminate, default
+		                case 'T': strcpy(cp, "OK"); *lenp = 2; break;  // Test
+		                case 'k': strcpy(cp, "\e[0;30m"); *lenp = 7; break; // Black
+		                case 'K': strcpy(cp, "\e[1;30m"); *lenp = 7; break; // Bright Black (Dark Grey)
+		                case 'r': strcpy(cp, "\e[0;31m"); *lenp = 7; break; // Red
+		                case 'R': strcpy(cp, "\e[1;31m"); *lenp = 7; break; // Bright Red
+		                case 'g': strcpy(cp, "\e[0;32m"); *lenp = 7; break; // Green
+		                case 'G': strcpy(cp, "\e[1;32m"); *lenp = 7; break; // Bright Green
+		                case 'y': strcpy(cp, "\e[0;33m"); *lenp = 7; break; // Yellow (Brown/Orange)
+		                case 'Y': strcpy(cp, "\e[1;33m"); *lenp = 7; break; // Bright Yellow (True Yellow)
+		                case 'b': strcpy(cp, "\e[0;34m"); *lenp = 7; break; // Blue
+		                case 'B': strcpy(cp, "\e[1;34m"); *lenp = 7; break; // Bright Blue
+		                case 'p': strcpy(cp, "\e[0;35m"); *lenp = 7; break; // Purple
+		                case 'P': strcpy(cp, "\e[1;35m"); *lenp = 7; break; // Bright Purple
+		                case 'c': strcpy(cp, "\e[0;36m"); *lenp = 7; break; // Cyan
+		                case 'C': strcpy(cp, "\e[1;36m"); *lenp = 7; break; // Bright Cyan
+		                case 'w': strcpy(cp, "\e[0;37m"); *lenp = 7; break; // White (Light Grey)
+		                case 'W': strcpy(cp, "\e[1;37m"); *lenp = 7; break; // Bright White
+		                default:
+		                        break;
+	        }
+	        p++;
+	        break;
+
+	case 'B': // Background Color
+	        switch(*p) {
+		                case '0':
+		                case 't':
+		                case 'n': strcpy(cp, "\e[0m"); *lenp = 4; break; // Terminate, default
+		                case 'T': strcpy(cp, "OK"); *lenp = 2; break;  // Test
+		                case 'k': strcpy(cp, "\e[40m"); *lenp = 5; break; // Black
+		                case 'r': strcpy(cp, "\e[41m"); *lenp = 5; break; // Red
+		                case 'g': strcpy(cp, "\e[42m"); *lenp = 5; break; // Green
+		                case 'y': strcpy(cp, "\e[43m"); *lenp = 5; break; // Yellow (Brown/Orange)
+		                case 'b': strcpy(cp, "\e[44m"); *lenp = 5; break; // Blue
+		                case 'p': strcpy(cp, "\e[45m"); *lenp = 5; break; // Purple
+		                case 'c': strcpy(cp, "\e[46m"); *lenp = 5; break; // Cyan
+		                case 'w': strcpy(cp, "\e[47m"); *lenp = 5; break; // White (Light Grey)
+		                default:
+		                        break;
+	        }
+	        p++;
+	        break;
+
+	case 'A': // Attributes
+	        switch(*p) {
+		                case '0':
+		                case 't':
+		                case 'n': strcpy(cp, "\e[0m"); *lenp = 4; break; // Terminate, default
+		                case 'T': strcpy(cp, "OK"); *lenp = 2; break;  // Test
+		                case 'h': strcpy(cp, "\e[1m"); *lenp = 4; break; // High Intensity (Bright)
+		                case 'u': strcpy(cp, "\e[4m"); *lenp = 4; break; // Underline
+		                case 'b': strcpy(cp, "\e[5m"); *lenp = 4; break; // Blink
+		                case 'i': strcpy(cp, "\e[7m"); *lenp = 4; break; // Invert
+		                case 'c': strcpy(cp, "\e[8m"); *lenp = 4; break; // Conceal
+		                default:
+		                        break;
+	        }
+	        p++;
+	        break;
+
 #endif

       case '\\':	/* \\ -> \ */

I don’t expect this standard to be used anywhere, but I’m sharing it here in case it’s useful. It doesn’t seem to break backwards compatibility or trigger color in scripts that aren’t aware of the color codes. The only case it might is if someone escapes a capital C, B, or A, but that seems like it’s very rare and I’ve never encountered it.

Scripts can also check if the color codes are available like this:


if [[ "$(printf "\CT")" == "OK" ]]; then
    printf "\CCCongratulations! \CcYou have the color codes enabled.\Cn\n"
else
    printf "No color codes available.\n"
fi

This is done in a backwards compatible way, since a regular printf will just display “CT” when given \CT, not “OK”.

As for the rest of the shell, well, Unicode has come to the rescue. All of CP437 is in Unicode, and that means you can now render those lovely BBS-era ANSI files directly to a modern terminal:

RRX Logo

Finally, all of the tools in one place to create beautiful, colorful shell environments without the need for specialized terminal emulators or fonts.