Skip to main content

Colors

Theming is about the twelve roles a view draws with. This page is about what a role is made of, and what happens when the terminal cannot show it.

IArlecchinoColor

The contract is a single member:

public interface IArlecchinoColor
{
string Ansi { get; }
}

A style is anything that can produce an escape sequence. Two implementations ship.

TermColor

public sealed class TermColor : IArlecchinoColor
{
public TerminalColor Foreground { get; init; } = TerminalColor.Default;
public TerminalColor Background { get; init; } = TerminalColor.Default;
public Rgb? ExactForeground { get; init; }
public Rgb? ExactBackground { get; init; }
public TextStyle Style { get; init; } = TextStyle.None;
}

The two Exact colors are drawn where the terminal can do 24-bit, and the palette colors beside them are what a terminal without it gets. Setting both is how a palette says a brand color and still degrades to something its author chose:

Header = new TermColor
{
Foreground = TerminalColor.BrightRed,
ExactForeground = new Rgb(0xC9, 0x38, 0x2B),
Style = TextStyle.Bold,
};

TerminalColor is the sixteen-color ANSI set — Default, the eight base colors, and their Bright counterparts. TextStyle is described on Text and width.

RgbTermColor

Where an exact color is the point — a swatch, a chart, a syntax highlighter — reach for the other implementation:

private readonly Surface _surface;

_surface.WriteAt(row, column, "████", new RgbTermColor { Foreground = new Rgb(63, 169, 245) });

Foreground and Background are both optional, so a swatch is a background with no foreground. On a terminal without 24-bit it falls back to the nearest of the sixteen colors.

Keep chrome on Theme. Palette roles stay on the sixteen-color set because they have to look right on a terminal the application does not control.

Rgb

Rgb is a (Red, Green, Blue) record struct — what the color modal edits and hands back.

MemberMeaning
Hex#RRGGBB
TryParseHex(text, out rgb)Parses #RRGGBB and RRGGBB
FromHsl(hue, saturation, lightness)Builds from HSL, which is what a color picker moves along
ToHsl()The other direction

Working a color out against the background

A color is readable or not against the background it lands on, and a terminal's background is whatever its user picked. Oklch, Contrast and Shade are the arithmetic for that, and they are what PaletteForBackground is written with.

Oklch is the space where lightness, chroma and hue come apart, so a color can be made lighter without becoming a different color:

MemberMeaning
Oklch.Of(rgb)The color in (Lightness, Chroma, Hue), lightness and chroma from 0, hue in degrees
ToRgb()Back again, trimmed to what sRGB holds
FitsScreenWhether sRGB holds it at all, before trimming moves it
Trimmed()The same lightness and hue with the chroma cut back to what sRGB holds

Contrast is the ratio the accessibility guidelines are written in:

MemberMeaning
Contrast.Between(one, other)The ratio, from 1 to 21
Contrast.Luminance(color)The luminance those ratios are built on
Contrast.IsDark(background)Whether a background wants light text
Contrast.PivotWhere that turn is: luminance 0.179, not the half-way gray, that being where white and black are equally far off
Contrast.Reach(background)The most any color can reach against it

Shade solves for the color rather than checking one:

var background = TerminalCapabilities.Background ?? new Rgb(0x14, 0x13, 0x17);
var warning = Shade.Against(background, hue: 70, chroma: 0.14, contrast: 5.5);
MemberMeaning
Shade.Against(background, hue, chroma, contrast)The color at that hue which reaches the contrast, chroma cut only where sRGB cannot hold it
Shade.Against(background, sample, contrast)The same, taking the hue and chroma off a color you already drew
Shade.Lifted(background, step)A background a step away from this one, for panels and borders
Shade.Scaled(contrast, lowest, highest, background)A wanted contrast brought down to the room a background actually has
Shade.Pull(background)How much a background's own color should sway a design drawn for a gray one
Shade.Turn(background, anchor, offset)How far to turn a design's hues so they sit at an angle from the background rather than across from it

Two of these are worth knowing before you reach for them. A background near the middle can only reach about 5:1 in either direction, so a ladder of contrasts written for a near-black terminal flattens on it — Scaled brings the whole ladder down instead of clamping three of its steps onto white. And Pull is nothing under a chroma of 0.06 and everything over 0.16, which is not a guess: every terminal theme in wide use falls under the first number, while a background somebody picked a color for starts around the second. A design is therefore left exactly as drawn on every theme anyone actually runs.

Caching

Both implementations cache their escape sequence, and the frame writer compares styles by reference. Hold on to a style instance rather than building one per cell:

private static readonly RgbTermColor Swatch = new() { Background = new Rgb(63, 169, 245) };

What the terminal can actually do

TerminalCapabilities.Color decides how styles are emitted, and is detected once at startup:

LevelWhenEffect
TrueColorCOLORTERM is truecolor or 24bit, or WT_SESSION is setRgbTermColor and the Exact colors emit 24-bit sequences
Paletteanything elseBoth fall back to the nearest of the sixteen colors
NoneNO_COLOR is set, TERM=dumb, or the Windows console refused virtual terminal modeno style sequence is emitted at all, including the per-line reset

Set it yourself to override the guess:

TerminalCapabilities.Color = ColorSupport.Palette;

TerminalCapabilities.NearestPaletteColor(rgb) is the same conversion the fallback uses, available for your own rendering.

Windows

SystemTerminal turns on ENABLE_VIRTUAL_TERMINAL_PROCESSING when it starts. If the console refuses — an old conhost, say — color drops to None and the alternate-screen sequences are not written either, so the application degrades to plain text instead of spraying escape codes across it.

Testing what came out

NO_COLOR=1 is the fastest way to see whether a screen still reads without color, and the test host asserts on frames with the color level pinned so a run on a developer's machine and a run on CI produce the same text.