Color in Terminals
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:

Even the intro screens looked gorgeous in the terminal:

Unfortunately, many modern Linux shell sessions still look like this:

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' # WhiteSo, 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 EOSIf 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:

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"
fiThis 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.