---
title: "There are many themes, but this one is yours"
slug: there-are-many-themes-but-this-one-is-yours
url: https://listedarticles.com/articles/there-are-many-themes-but-this-one-is-yours
canonical_url: https://earendil.com/posts/system-theme/
content_type: blog_post
language: en
published_at: 2026-10-09T00:00:00.000Z
updated_at: 2026-10-09T14:41:22.925Z
authored_by: human
publisher: "Earendil"
publisher_url: https://earendil.com/
topics: ["Design", "User Experience", "Developer Tools"]
license: all-rights-reserved
word_count: 2390
reading_minutes: 10
citation: "Earendil. \"There are many themes, but this one is yours.\" 9 Oct 2026. https://earendil.com/posts/system-theme/ (all-rights-reserved)"
# The full text follows. The web page shows an extract and sends readers
# to the source above; quote the citation and link the canonical URL.
---

# There are many themes, but this one is yours

> Earendil explains Pi's new default system theme, which asks the terminal for its colors and builds Pi's theme from them, and why the hard part is contrast: perceived readability varies between people, so they also built a contrast survey to train a better contrast algorithm.

# There are many themes, but this one is yours

At Earendil, we want to build products that respect the choices of the people using them. So when we were refreshing Pi's themes, we wanted a theme that adapts to the terminal it runs in. Most people who spend their day in a terminal have picked their own themes for it. Why not make Pi reflect their choices?

The result is the new system theme, which is now Pi's default. It asks your terminal for its colors and builds Pi's theme from them. In this post we share how it works.

Before you read the rest of the post, we want to spoil something: it's all about contrast. Picking the colors in the terminal UI is a question of taste that the user has already answered. From there, all other decisions come down to one question: how much does a color stand out from the background it's on? If we get it wrong, we create a bad experience for the user.

But the catch is that contrast is a matter of perception and not something we can trivially measure.  Two people can look at the same pair of colors and disagree about whether it's readable.  We built a small [interactive survey you can take](https://contrastsurvey.earendil.com/) to show you how you perceive that contrast, and how it compares against other people.  You'll find details about that survey at the end of the post.

## Can you trust the ANSI palette?

Every terminal theme defines 16 ANSI colors, and the simplest way to match your terminal would be to use them directly. But there aren't really any rules to the ANSI palette. The original standard, [ECMA-48](https://ecma-international.org/publications-and-standards/standards/ecma-48/), was adopted in 1976. Its nearly identical American counterpart from 1979, ANSI X3.64, is where the name comes from. The standard names eight colors (black, red, green, yellow, blue, magenta, cyan and white), but doesn't say what they should look like or how they should relate to the background.

The eight bright variants aren't part of the standard. Quite a few terminals [rendered bold text in a brighter color](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit), which effectively gave them eight more colors. The codes to select bright colors directly were added later by IBM's aixterm and adopted by other terminals like [xterm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html).

Since the bright colors started out as bold text, we would assume that "bright" was meant to stand out more. And since early terminals mostly showed light text on a dark screen, brighter also meant more contrast. We looked at the more than 460 themes that come with Ghostty, and today this only sort of holds. In dark themes, the bright variant has more contrast in about 60% of cases. In light themes, it's only about a quarter, since bright usually still means lighter, which on a light background means less contrast. Individual themes don't agree either: in Gruvbox Dark, bright blue has more contrast than blue, in Catppuccin Mocha it has less, and in Tokyo Night they are the same color. The contrast against the background also varies a lot. We measured it with the [WCAG 2 contrast ratio](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html), which compares the luminance of two colors and ranges from 1:1 (no contrast) to 21:1 (black on white). Normal text should reach at least 4.5:1, and large text and UI elements 3:1. Bright black, which a lot of software uses for secondary text, doesn't even reach 3:1 in most dark themes.

This isn't a flaw of the themes. The palette was made to color the output of simple applications, like a red error or a green success message, and many themes are designed to look good rather than to meet contrast minimums. But it makes it hard to build an accessible, more complex TUI on top of it. Pi has around 60 color roles, from body text and dim text to panels behind tool calls and red text on a red error panel. We wanted to use your colors and still guarantee that all of them stay readable.

## Contrast is all you need

Contrast is one of the most important aspects of color in user interfaces. If it is too low, people will have a hard time using your product. Contrast is mainly driven by lightness. Saturation affects it a little, but by far the most important factor is how light or dark a color is compared to the color behind it.

RGB, the way we usually write colors, doesn't have a lightness axis. It was made for machines to display colors on a monitor, not for humans to understand them. As a 3D shape, it is a neat cube with one axis per channel. But colors that are close to each other in this cube aren't necessarily colors humans would describe as similar. #0000ff and #00ff00, for example, both have one channel at full strength, but on white, the blue has a WCAG contrast ratio of 8.6:1 and the green only 1.4:1.

Perceptual color spaces like [OKLCH](https://bottosson.github.io/posts/oklab/) are built around human perception instead. Colors that are close to each other in OKLCH are also colors humans would describe as similar, and its axes are the ones humans use to describe color: lightness, chroma (how colorful a color is) and hue. Because it follows human perception rather than a monitor's hardware, its shape is a lot weirder than a cube.

With a lightness axis, the idea behind the system theme is simple: Pi decides the lightness of every color based on contrast requirements, and takes the hue and chroma from your terminal's palette.

## Lightness

To figure out what lightness each color needs, we wrote down every place in the UI where two colors meet. Every panel needs enough contrast with the terminal background to read as a separate area, but not so much that it distracts. Every foreground color needs enough contrast on every background it can appear on. An error message, for example, has to be readable on the background, on the selected row and on all three tool panels. In code, this is a list of rules:

```
const COLORS = ["accent", "success", "error", "warning"];
const SURFACES = ["background", "selectedBg", ...TOOL_PANELS];
{ token: "text", on: ["background"], level: "text" },
...each(COLORS, SURFACES, "readable"),
{ token: "dim", on: [...SURFACES, "customMessageBg"], level: "subtle" },
```
A contrast algorithm normally takes two colors and returns the contrast between them. Here we need the reverse: we know the background and how much contrast we want, and need the color. I have reversed contrast algorithms before, and you can find implementations for both [WCAG](https://github.com/dgtlntv/wcag-contrast-palette) and [perceptual contrast](https://github.com/dgtlntv/perceptual-contrast-palette) on GitHub. With a reversed algorithm, calculating the theme becomes a loop: starting with the panels, Pi calculates the lightness each color needs for each of its rules and takes the strictest one.

## The Algorithm

Our first prototype did exactly that, together with a review app in which we tuned the contrast minimums. The app can render Pi with any of the themes that come with Ghostty, so we could check a sample of very different themes to make sure the system holds up beyond the default one.

That prototype used a well known perceptual contrast algorithm and that reference implementation. We then used that against a large number of ghostty themes and ensured that it looked good against all the themes. We then did not want to ship that algorithm itself. We tried to use simpler measures but were unable to approximate the results. In the end we had a coding agent do the fitting. For each contrast level in the reference the agent ran the original algorithm on every gray background from white to black and recorded the lightness and fitted a polynomial to the results. It settled on a fifth degree polynomial which was found to stay close enough to the reference lightness. Pi now only ships with those coefficients.

The chart below shows those polynomials, one curve for each contrast level in the rules above. Along the bottom is the lightness of the surface a color is drawn on, and up the side the lightness the color needs on it. The diagonal is no contrast at all: a color exactly as light as its surface. The vertical lines are the surfaces: the background, and the panels, which Pi solves first on the background. Where a rule's curve crosses one of its surfaces, you can read off the lightness that rule needs there, and the strictest one wins. Drag the background to see how every color follows it.

We first convert the queried terminal colors from RGB into OKLCH and [OKHSL](https://bottosson.github.io/posts/colorpicker/). From that we get the original lightness L, chroma C and hue H as well as the saturation S relative to what sRGB can display at that lightness. Once the polynomial is evaluated against the lightness of the background. The resulting lightness is then not used as a direct replacement, but converted into an OKHSL lightness and a new color is computed using the original hue and adjusted saturation. A bell shaped saturation curve is applied. Strongest at the middle lightness and weaker towards black and white. Finally that is converted to OKLCH and Pi limits the original chroma so that H stays the same, L comes from the contrast rules and C becomes what the adjusted OKHSL produces. This is so that a pale pink for instance, when moved towards a darker shade, might otherwise make it too vivid. The adjustment ensures that it can never be more colorful than the original color.

## Hue & chroma

Pi keeps the hue of your palette colors as it is. Chroma is trickier. The weird shape of OKLCH shows how much chroma a screen can display, which depends on both hue and lightness. At a lightness of 0.9, the most colorful yellow a screen can show has a chroma of about 0.2, while the most colorful blue only reaches about 0.05. So when Pi moves a palette color to the lightness it needs, its chroma might not exist at that lightness, and mapping it back to a displayable color can change the lightness Pi just calculated.

That's why Pi builds its colors in OKHSL. OKHSL is built on the same foundation as OKLCH and uses the same hues, but it stretches the weird shape back into a cylinder. Its saturation goes from 0% to 100%, where 100% always means "the most colorful this hue can be at this lightness". It tries to keep the best of both worlds: it stays as close to human perception as it can, while bringing back the simple geometry that makes RGB based color spaces easy to work with. Every combination of hue, saturation and lightness is a color your screen can display, which makes it a good fit for generating themes and palettes in general. Pi also lets saturation fall off toward black and white, so that very dark and very light colors, like a panel only slightly lighter than the background, get a hint of color rather than a bright block.

But keeping the saturation the same doesn't keep a color equally colorful. Since saturation is relative to what the screen can display, the same percentage can mean very different amounts of chroma at different lightnesses. Shortly after the release, a [bug report](https://github.com/earendil-works/pi/issues/10255) showed that Pi looked much more vivid than the terminal with Catppuccin Frappé. Catppuccin's pink, #f4b8e4, has an OKHSL saturation of 84%, but that is 84% of the little chroma a screen can show at such a high lightness. Pi's accent needs to be darker to be readable, and since the shape is much wider there, 84% saturation becomes #eb76d1, with about twice the chroma of the original pink. The fix was to also cap the chroma: a palette color can move to a different lightness, but it can never become more colorful than it is in your palette. With the cap, the accent becomes #cc92bd, which looks like Catppuccin again.

So in the end, the chroma of a palette color is limited three times: by what your screen can display, through OKHSL; by the falloff toward black and white; and by the chroma it has in your palette.

The two views below separate the color space from what Pi does inside it. OKHSL's cylinder stays fixed. Pi's range depends on the source color and the family of the UI role: the source's saturation applies at its own lightness, and the falloff is relative to that point. Moving toward the middle never raises the saturation above the source's. Different families use different falloffs; a yellow warning and a violet accent do not use the same curve.

Pick a color to check whether Pi can make it from the source, here or in the terminal above. The dot is the source; squares are the colors Pi uses in this theme. Drag either view to rotate both.

## The result

Pi asks the terminal for its foreground, background and ANSI colors on startup, and rebuilds the theme when the terminal switches between light and dark. If a terminal only reports its background, Pi uses its own hues. If it reports nothing, Pi falls back to the ANSI colors and lets the terminal draw them. The generated colors keep the hues of your terminal, but their lightness is adjusted to a similar contrast. Gruvbox's dark red becomes a lot lighter, Catppuccin Latte's red a little darker, and Nord's colors barely move.

If you haven't picked a theme, you are already using the system theme. Otherwise, you can switch to it in /settings under Theme. If your terminal theme looks off in Pi, please [open an issue](https://github.com/earendil-works/pi/issues) with the name of the theme.

## The survey

All numbers in this post that relate to contrast are estimates of how the average person sees contrast, as determined by existing contrast perception algorithms. But there is no open data set that really tells us the story. We are working on our own open source contrast algorithm, and for that we would love to see how real people perceive contrast on real screens.

The survey takes about 3 minutes to complete, and at the end we'll show you how your answers compare to others who took the survey.

We would love for you to take it.  You can find it at [contrastsurvey.earendil.com](https://contrastsurvey.earendil.com/), and if you participate in it, we'll use your answers to train a contrast algorithm.
