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 DOS-based 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

Unfortunately, many modern Linux shell sessions still look 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. Ironically, I find that it’s the frontier coding agents (such as Opencode, Claude, or Codex) pushing the envelope on TUI/modern terminal design these days. Who knew the world’s newest technology would be leveraging one of the oldest?

Python’s Textual framework also does a great job of mixing Unicode and modern terminals to maximum effect. There are also whole communities dedicated to making Linux look great.

But I’d like to focus on how there’s no real agreed shorthand for programmers to use to insert color into their code. If you’ve done any Bash scripting, you’ve probably run into something that looks like this:

define-ansi-colors.sh
# 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}"

You’ll find this kind of thing all over the place. The Dead Souls LPC mudlib provides something similar:

colors.c
write(
    "%^RED%^RED\t%%^^RED%%^^\t\t%^BOLD%^%%^^BOLD%%^^%%^^RED%%^^%^RESET%^\n"
    "%^GREEN%^GREEN\t%%^^GREEN%%^^\t%^BOLD%^%%^^BOLD%%^^%%^^GREEN%%^^%^RESET%^\n"
    "%^ORANGE%^ORANGE\t%%^^ORANGE%%^^\t%^BOLD%^%%^^BOLD%%^^%%^^ORANGE%%^^%^RESET%^\n"
    "%^YELLOW%^YELLOW\t%%^^YELLOW%%^^\t%^BOLD%^%%^^BOLD%%^^%%^^YELLOW%%^^%^RESET%^\n"
    "%^BLUE%^BLUE\t%%^^BLUE%%^^\t%^BOLD%^%%^^BOLD%%^^%%^^BLUE%%^^%^RESET%^\n"
    "%^CYAN%^CYAN\t%%^^CYAN%%^^\t%^BOLD%^%%^^BOLD%%^^%%^^CYAN%%^^%^RESET%^\n"
    "%^MAGENTA%^MAGENTA\t%%^^MAGENTA%%^^\t%^BOLD%^%%^^BOLD%%^^%%^^MAGENTA%%^^%^RESET%^\n"
    "%^BLACK%^BLACK\t%%^^BLACK%%^^\t%^BOLD%^%%^^BOLD%%^^%%^^BLACK%%^^%^RESET%^\n"
    "%^WHITE%^WHITE\t%%^^WHITE%%^^\t%^BOLD%^%%^^BOLD%%^^%%^^WHITE%%^^%^RESET%^\n"
    "%^BLACK%^B_RED%^B_RED\t\t\t%%^^B_RED%%^^%^RESET%^\n"
    "%^BLACK%^%^B_GREEN%^B_GREEN\t\t\t%%^^B_GREEN%%^^%^RESET%^\n"
    "%^BLACK%^%^B_ORANGE%^B_ORANGE\t\t%%^^B_ORANGE%%^^%^RESET%^\n"
    "%^BLACK%^%^B_YELLOW%^B_YELLOW\t\t%%^^B_YELLOW%%^^%^RESET%^\n"
    "%^BLACK%^%^B_BLUE%^B_BLUE\t\t\t%%^^B_BLUE%%^^%^RESET%^\n"
    "%^BLACK%^%^B_CYAN%^B_CYAN\t\t\t%%^^B_CYAN%%^^%^RESET%^\n"
    "%^BLACK%^%^B_MAGENTA%^B_MAGENTA\t\t%%^^B_MAGENTA%%^^%^RESET%^\n"
    "%^BOLD%^%^BLACK%^%^B_BLACK%^B_BLACK\t\t\t%%^^B_BLACK%%^^%^RESET%^\n"
    "%^BLACK%^%^B_WHITE%^B_WHITE\t\t\t%%^^B_WHITE%%^^%^RESET%^\n"
    "Special tags: %%^^BOLD%%^^ and %%^^FLASH%%^^ and %%^^RESET%%^^\n\n"
    "You can mix and match, for example: \n"
    "%%^^B_RED%%^^%%^^CYAN%%^^%%^^BOLD%%^^%%^^FLASH%%^^Foo!%%^^RESET%%^^:"
    "%^B_RED%^%^CYAN%^%^BOLD%^%^FLASH%^Foo!%^RESET%^"
);

Boy, that sure is easy to visualize, isn’t it?

That’s the problem with these “full name” color-variable schemes. They tend to break down visually once you use a lot of them together. Going back to the messages from TradeWars 2002 above, even a simple line trying to convey simple information becomes complicated:

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 still very difficult for the mind to process. Different color variables are varying lengths (‘red’ vs ‘yellow’), so 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 to this problem for a long time. It uses the concept of formatting codes similar to printf, but inclusive of formatting codes for colors. Here’s what the formatting codes look like, taken directly from the EPIC documentation:

EPIC Color Formatting Codes
%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 these 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 almost any printf implementation because a lot of those are in use already. For example, %c, which EPIC uses for ‘cyan’ is used by printf to display a character.

To work around this, I improvised a new form of printf-like shorthand that makes it easy to embed terminal attributes, as well as foreground and background colors. It uses three new collision-resistant escape codes:

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

These are then followed by a single letter representing the desired color or attribute. I’ve followed the EPIC convention and expanded it a little. For example, using TradeWars 2002 again:

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

Which ends up rendering like this:

Example Color Formatting with printf

All sixteen colors of the “ANSI BBS” palette are in this design:

Code Expansion Preview
\Cb \e[34m Hello World!
\Cc \e[36m Hello World!
\Cg \e[32m Hello World!
\Ck \e[30m Hello World!
\Cp \e[35m Hello World!
\Cr \e[31m Hello World!
\Cw \e[37m Hello World!
\Cy \e[33m Hello World!
Code Expansion Preview
\CB \e[94m Hello World!
\CC \e[96m Hello World!
\CG \e[92m Hello World!
\CK \e[90m Hello World!
\CP \e[95m Hello World!
\CR \e[91m Hello World!
\CW \e[97m Hello World!
\CY \e[93m Hello World!

As are all sixteen colors from the Commodore 64’s VIC-II, among others:

Code Expansion Preview
\Ca \e[38;5;8m Hello World!
\Cd \e[38;5;239m Hello World!
\Ce \e[38;5;94m Hello World!
\Cf \e[38;5;210m Hello World!
\Ch \e[38;5;249m Hello World!
\Ci \e[38;5;19m Hello World!
\Cj \e[38;5;227m Hello World!
\Cl \e[38;5;157m Hello World!
\Cm \e[38;5;90m Hello World!
\Co \e[38;5;166m Hello World!
\Cq \e[38;5;88m Hello World!
\Cs \e[38;5;116m Hello World!
\Cu \e[38;5;62m Hello World!
\Cv \e[38;5;129m Hello World!
\Cx \e[38;5;71m Hello World!
\Cz \e[38;5;208m Hello World!
Code Expansion Preview
\CA \e[38;5;244m Hello World!
\CD \e[38;5;240m Hello World!
\CE \e[38;5;130m Hello World!
\CF \e[38;5;198m Hello World!
\CH \e[38;5;252m Hello World!
\CI \e[38;5;39m Hello World!
\CJ \e[38;5;226m Hello World!
\CL \e[38;5;118m Hello World!
\CM \e[38;5;163m Hello World!
\CO \e[38;5;208m Hello World!
\CQ \e[38;5;124m Hello World!
\CS \e[38;5;51m Hello World!
\CU \e[38;5;45m Hello World!
\CV \e[38;5;177m Hello World!
\CX \e[38;5;154m Hello World!
\CZ \e[38;5;214m Hello World!

This scheme provides a rich 48-color retro-computing palette that can be called from any application with a printf-like syntax. The same keys work for backgrounds (\Br will provide a red background, for example). The following attributes are also supported:

Code Expansion Description
\Ab \e[5m Blink
\Ac \e[8m Conceal
\An \e[0m Clear All
\Ah \e[1m Bold / High Intensity
\Ai \e[7m Inverse
\At OK Expands to “OK”
\Au \e[4m Underline

Using the upper-case version of the code will cancel the attribute. For example: \AbHello World!\AB will display a blinking hello world and then turn blink off at the end.

Two universal keys are available no matter the escape code. Key “n” will always clear everything (ANSI \e[0m), whether \An, \Bn, or \Cn. The “t” key will expand to “OK” if these extensions are installed, so you can check for them ahead of time:

check-for-color-codes.sh
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”.

Patches#

To be clear, these are unsupported hackerware. I don’t expect this extended formatting standard to be used anywhere, but I’m sharing these patches in case someone finds them 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.

bash-printf-color.patch
--- bash-4.3/builtins/printf.def	2016-05-05 16:12:16.791135821 -0700
+++ bash-4.3/builtins/printf.def	2016-05-05 17:08:21.067931336 -0700
@@ -891,6 +891,149 @@
 	      *lenp = temp;
 	  }
 	break;
+
+	case 'C': // Foreground Color
+		switch(*p) {
+			case 'N':
+			case 'n': strcpy(cp, "\e[0m"); *lenp = 4; break; // Terminate, default
+			case 'T':
+			case 't': strcpy(cp, "OK"); *lenp = 2; break;  // Test
+			case 'b': strcpy(cp, "\e[34m"); *lenp = 5; break; // Blue
+			case 'B': strcpy(cp, "\e[94m"); *lenp = 5; break; // Bright Blue
+			case 'c': strcpy(cp, "\e[36m"); *lenp = 5; break; // Cyan
+			case 'C': strcpy(cp, "\e[96m"); *lenp = 5; break; // Bright Cyan
+			case 'g': strcpy(cp, "\e[32m"); *lenp = 5; break; // Green
+			case 'G': strcpy(cp, "\e[92m"); *lenp = 5; break; // Bright Green
+			case 'k': strcpy(cp, "\e[30m"); *lenp = 5; break; // Black
+			case 'K': strcpy(cp, "\e[90m"); *lenp = 5; break; // Bright Black
+			case 'p': strcpy(cp, "\e[35m"); *lenp = 5; break; // Purple
+			case 'P': strcpy(cp, "\e[95m"); *lenp = 5; break; // Bright Purple
+			case 'r': strcpy(cp, "\e[31m"); *lenp = 5; break; // Red
+			case 'R': strcpy(cp, "\e[91m"); *lenp = 5; break; // Bright Red
+			case 'w': strcpy(cp, "\e[37m"); *lenp = 5; break; // White
+			case 'W': strcpy(cp, "\e[97m"); *lenp = 5; break; // Bright White
+			case 'y': strcpy(cp, "\e[33m"); *lenp = 5; break; // Yellow
+			case 'Y': strcpy(cp, "\e[93m"); *lenp = 5; break; // Bright Yellow
+			case 'a': strcpy(cp, "\e[38;5;8m"); *lenp = 9; break; // Average Grey
+			case 'A': strcpy(cp, "\e[38;5;244m"); *lenp = 11; break; // Bright Average Grey
+			case 'd': strcpy(cp, "\e[38;5;239m"); *lenp = 11; break; // Dark Gray
+			case 'D': strcpy(cp, "\e[38;5;240m"); *lenp = 11; break; // Bright Dark Gray
+			case 'e': strcpy(cp, "\e[38;5;94m"); *lenp = 10; break; // Earth Brown
+			case 'E': strcpy(cp, "\e[38;5;130m"); *lenp = 11; break; // Bright Earth Brown
+			case 'f': strcpy(cp, "\e[38;5;210m"); *lenp = 11; break; // Fluorescent Pink
+			case 'F': strcpy(cp, "\e[38;5;198m"); *lenp = 11; break; // Bright Fluorescent Pink
+			case 'h': strcpy(cp, "\e[38;5;249m"); *lenp = 11; break; // High Grey
+			case 'H': strcpy(cp, "\e[38;5;252m"); *lenp = 11; break; // Bright High Grey
+			case 'i': strcpy(cp, "\e[38;5;19m"); *lenp = 10; break; // Indigo
+			case 'I': strcpy(cp, "\e[38;5;39m"); *lenp = 10; break; // Bright Indigo
+			case 'j': strcpy(cp, "\e[38;5;227m"); *lenp = 11; break; // Jigawatt Yellow
+			case 'J': strcpy(cp, "\e[38;5;226m"); *lenp = 11; break; // Bright Jigawatt Yellow
+			case 'l': strcpy(cp, "\e[38;5;157m"); *lenp = 11; break; // Lime
+			case 'L': strcpy(cp, "\e[38;5;118m"); *lenp = 11; break; // Bright Lime
+			case 'm': strcpy(cp, "\e[38;5;90m"); *lenp = 10; break; // Magenta
+			case 'M': strcpy(cp, "\e[38;5;163m"); *lenp = 11; break; // Bright Magenta
+			case 'o': strcpy(cp, "\e[38;5;166m"); *lenp = 11; break; // Orange
+			case 'O': strcpy(cp, "\e[38;5;208m"); *lenp = 11; break; // Bright Orange
+			case 'q': strcpy(cp, "\e[38;5;88m"); *lenp = 10; break; // Quartz Red
+			case 'Q': strcpy(cp, "\e[38;5;124m"); *lenp = 11; break; // Bright Quartz Red
+			case 's': strcpy(cp, "\e[38;5;116m"); *lenp = 11; break; // Sky
+			case 'S': strcpy(cp, "\e[38;5;51m"); *lenp = 10; break; // Bright Sky
+			case 'u': strcpy(cp, "\e[38;5;62m"); *lenp = 10; break; // Uber Blue
+			case 'U': strcpy(cp, "\e[38;5;45m"); *lenp = 10; break; // Bright Uber Blue
+			case 'v': strcpy(cp, "\e[38;5;129m"); *lenp = 11; break; // Violet
+			case 'V': strcpy(cp, "\e[38;5;177m"); *lenp = 11; break; // Bright Violet
+			case 'x': strcpy(cp, "\e[38;5;71m"); *lenp = 10; break; // Xanh Green
+			case 'X': strcpy(cp, "\e[38;5;154m"); *lenp = 11; break; // Bright Xanh Green
+			case 'z': strcpy(cp, "\e[38;5;208m"); *lenp = 11; break; // Zenith Orange
+			case 'Z': strcpy(cp, "\e[38;5;214m"); *lenp = 11; break; // Bright Zenith Orange
+			default:
+				break;
+		}
+		p++;
+		break;
+
+	case 'B': // Background Color
+		switch(*p) {
+			case 'N':
+			case 'n': strcpy(cp, "\e[0m"); *lenp = 4; break; // Terminate, default
+			case 'T':
+			case 't': strcpy(cp, "OK"); *lenp = 2; break;  // Test
+			case 'b': strcpy(cp, "\e[44m"); *lenp = 5; break; // Blue
+			case 'B': strcpy(cp, "\e[104m"); *lenp = 6; break; // Bright Blue
+			case 'c': strcpy(cp, "\e[46m"); *lenp = 5; break; // Cyan
+			case 'C': strcpy(cp, "\e[106m"); *lenp = 6; break; // Bright Cyan
+			case 'g': strcpy(cp, "\e[42m"); *lenp = 5; break; // Green
+			case 'G': strcpy(cp, "\e[102m"); *lenp = 6; break; // Bright Green
+			case 'k': strcpy(cp, "\e[40m"); *lenp = 5; break; // Black
+			case 'K': strcpy(cp, "\e[100m"); *lenp = 6; break; // Bright Black
+			case 'p': strcpy(cp, "\e[45m"); *lenp = 5; break; // Purple
+			case 'P': strcpy(cp, "\e[105m"); *lenp = 6; break; // Bright Purple
+			case 'r': strcpy(cp, "\e[41m"); *lenp = 5; break; // Red
+			case 'R': strcpy(cp, "\e[101m"); *lenp = 6; break; // Bright Red
+			case 'w': strcpy(cp, "\e[47m"); *lenp = 5; break; // White
+			case 'W': strcpy(cp, "\e[107m"); *lenp = 6; break; // Bright White
+			case 'y': strcpy(cp, "\e[43m"); *lenp = 5; break; // Yellow
+			case 'Y': strcpy(cp, "\e[103m"); *lenp = 6; break; // Bright Yellow
+			case 'a': strcpy(cp, "\e[48;5;8m"); *lenp = 9; break; // Average Grey
+			case 'A': strcpy(cp, "\e[48;5;244m"); *lenp = 11; break; // Bright Average Grey
+			case 'd': strcpy(cp, "\e[48;5;239m"); *lenp = 11; break; // Dark Gray
+			case 'D': strcpy(cp, "\e[48;5;240m"); *lenp = 11; break; // Bright Dark Gray
+			case 'e': strcpy(cp, "\e[48;5;94m"); *lenp = 10; break; // Earth Brown
+			case 'E': strcpy(cp, "\e[48;5;130m"); *lenp = 11; break; // Bright Earth Brown
+			case 'f': strcpy(cp, "\e[48;5;210m"); *lenp = 11; break; // Fluorescent Pink
+			case 'F': strcpy(cp, "\e[48;5;198m"); *lenp = 11; break; // Bright Fluorescent Pink
+			case 'h': strcpy(cp, "\e[48;5;249m"); *lenp = 11; break; // High Grey
+			case 'H': strcpy(cp, "\e[48;5;252m"); *lenp = 11; break; // Bright High Grey
+			case 'i': strcpy(cp, "\e[48;5;19m"); *lenp = 10; break; // Indigo
+			case 'I': strcpy(cp, "\e[48;5;39m"); *lenp = 10; break; // Bright Indigo
+			case 'j': strcpy(cp, "\e[48;5;227m"); *lenp = 11; break; // Jigawatt Yellow
+			case 'J': strcpy(cp, "\e[48;5;226m"); *lenp = 11; break; // Bright Jigawatt Yellow
+			case 'l': strcpy(cp, "\e[48;5;157m"); *lenp = 11; break; // Lime
+			case 'L': strcpy(cp, "\e[48;5;118m"); *lenp = 11; break; // Bright Lime
+			case 'm': strcpy(cp, "\e[48;5;90m"); *lenp = 10; break; // Magenta
+			case 'M': strcpy(cp, "\e[48;5;163m"); *lenp = 11; break; // Bright Magenta
+			case 'o': strcpy(cp, "\e[48;5;166m"); *lenp = 11; break; // Orange
+			case 'O': strcpy(cp, "\e[48;5;208m"); *lenp = 11; break; // Bright Orange
+			case 'q': strcpy(cp, "\e[48;5;88m"); *lenp = 10; break; // Quartz Red
+			case 'Q': strcpy(cp, "\e[48;5;124m"); *lenp = 11; break; // Bright Quartz Red
+			case 's': strcpy(cp, "\e[48;5;116m"); *lenp = 11; break; // Sky
+			case 'S': strcpy(cp, "\e[48;5;51m"); *lenp = 10; break; // Bright Sky
+			case 'u': strcpy(cp, "\e[48;5;62m"); *lenp = 10; break; // Uber Blue
+			case 'U': strcpy(cp, "\e[48;5;45m"); *lenp = 10; break; // Bright Uber Blue
+			case 'v': strcpy(cp, "\e[48;5;129m"); *lenp = 11; break; // Violet
+			case 'V': strcpy(cp, "\e[48;5;177m"); *lenp = 11; break; // Bright Violet
+			case 'x': strcpy(cp, "\e[48;5;71m"); *lenp = 10; break; // Xanh Green
+			case 'X': strcpy(cp, "\e[48;5;154m"); *lenp = 11; break; // Bright Xanh Green
+			case 'z': strcpy(cp, "\e[48;5;208m"); *lenp = 11; break; // Zenith Orange
+			case 'Z': strcpy(cp, "\e[48;5;214m"); *lenp = 11; break; // Bright Zenith Orange
+			default:
+				break;
+		}
+		p++;
+		break;
+
+	case 'A': // Attributes
+		switch(*p) {
+			case 'N':
+			case 'n': strcpy(cp, "\e[0m"); *lenp = 4; break; // Terminate, default
+			case 'T':
+			case 't': strcpy(cp, "OK"); *lenp = 2; break;  // Test
+			case 'h': strcpy(cp, "\e[1m"); *lenp = 4; break; // Bold / High Intensity
+			case 'H': strcpy(cp, "\e[22m"); *lenp = 5; break; // Disable Bold, et al
+			case 'u': strcpy(cp, "\e[4m"); *lenp = 4; break; // Underline
+			case 'U': strcpy(cp, "\e[24m"); *lenp = 5; break; // Disable Underline
+			case 'b': strcpy(cp, "\e[5m"); *lenp = 4; break; // Blink
+			case 'B': strcpy(cp, "\e[25m"); *lenp = 5; break; // Disable Blink
+			case 'i': strcpy(cp, "\e[7m"); *lenp = 4; break; // Invert
+			case 'I': strcpy(cp, "\e[27m"); *lenp = 5; break; // Disable Invert
+			case 'c': strcpy(cp, "\e[8m"); *lenp = 4; break; // Conceal
+			case 'C': strcpy(cp, "\e[28m"); *lenp = 5; break; // Disable Conceal
+			default:
+				break;
+		}
+		p++;
+		break;
+
 #endif
 	
       case '\\':	/* \\ -> \ */
coreutils-printf-color.patch
--- coreutils/src/printf.c  2026-01-21 20:51:40.000000000 +0700
+++ coreutils/src/printf.c  2026-05-20 06:12:42.588110152 +0700
@@ -168,6 +168,153 @@
 STRTOX (uintmax_t,   vstrtoumax, strtoumax (s, &end, 0))
 STRTOX (long double, vstrtold,   cl_strtold (s, &end))
 
+/* Output color */
+static void
+print_esc_color (char *p) {
+
+#define putansi(x) fputs("\e["x"m",stdout)
+
+if (*p == 'C') {
+   switch (*++p) {     // Foreground Color
+       case 'N':
+       case 'n':   putansi("0");       break;  // Terminate, default
+       case 'T':
+       case 't':   fputs("OK",stdout);
+       case 'b':   putansi("34");  break;  // Blue
+       case 'B':   putansi("94");  break;  // Bright Blue
+       case 'c':   putansi("36");  break;  // Cyan
+       case 'C':   putansi("96");  break;  // Bright Cyan
+       case 'g':   putansi("32");  break;  // Green
+       case 'G':   putansi("92");  break;  // Bright Green
+       case 'k':   putansi("30");  break;  // Black
+       case 'K':   putansi("90");  break;  // Bright Black
+       case 'p':   putansi("35");  break;  // Purple
+       case 'P':   putansi("95");  break;  // Bright Purple
+       case 'r':   putansi("31");  break;  // Red
+       case 'R':   putansi("91");  break;  // Bright Red
+       case 'w':   putansi("37");  break;  // White
+       case 'W':   putansi("97");  break;  // Bright White
+       case 'y':   putansi("33");  break;  // Yellow
+       case 'Y':   putansi("93");  break;  // Bright Yellow
+       case 'a':   putansi("38;5;8");  break;  // Average Grey
+       case 'A':   putansi("38;5;244");  break;  // Bright Average Grey
+       case 'd':   putansi("38;5;239");  break;  // Dark Gray
+       case 'D':   putansi("38;5;240");  break;  // Bright Dark Gray
+       case 'e':   putansi("38;5;94");  break;  // Earth Brown
+       case 'E':   putansi("38;5;130");  break;  // Bright Earth Brown
+       case 'f':   putansi("38;5;210");  break;  // Fluorescent Pink
+       case 'F':   putansi("38;5;198");  break;  // Bright Fluorescent Pink
+       case 'h':   putansi("38;5;249");  break;  // High Grey
+       case 'H':   putansi("38;5;252");  break;  // Bright High Grey
+       case 'i':   putansi("38;5;19");  break;  // Indigo
+       case 'I':   putansi("38;5;39");  break;  // Bright Indigo
+       case 'j':   putansi("38;5;227");  break;  // Jigawatt Yellow
+       case 'J':   putansi("38;5;226");  break;  // Bright Jigawatt Yellow
+       case 'l':   putansi("38;5;157");  break;  // Lime
+       case 'L':   putansi("38;5;118");  break;  // Bright Lime
+       case 'm':   putansi("38;5;90");  break;  // Magenta
+       case 'M':   putansi("38;5;163");  break;  // Bright Magenta
+       case 'o':   putansi("38;5;166");  break;  // Orange
+       case 'O':   putansi("38;5;208");  break;  // Bright Orange
+       case 'q':   putansi("38;5;88");  break;  // Quartz Red
+       case 'Q':   putansi("38;5;124");  break;  // Bright Quartz Red
+       case 's':   putansi("38;5;116");  break;  // Sky
+       case 'S':   putansi("38;5;51");  break;  // Bright Sky
+       case 'u':   putansi("38;5;62");  break;  // Uber Blue
+       case 'U':   putansi("38;5;45");  break;  // Bright Uber Blue
+       case 'v':   putansi("38;5;129");  break;  // Violet
+       case 'V':   putansi("38;5;177");  break;  // Bright Violet
+       case 'x':   putansi("38;5;71");  break;  // Xanh Green
+       case 'X':   putansi("38;5;154");  break;  // Bright Xanh Green
+       case 'z':   putansi("38;5;208");  break;  // Zenith Orange
+       case 'Z':   putansi("38;5;214");  break;  // Bright Zenith Orange
+       default:
+           break;
+   }
+}
+
+else if (*p == 'B') {
+   switch (*++p) {     // Background Color
+       case 'N':
+       case 'n':   putansi("0");       break;  // Terminate, default
+       case 'T':
+       case 't':   fputs("OK",stdout);
+       case 'b':   putansi("44");  break;  // Blue
+       case 'B':   putansi("104");  break;  // Bright Blue
+       case 'c':   putansi("46");  break;  // Cyan
+       case 'C':   putansi("106");  break;  // Bright Cyan
+       case 'g':   putansi("42");  break;  // Green
+       case 'G':   putansi("102");  break;  // Bright Green
+       case 'k':   putansi("40");  break;  // Black
+       case 'K':   putansi("100");  break;  // Bright Black
+       case 'p':   putansi("45");  break;  // Purple
+       case 'P':   putansi("105");  break;  // Bright Purple
+       case 'r':   putansi("41");  break;  // Red
+       case 'R':   putansi("101");  break;  // Bright Red
+       case 'w':   putansi("47");  break;  // White
+       case 'W':   putansi("107");  break;  // Bright White
+       case 'y':   putansi("43");  break;  // Yellow
+       case 'Y':   putansi("103");  break;  // Bright Yellow
+       case 'a':   putansi("48;5;8");  break;  // Average Grey
+       case 'A':   putansi("48;5;244");  break;  // Bright Average Grey
+       case 'd':   putansi("48;5;239");  break;  // Dark Gray
+       case 'D':   putansi("48;5;240");  break;  // Bright Dark Gray
+       case 'e':   putansi("48;5;94");  break;  // Earth Brown
+       case 'E':   putansi("48;5;130");  break;  // Bright Earth Brown
+       case 'f':   putansi("48;5;210");  break;  // Fluorescent Pink
+       case 'F':   putansi("48;5;198");  break;  // Bright Fluorescent Pink
+       case 'h':   putansi("48;5;249");  break;  // High Grey
+       case 'H':   putansi("48;5;252");  break;  // Bright High Grey
+       case 'i':   putansi("48;5;19");  break;  // Indigo
+       case 'I':   putansi("48;5;39");  break;  // Bright Indigo
+       case 'j':   putansi("48;5;227");  break;  // Jigawatt Yellow
+       case 'J':   putansi("48;5;226");  break;  // Bright Jigawatt Yellow
+       case 'l':   putansi("48;5;157");  break;  // Lime
+       case 'L':   putansi("48;5;118");  break;  // Bright Lime
+       case 'm':   putansi("48;5;90");  break;  // Magenta
+       case 'M':   putansi("48;5;163");  break;  // Bright Magenta
+       case 'o':   putansi("48;5;166");  break;  // Orange
+       case 'O':   putansi("48;5;208");  break;  // Bright Orange
+       case 'q':   putansi("48;5;88");  break;  // Quartz Red
+       case 'Q':   putansi("48;5;124");  break;  // Bright Quartz Red
+       case 's':   putansi("48;5;116");  break;  // Sky
+       case 'S':   putansi("48;5;51");  break;  // Bright Sky
+       case 'u':   putansi("48;5;62");  break;  // Uber Blue
+       case 'U':   putansi("48;5;45");  break;  // Bright Uber Blue
+       case 'v':   putansi("48;5;129");  break;  // Violet
+       case 'V':   putansi("48;5;177");  break;  // Bright Violet
+       case 'x':   putansi("48;5;71");  break;  // Xanh Green
+       case 'X':   putansi("48;5;154");  break;  // Bright Xanh Green
+       case 'z':   putansi("48;5;208");  break;  // Zenith Orange
+       case 'Z':   putansi("48;5;214");  break;  // Bright Zenith Orange
+       default:
+           break;
+   }
+}
+
+else if (*p == 'A') {
+   switch (*++p) {     // Attributes
+       case 'N':
+       case 'n':   putansi("0");       break;  // Terminate, default
+       case 'T':
+       case 't':   fputs("OK",stdout);
+       case 'h':   putansi("1");       break;  // High Intensity (Bright)
+       case 'H':   putansi("22");       break;  // Disable High Intensity (Bright)
+       case 'u':   putansi("4");       break;  // Underline
+       case 'U':   putansi("24");       break;  // Disable Underline
+       case 'b':   putansi("5");       break;  // Blink
+       case 'B':   putansi("25");       break;  // Disable Blink
+       case 'i':   putansi("7");       break;  // Invert
+       case 'I':   putansi("27");       break;  // Disable Invert
+       case 'c':   putansi("8");       break;  // Conceal
+       case 'C':   putansi("28");       break;  // Disable Conceal
+       default:
+           break;
+   }
+}
+
+}
+
 /* Output a single-character \ escape.  */
 
 static void
@@ -245,6 +392,10 @@
     }
   else if (*p && strchr ("\"\\abcefnrtv", *p))
     print_esc_char (*p++);
+  else if (*p && strchr ("ABC", *p)) {
+    print_esc_color (p);
+    p += 2;
+  }
   else if (*p == 'u' || *p == 'U')
     {
       char esc_char = *p;
fluffos-printf-color.patch
--- fluff/src/compiler/internal/lex.cc	2023-12-26 16:05:54.542048545 +0700
+++ fluff/src/compiler/internal/lex.cc	2023-12-26 16:52:11.006148297 +0700
@@ -2748,6 +2748,139 @@
           case LEX_EOF:
             lexerror("End of file in string");
             return YYEOF;
+	  case 'C': // Foreground Color
+	    switch (*outp++) {
+              case 'N':
+              case 'n': strcpy((char *)to, "\e[0m"); to += 4; break; // Terminate, default
+              case 'T':
+              case 't': strcpy((char *)to, "OK"); to += 2; break; // Test
+              case 'b': strcpy((char *)to, "\e[34m"); to += 5; break; // Blue
+              case 'B': strcpy((char *)to, "\e[94m"); to += 5; break; // Bright Blue
+              case 'c': strcpy((char *)to, "\e[36m"); to += 5; break; // Cyan
+              case 'C': strcpy((char *)to, "\e[96m"); to += 5; break; // Bright Cyan
+              case 'g': strcpy((char *)to, "\e[32m"); to += 5; break; // Green
+              case 'G': strcpy((char *)to, "\e[92m"); to += 5; break; // Bright Green
+              case 'k': strcpy((char *)to, "\e[30m"); to += 5; break; // Black
+              case 'K': strcpy((char *)to, "\e[90m"); to += 5; break; // Bright Black
+              case 'p': strcpy((char *)to, "\e[35m"); to += 5; break; // Purple
+              case 'P': strcpy((char *)to, "\e[95m"); to += 5; break; // Bright Purple
+              case 'r': strcpy((char *)to, "\e[31m"); to += 5; break; // Red
+              case 'R': strcpy((char *)to, "\e[91m"); to += 5; break; // Bright Red
+              case 'w': strcpy((char *)to, "\e[37m"); to += 5; break; // White
+              case 'W': strcpy((char *)to, "\e[97m"); to += 5; break; // Bright White
+              case 'y': strcpy((char *)to, "\e[33m"); to += 5; break; // Yellow
+              case 'Y': strcpy((char *)to, "\e[93m"); to += 5; break; // Bright Yellow
+              case 'a': strcpy((char *)to, "\e[38;5;8m"); to += 9; break; // Average Grey
+              case 'A': strcpy((char *)to, "\e[38;5;244m"); to += 11; break; // Bright Average Grey
+              case 'd': strcpy((char *)to, "\e[38;5;239m"); to += 11; break; // Dark Gray
+              case 'D': strcpy((char *)to, "\e[38;5;240m"); to += 11; break; // Bright Dark Gray
+              case 'e': strcpy((char *)to, "\e[38;5;94m"); to += 10; break; // Earth Brown
+              case 'E': strcpy((char *)to, "\e[38;5;130m"); to += 11; break; // Bright Earth Brown
+              case 'f': strcpy((char *)to, "\e[38;5;210m"); to += 11; break; // Fluorescent Pink
+              case 'F': strcpy((char *)to, "\e[38;5;198m"); to += 11; break; // Bright Fluorescent Pink
+              case 'h': strcpy((char *)to, "\e[38;5;249m"); to += 11; break; // High Grey
+              case 'H': strcpy((char *)to, "\e[38;5;252m"); to += 11; break; // Bright High Grey
+              case 'i': strcpy((char *)to, "\e[38;5;19m"); to += 10; break; // Indigo
+              case 'I': strcpy((char *)to, "\e[38;5;39m"); to += 10; break; // Bright Indigo
+              case 'j': strcpy((char *)to, "\e[38;5;227m"); to += 11; break; // Jigawatt Yellow
+              case 'J': strcpy((char *)to, "\e[38;5;226m"); to += 11; break; // Bright Jigawatt Yellow
+              case 'l': strcpy((char *)to, "\e[38;5;157m"); to += 11; break; // Lime
+              case 'L': strcpy((char *)to, "\e[38;5;118m"); to += 11; break; // Bright Lime
+              case 'm': strcpy((char *)to, "\e[38;5;90m"); to += 10; break; // Magenta
+              case 'M': strcpy((char *)to, "\e[38;5;163m"); to += 11; break; // Bright Magenta
+              case 'o': strcpy((char *)to, "\e[38;5;166m"); to += 11; break; // Orange
+              case 'O': strcpy((char *)to, "\e[38;5;208m"); to += 11; break; // Bright Orange
+              case 'q': strcpy((char *)to, "\e[38;5;88m"); to += 10; break; // Quartz Red
+              case 'Q': strcpy((char *)to, "\e[38;5;124m"); to += 11; break; // Bright Quartz Red
+              case 's': strcpy((char *)to, "\e[38;5;116m"); to += 11; break; // Sky
+              case 'S': strcpy((char *)to, "\e[38;5;51m"); to += 10; break; // Bright Sky
+              case 'u': strcpy((char *)to, "\e[38;5;62m"); to += 10; break; // Uber Blue
+              case 'U': strcpy((char *)to, "\e[38;5;45m"); to += 10; break; // Bright Uber Blue
+              case 'v': strcpy((char *)to, "\e[38;5;129m"); to += 11; break; // Violet
+              case 'V': strcpy((char *)to, "\e[38;5;177m"); to += 11; break; // Bright Violet
+              case 'x': strcpy((char *)to, "\e[38;5;71m"); to += 10; break; // Xanh Green
+              case 'X': strcpy((char *)to, "\e[38;5;154m"); to += 11; break; // Bright Xanh Green
+              case 'z': strcpy((char *)to, "\e[38;5;208m"); to += 11; break; // Zenith Orange
+              case 'Z': strcpy((char *)to, "\e[38;5;214m"); to += 11; break; // Bright Zenith Orange
+	      default: break;
+	    }
+	    break;
+	  case 'B': // Background Color
+	    switch (*outp++) {
+              case 'N':
+              case 'n': strcpy((char *)to, "\e[0m"); to += 4; break; // Terminate, default
+              case 'T':
+              case 't': strcpy((char *)to, "OK"); to += 2; break; // Test
+              case 'b': strcpy((char *)to, "\e[44m"); to += 5; break; // Blue
+              case 'B': strcpy((char *)to, "\e[104m"); to += 6; break; // Bright Blue
+              case 'c': strcpy((char *)to, "\e[46m"); to += 5; break; // Cyan
+              case 'C': strcpy((char *)to, "\e[106m"); to += 6; break; // Bright Cyan
+              case 'g': strcpy((char *)to, "\e[42m"); to += 5; break; // Green
+              case 'G': strcpy((char *)to, "\e[102m"); to += 6; break; // Bright Green
+              case 'k': strcpy((char *)to, "\e[40m"); to += 5; break; // Black
+              case 'K': strcpy((char *)to, "\e[100m"); to += 6; break; // Bright Black
+              case 'p': strcpy((char *)to, "\e[45m"); to += 5; break; // Purple
+              case 'P': strcpy((char *)to, "\e[105m"); to += 6; break; // Bright Purple
+              case 'r': strcpy((char *)to, "\e[41m"); to += 5; break; // Red
+              case 'R': strcpy((char *)to, "\e[101m"); to += 6; break; // Bright Red
+              case 'w': strcpy((char *)to, "\e[47m"); to += 5; break; // White
+              case 'W': strcpy((char *)to, "\e[107m"); to += 6; break; // Bright White
+              case 'y': strcpy((char *)to, "\e[43m"); to += 5; break; // Yellow
+              case 'Y': strcpy((char *)to, "\e[103m"); to += 6; break; // Bright Yellow
+              case 'a': strcpy((char *)to, "\e[48;5;8m"); to += 9; break; // Average Grey
+              case 'A': strcpy((char *)to, "\e[48;5;244m"); to += 11; break; // Bright Average Grey
+              case 'd': strcpy((char *)to, "\e[48;5;239m"); to += 11; break; // Dark Gray
+              case 'D': strcpy((char *)to, "\e[48;5;240m"); to += 11; break; // Bright Dark Gray
+              case 'e': strcpy((char *)to, "\e[48;5;94m"); to += 10; break; // Earth Brown
+              case 'E': strcpy((char *)to, "\e[48;5;130m"); to += 11; break; // Bright Earth Brown
+              case 'f': strcpy((char *)to, "\e[48;5;210m"); to += 11; break; // Fluorescent Pink
+              case 'F': strcpy((char *)to, "\e[48;5;198m"); to += 11; break; // Bright Fluorescent Pink
+              case 'h': strcpy((char *)to, "\e[48;5;249m"); to += 11; break; // High Grey
+              case 'H': strcpy((char *)to, "\e[48;5;252m"); to += 11; break; // Bright High Grey
+              case 'i': strcpy((char *)to, "\e[48;5;19m"); to += 10; break; // Indigo
+              case 'I': strcpy((char *)to, "\e[48;5;39m"); to += 10; break; // Bright Indigo
+              case 'j': strcpy((char *)to, "\e[48;5;227m"); to += 11; break; // Jigawatt Yellow
+              case 'J': strcpy((char *)to, "\e[48;5;226m"); to += 11; break; // Bright Jigawatt Yellow
+              case 'l': strcpy((char *)to, "\e[48;5;157m"); to += 11; break; // Lime
+              case 'L': strcpy((char *)to, "\e[48;5;118m"); to += 11; break; // Bright Lime
+              case 'm': strcpy((char *)to, "\e[48;5;90m"); to += 10; break; // Magenta
+              case 'M': strcpy((char *)to, "\e[48;5;163m"); to += 11; break; // Bright Magenta
+              case 'o': strcpy((char *)to, "\e[48;5;166m"); to += 11; break; // Orange
+              case 'O': strcpy((char *)to, "\e[48;5;208m"); to += 11; break; // Bright Orange
+              case 'q': strcpy((char *)to, "\e[48;5;88m"); to += 10; break; // Quartz Red
+              case 'Q': strcpy((char *)to, "\e[48;5;124m"); to += 11; break; // Bright Quartz Red
+              case 's': strcpy((char *)to, "\e[48;5;116m"); to += 11; break; // Sky
+              case 'S': strcpy((char *)to, "\e[48;5;51m"); to += 10; break; // Bright Sky
+              case 'u': strcpy((char *)to, "\e[48;5;62m"); to += 10; break; // Uber Blue
+              case 'U': strcpy((char *)to, "\e[48;5;45m"); to += 10; break; // Bright Uber Blue
+              case 'v': strcpy((char *)to, "\e[48;5;129m"); to += 11; break; // Violet
+              case 'V': strcpy((char *)to, "\e[48;5;177m"); to += 11; break; // Bright Violet
+              case 'x': strcpy((char *)to, "\e[48;5;71m"); to += 10; break; // Xanh Green
+              case 'X': strcpy((char *)to, "\e[48;5;154m"); to += 11; break; // Bright Xanh Green
+              case 'z': strcpy((char *)to, "\e[48;5;208m"); to += 11; break; // Zenith Orange
+              case 'Z': strcpy((char *)to, "\e[48;5;214m"); to += 11; break; // Bright Zenith Orange
+	      default: break;
+	    }
+	    break;
+	  case 'A': // Attributes
+	    switch (*outp++) {
+              case 'N':
+              case 'n': strcpy((char *)to, "\e[0m"); to += 4; break; // Terminate, default
+              case 'T':
+              case 't': strcpy((char *)to, "OK"); to += 2; break; // Test
+              case 'h': strcpy((char *)to, "\e[1m"); to += 4; break; // High Intensity (Bright)
+              case 'H': strcpy((char *)to, "\e[22m"); to += 5; break; // Disable High Intensity (Bright)
+              case 'u': strcpy((char *)to, "\e[4m"); to += 4; break; // Underline
+              case 'U': strcpy((char *)to, "\e[24m"); to += 5; break; // Disable Underline
+              case 'b': strcpy((char *)to, "\e[5m"); to += 4; break; // Blink
+              case 'B': strcpy((char *)to, "\e[25m"); to += 5; break; // Disable Blink
+              case 'i': strcpy((char *)to, "\e[7m"); to += 4; break; // Invert
+              case 'I': strcpy((char *)to, "\e[27m"); to += 5; break; // Disable Invert
+              case 'c': strcpy((char *)to, "\e[8m"); to += 4; break; // Conceal
+              case 'C': strcpy((char *)to, "\e[28m"); to += 5; break; // Disable Conceal
+	      default: break;
+	    }
+	    break;
           case 'n':
             *to++ = '\n';
             break;

Updates#

This page was originally written in 2020, and the original patches used to clear all attributes whenever low-intensity colors were specified. For example, printf "\Cr" would emit \e[0;31m. This was obviously bad since it would unceremoniously destroy any attributes before it.

Furthermore, earlier patches used a \e[1; prefix to low-intensity colors to display high-intensity in the way old DOS bulletin board systems would. This has been replaced with \e[91m and friends.

Finally, in closing, I’d like to say that I think Kovid Goyal’s argument is correct, and we should avoid using “bold as bright” wherever we can. It is the 21st Century after all. However, Kovid’s argument ignores one important point: \e[1m was shit on by developers decades ago, and we’re all paying the price for that now. We cannot, as they say, go back.