Skip to main content

Theming

Every drawing call takes an IArlecchinoColor. Theme is the static accessor views read from, and ThemePalette is the object behind it. Views should pick a role, not a color: swapping the palette then restyles the whole application, chrome included.

Roles

RoleDefaultUsed for
Defaultterminal default foreground and backgroundOrdinary text
Headercrimson, boldScreen titles
TableHeaderbone, boldColumn headers
AccentboneText that stands out without being alarming
InfoashBox borders and structural lines
SecondaryashHints, footers, secondary text
Inputink on boneThe editable part of a text field
Caretblack on whiteThe symbol the caret stands on in a line of text
Selectionbone on hairlineThe cursor row of an unfocused pane, and what is selected in a line
ActivecrimsonSomething switched on or available
ActiveSelectionink on ashThe cursor row of the focused pane
Warningink on amberThe output line when it carries text
Errorbone on crimsonModal validation messages

Swapping the palette

builder.Services
.AddArlecchino()
.UseTheme(new ThemePalette
{
Header = new TermColor { Foreground = TerminalColor.BrightCyan, Style = TextStyle.Bold },
Selection = new TermColor { Background = TerminalColor.Blue },
});

ThemePalette properties are init-only and each has a default, so a partial palette is a valid one. Theme.Palette is assigned when ArlecchinoOptions is resolved from the container, which is why Theme.Header works from a view without anything plumbed through to it. Assigning Theme.Palette directly also works when there is no container at all.

The palette is process-wide

Theme.Palette and TerminalCapabilities.Color are process-wide on purpose: that is what lets a view write Theme.Header with nothing passed to it. The price is that one process hosts one look — two hosts side by side share the palette and the color level, and the last one built wins. A test that changes either of them shares the change with whatever else is running, which is why the test host pins the color level as it builds.

The framework's own palette

The defaults are the harlequin mask in colors — crimson #C9382B, bone #EDE6D9, ink #141317 and the hairline #2E2B33 of the brand assets. An application gets them without asking; ThemePalette.Arlecchino is the same thing under a name, for saying so out loud:

builder.Services
.AddArlecchino()
.UseTheme(ThemePalette.Arlecchino);

The background is left to the terminal everywhere except the two cursor rows, which have to paint behind their text to be a selection at all. That is what makes it sit on a light terminal as readily as on a dark one — it colors the writing, not the screen.

Every entry names an exact color and a palette color, so a terminal without 24-bit shows the fallback its author picked rather than whatever the nearest-color arithmetic lands on. Crimson falls back to BrightRed, bone to BrightWhite, the hairline to BrightBlack. How that is expressed is on Colors.

Crimson is spent on one thing at a time: titles, Active, and Error. The cursor row is deliberately not crimson — a selection that looks like a failure is a screen you have to read twice:

RoleColorReads as
ActiveSelectionink on ash #8A8189Where the cursor is
Selectionbone on hairline #2E2B33Where it was, in the pane without focus
Warningink on amber #D08A2CWorth noticing — deprecated, drifted
Errorbone on crimson #C9382BSomething is wrong

The sixteen plain colors

What was the default before 2.0 is still there as ThemePalette.Basic — magenta titles, blue column headers, cyan borders, a green cursor row, and nothing exact behind any of them:

builder.Services.AddArlecchino().UseTheme(ThemePalette.Basic);

That is the whole of the way back. See Migrating to 2.0.

A palette for the terminal you landed on

A palette is written against a background nobody promised. PaletteForBackground is handed the color the terminal turned out to be and answers with the palette to wear:

builder.Services.AddArlecchino(options =>
options.PaletteForBackground = background => new ThemePalette
{
Header = new RgbTermColor
{
Foreground = Shade.Against(background, hue: 25, chroma: 0.16, contrast: 7),
Style = TextStyle.Bold,
},
Secondary = new RgbTermColor { Foreground = Shade.Against(background, hue: 25, chroma: 0.02, contrast: 3) },
Selection = new RgbTermColor { Background = Shade.Lifted(background, 0.06) },
});

It runs once, as the application starts and the terminal is asked what color it is, so it costs nothing per frame. A terminal that will not say — one that ignores the OSC 11 query, or a redirected run — leaves Theme exactly as it was given, which means the palette above is the improvement and the plain one is the floor. The arithmetic behind Shade is on Colors.

Writing a palette of your own

Start from the defaults and override what matters. Two rules keep a palette usable:

  • Say both colors. An Exact color with a palette color behind it degrades the way you chose.
  • Keep the background out of it except where a row has to read as selected. A palette that paints the screen fights every terminal it did not expect.
public static class Solarized
{
public static ThemePalette Palette { get; } = new()
{
Header = new TermColor
{
Foreground = TerminalColor.BrightYellow,
ExactForeground = new Rgb(0xB5, 0x89, 0x00),
Style = TextStyle.Bold,
},
Info = new TermColor
{
Foreground = TerminalColor.BrightBlack,
ExactForeground = new Rgb(0x58, 0x6E, 0x75),
},
};
}