The 1970s: Terminal Colors
In contrast to high-resolution colors, which fit into a nicely uniform representation with a color space tag and three coordinates, terminal color formats from the 1970s and 1980s may not even have coordinates, only integer index values. ANSI escape codes support four different kinds of colors:
- The default foreground and background colors.
AnsiColor
, the 16 extended ANSI colors.- 8-bit indexed colors, which comprise
AnsiColor
,EmbeddedRgb
, andGrayGradient
. TrueColor
, 24-bit RGB colors.
Treating these color types uniformly requires one more:
Colorant
not only has variants forAnsiColor
,EmbeddedRgb
,GrayGradient
, andTrueColor
, but also for the default colorColorant::Default
and high-resolutionColor
.
Colorant::Default
represents either the default foreground or background
color, depending on context. Default colors, like ANSI colors, have no intrinsic
color values and hence are abstract. Unlike all other colors, which display
just the same in the foreground and background, they also are
context-sensitive. However, since prettypretty already needs to distinguish
between foreground and background colors for all other colors, having only one
context-free default color makes for less awkward use, a claim based on
experience.
AnsiColor
represents the 16 extended ANSI colors. They are eight base
colors—black, red, green, yellow, blue, magenta, cyan, and white—and their
bright variations—including bright black and bright white. ANSI colors have
names but no agreed-upon, intrinsic color values.
EmbeddedRgb
is a 6x6x6 RGB cube, i.e., every coordinate ranges from 0 to
5, inclusive. Xterm’s formula for converting to 24-bit RGB colors is widely
accepted. The color swatch below shows all 216 colors, with blue cycling
every column, green increasing every six columns, and red increasing every
row. It’s only the first of many color swatches throughout the
documentation.
GrayGradient
represents a 24-step gradient from almost black to almost
white. As for the embedded RGB cube, Xterm’s formula for converting to
24-bit RGB grays is widely accepted. The color swatch below illustrates the
gray gradient.
EightBitColor
combines the previous three colors into a single type for all
8-bit colors. Using the From
trait, instances of AnsiColor
,
EmbeddedRgb
, GrayGradient
, and u8
convert to EightBitColor
, which
in turn converts to u8
and Colorant
(see below). This enumeration exists
to conveniently and precisely represent 8-bit colors.
TrueColor
represents 24-bit RGB colors. Even in the early 1990s, when
24-bit graphic cards first became widely available, the term was a misnomer.
For example, Kodak’s Photo CD was
introduced at the same time and had a considerably wider gamut than the
device RGB of graphic cards. Alas, the term lives on. Terminal emulators
often advertise support for 16 million colors by setting the COLORTERM
environment variable to truecolor
.
Finally, Colorant
combines the just listed types with high-resolution colors
into a single coherent type for all kinds of colors. It does not model that
ANSI colors can appear as themselves and as 8-bit indexed colors. Prettypretty
used to include the corresponding wrapper type, but the wrapper didn’t enable
new functionality and mostly just got in the way. All color types implement
Into<Colorant>
and methods that require a colorant usually accept an impl
thereof, thus drastically reducing the need to manually wrap colors. A custom
conversion function for PyO3 effectively offers the same functionality in
Python, which also has Colorant.of()
.
Coding With Terminal Colors
The example code below illustrates how AnsiColor
, EmbeddedRgb
, and
GrayGradient
abstract over the underlying 8-bit index space while also
providing convenient access to RGB coordinates and gray levels. Embedded RGB and
gray gradient colors also nicely convert to true as well as high-resolution
colors, but ANSI colors do not.
#![allow(unused)] fn main() { extern crate prettypretty; use prettypretty::{Color, assert_same_color}; use prettypretty::error::OutOfBoundsError; use prettypretty::style::{AnsiColor, Colorant, EmbeddedRgb, GrayGradient, TrueColor}; let red = AnsiColor::BrightRed; assert_eq!(u8::from(red), 9); // What's the color value of ANSI red? We don't know! let purple = EmbeddedRgb::new(3, 1, 4)?; let index = 16 + 3 * 36 + 1 * 6 + 4 * 1; assert_eq!(index, 134); assert_eq!(u8::from(purple), index); assert_eq!(TrueColor::from(purple), TrueColor::new(175, 95, 215)); assert_same_color!(Color::from(purple), Color::from_24bit(175, 95, 215)); let gray = GrayGradient::new(18)?; let index = 232 + 18; assert_eq!(index, 250); assert_eq!(gray.level(), 18); assert_eq!(u8::from(gray), index); assert_eq!(TrueColor::from(gray), TrueColor::new(188, 188, 188)); assert_same_color!(Color::from(gray), Color::from_24bit(188, 188, 188)); let green = Colorant::from(71); assert!(matches!(green, Colorant::Embedded(_))); if let Colorant::Embedded(also_green) = green { assert_eq!(also_green[0], 1); assert_eq!(also_green[1], 3); assert_eq!(also_green[2], 1); assert_eq!(TrueColor::from(also_green), TrueColor::new(95, 175, 95)); assert_same_color!(Color::from(also_green), Color::from_24bit(95, 175, 95)); } else { unreachable!("green is an embedded RGB color") } Ok::<(), OutOfBoundsError>(()) }
The Python version is next. Even though Python does not support traits including conversion traits, most of the code is quite similar to the Rust version. It does help that the Python version adds a few methods that offer functionality equivalent to conversion traits.
from prettypretty.color import Color
from prettypretty.color.style import AnsiColor, Colorant, EmbeddedRgb, GrayGradient
from prettypretty.color.style import TrueColor
red = AnsiColor.BrightRed
assert red.to_8bit() == 9
# What's the color value of ANSI red? We don't know!
purple = EmbeddedRgb(3, 1, 4)
index = 16 + 3 * 36 + 1 * 6 + 4 * 1
assert index == 134
assert purple.to_8bit() == index
true_purple = TrueColor(*purple.to_24bit())
assert true_purple == TrueColor(175, 95, 215)
assert purple.to_color() == Color.from_24bit(175, 95, 215)
gray = GrayGradient(18)
index = 232 + 18
assert index == 250
assert gray.level() == 18
assert gray.to_8bit() == index
true_gray = TrueColor(*gray.to_24bit())
assert true_gray == TrueColor(188, 188, 188)
assert gray.to_color() == Color.from_24bit(188, 188, 188)
green = Colorant.of(71)
assert isinstance(green, Colorant.Embedded)
also_green = green[0] # The only valid index is 0!
assert also_green[0] == 1
assert also_green[1] == 3
assert also_green[2] == 1
true_green = TrueColor(*green.try_to_24bit())
assert true_green == TrueColor(95, 175, 95)
assert also_green.to_color() == Color.from_24bit(95, 175, 95)