# Colour Sentry

> For agents: start with the [agent guide](https://pi-tui.ratstack.sh/llms.txt). Every page here has a Markdown twin, and every pattern has a JSON record.

Presentation / Theming · `colour-sentry` · [HTML](https://pi-tui.ratstack.sh/patterns/colour-sentry/) · [JSON](https://pi-tui.ratstack.sh/patterns/colour-sentry.json) · [all patterns](https://pi-tui.ratstack.sh/patterns.md)

Also known as Validated theme colours.

## Intent

Validate colour overrides before using them in themed terminal output.

## Motivation

Powerline Footer normalizes user colour overrides before emitting terminal output.

## Applicability

- Use this when an extension exposes configurable appearance.

## Structure

```text
override -> validate / parse
valid colour + theme -> styled text
```

## Participants

- [`parseColor`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/tui/README.md#colors-and-terminal-styles): Converts supported colour values into a Color.
- [`theme.style`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#apply-themes-correctly): Styles text through semantic or concrete colours.
- `Override validator`: Rejects invalid values before they become terminal styles.

Pi component APIs: [`parseColor`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/tui/README.md#colors-and-terminal-styles), [`theme.style`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#apply-themes-correctly), `Text`

## Consequences

- Validated colours can flow through the current theme helpers.
- Invalid overrides need rejection and reused colour math belongs outside rendering.

## Implementation

- Reject invalid colour values before emitting ANSI output.
- Apply the supplied theme rather than fixed ANSI colours.
- Compute reusable concrete colours outside the render path.

## States

Each state was rendered at 40, 60, 80 and 120 columns in Pi's dark and light themes. Each frame links one WebP. An animated frame links its first image, then the animation.

### Active theme

`default`: Show synthetic labels styled by active semantic tokens.

Checks at every width and theme:

- width: ✓ pass
- style-leak: ✓ pass
- hard-coded-colour: ✓ pass
- height: ✓ pass

Frames:

- 40 columns: ![Colour Sentry, Active theme, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/colour-sentry/default-40-dark.8a65b64322f8.webp) ![Colour Sentry, Active theme, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/colour-sentry/default-40-light.da9ad3881750.webp)
- 60 columns: ![Colour Sentry, Active theme, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/colour-sentry/default-60-dark.6f48139ed5bf.webp) ![Colour Sentry, Active theme, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/colour-sentry/default-60-light.9836a80364a8.webp)
- 80 columns: ![Colour Sentry, Active theme, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/colour-sentry/default-80-dark.b3d5e955155a.webp) ![Colour Sentry, Active theme, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/colour-sentry/default-80-light.af37cd51bff6.webp)
- 120 columns: ![Colour Sentry, Active theme, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/colour-sentry/default-120-dark.3cec06c42eb2.webp) ![Colour Sentry, Active theme, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/colour-sentry/default-120-light.4654d6d5f54f.webp)

### Valid override

`override`: Apply a validated colour override through theme.style.

Checks at every width and theme:

- width: ✓ pass
- style-leak: ✓ pass
- hard-coded-colour: ✓ pass
- height: ✓ pass

Frames:

- 40 columns: ![Colour Sentry, Valid override, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/colour-sentry/override-40-dark.5365c4183ad7.webp) ![Colour Sentry, Valid override, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/colour-sentry/override-40-light.119515cf5557.webp)
- 60 columns: ![Colour Sentry, Valid override, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/colour-sentry/override-60-dark.bd51988303af.webp) ![Colour Sentry, Valid override, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/colour-sentry/override-60-light.d6cc3f765e40.webp)
- 80 columns: ![Colour Sentry, Valid override, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/colour-sentry/override-80-dark.49496ec33580.webp) ![Colour Sentry, Valid override, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/colour-sentry/override-80-light.fbe75f790310.webp)
- 120 columns: ![Colour Sentry, Valid override, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/colour-sentry/override-120-dark.5eca26a9bf29.webp) ![Colour Sentry, Valid override, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/colour-sentry/override-120-light.21e9db28f938.webp)

### Invalid override

`invalid`: Show an explicit rejected override and unchanged output.

Checks at every width and theme:

- width: ✓ pass
- style-leak: ✓ pass
- hard-coded-colour: ✓ pass
- height: ✓ pass

Frames:

- 40 columns: ![Colour Sentry, Invalid override, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/colour-sentry/invalid-40-dark.225796720b14.webp) ![Colour Sentry, Invalid override, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/colour-sentry/invalid-40-light.db84edec26f7.webp)
- 60 columns: ![Colour Sentry, Invalid override, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/colour-sentry/invalid-60-dark.af56c497736a.webp) ![Colour Sentry, Invalid override, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/colour-sentry/invalid-60-light.b42622e394dd.webp)
- 80 columns: ![Colour Sentry, Invalid override, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/colour-sentry/invalid-80-dark.f8a5708811e6.webp) ![Colour Sentry, Invalid override, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/colour-sentry/invalid-80-light.24cfd234aba8.webp)
- 120 columns: ![Colour Sentry, Invalid override, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/colour-sentry/invalid-120-dark.984e9be347d5.webp) ![Colour Sentry, Invalid override, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/colour-sentry/invalid-120-light.eee3669c65d6.webp)

### Don't: fixed ANSI

`dont-fixed-ansi`: Render a fixed ANSI-colour label instead of resolving the active theme.

This state is a counter-example. It fails hard-coded-colour on purpose.

Checks at every width and theme:

- width: ✓ pass
- style-leak: ✓ pass
- hard-coded-colour: ✓ fails, as intended
- height: ✓ pass

Evidence from the checker:

- hard-coded-colour at 40 dark: line 3, column 1: frame 0: fg=#ff0077 is not an active theme token
- hard-coded-colour at 40 light: line 3, column 1: frame 0: fg=#ff0077 is not an active theme token
- hard-coded-colour at 60 dark: line 3, column 1: frame 0: fg=#ff0077 is not an active theme token
- hard-coded-colour at 60 light: line 3, column 1: frame 0: fg=#ff0077 is not an active theme token
- hard-coded-colour at 80 dark: line 3, column 1: frame 0: fg=#ff0077 is not an active theme token
- hard-coded-colour at 80 light: line 3, column 1: frame 0: fg=#ff0077 is not an active theme token
- hard-coded-colour at 120 dark: line 3, column 1: frame 0: fg=#ff0077 is not an active theme token
- hard-coded-colour at 120 light: line 3, column 1: frame 0: fg=#ff0077 is not an active theme token

Frames:

- 40 columns: ![Colour Sentry, Don't: fixed ANSI, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/colour-sentry/dont-fixed-ansi-40-dark.bbd5e35c4317.webp) ![Colour Sentry, Don't: fixed ANSI, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/colour-sentry/dont-fixed-ansi-40-light.b348e6fce0c2.webp)
- 60 columns: ![Colour Sentry, Don't: fixed ANSI, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/colour-sentry/dont-fixed-ansi-60-dark.847cae2fe1cd.webp) ![Colour Sentry, Don't: fixed ANSI, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/colour-sentry/dont-fixed-ansi-60-light.da96d3a87632.webp)
- 80 columns: ![Colour Sentry, Don't: fixed ANSI, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/colour-sentry/dont-fixed-ansi-80-dark.58367eb8cd48.webp) ![Colour Sentry, Don't: fixed ANSI, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/colour-sentry/dont-fixed-ansi-80-light.850730840892.webp)
- 120 columns: ![Colour Sentry, Don't: fixed ANSI, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/colour-sentry/dont-fixed-ansi-120-dark.2ffb95d0aa91.webp) ![Colour Sentry, Don't: fixed ANSI, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/colour-sentry/dont-fixed-ansi-120-light.bcd452dd74a7.webp)

## Sample code

`stories/patterns/presentation/colour-sentry.ts`, the story the frames above were rendered from.

```ts
// Colour Sentry: validate overrides before they become terminal styles.
import { colorToHex, parseColor, Text, type Color } from "@earendil-works/pi-tui";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "colour-sentry", title: "Colour Sentry", kind: "component",
  apis: ["parseColor", "theme.style", "Text"], rows: 10,
  states: [
    { id: "default", label: "Active theme" },
    { id: "override", label: "Valid override", steps: [{ type: "action", name: "valid" }] },
    { id: "invalid", label: "Invalid override", steps: [{ type: "action", name: "invalid" }] },
    { id: "dont-fixed-ansi", label: "Don't: fixed ANSI", steps: [{ type: "action", name: "fixed" }],
      expectFail: ["hard-coded-colour"] },
  ],
  setup({ theme, action, tui }) {
    let colour: Color = theme.colors.accent;
    let note = "Default: accent", rejected = false, fixed = false;
    function override(value: string) {
      try {
        const parsed = parseColor(value);
        colour = parsed; note = "Accepted: active success colour"; rejected = false;
      } catch {
        note = "Rejected: not-a-colour"; rejected = true;
      }
      tui.requestRender();
    }
    action("valid", () => override(colorToHex(theme.colors.success)));
    action("invalid", () => override("not-a-colour"));
    action("fixed", () => { fixed = true; tui.requestRender(); });
    return {
      invalidate() {},
      render(width) {
        const label = fixed
          ? "\x1b[38;2;255;0;119mBuild ready · fixed RGB\x1b[0m"
          : theme.style("Build ready", { fg: colour, bold: true });
        return new Text([
          theme.fg("accent", "Appearance overrides"), "",
          label, theme.fg(rejected ? "error" : "muted", note), "",
          theme.fg("dim", fixed ? "Don't bypass the active palette" :
            rejected ? "Previous accepted output is unchanged" : "Validation happens before styling"),
        ].join("\n"), 0, 0).render(width);
      },
    };
  },
};
```

## Known uses: seen in Nico's repos

- [**pi-powerline-footer**](https://github.com/nicobailon/pi-powerline-footer): Theme configuration and sanitization
  - [theme.ts:1-50](https://github.com/nicobailon/pi-powerline-footer/blob/859dee671b633fb533b07ceba3e6c1ab1c43360a/theme.ts#L1-L50) @859dee67

## For agents: choose and check

Choose Colour Sentry when your job matches its intent and applicability above. Its neighbours in Theming are listed below. Read the one whose intent fits your job more closely before you commit.

- [Palette Deck](https://pi-tui.ratstack.sh/patterns/palette-deck.md): Centralize coordinated colour choices behind named presets and a default.

Check your version:

- Render your version at 40, 60, 80 and 120 columns in the dark and light themes.
- Check width: no rendered line is wider than the terminal.
- Check style-leak: no line ends with colour, bold or a link still switched on.
- Check hard-coded-colour: every colour on screen comes from the active theme.
- Check height: the output fits in the rows the terminal has.
- "Don't: fixed ANSI" is a counter-example. Its frames fail hard-coded-colour on purpose. Your version should not look like it.
- Read the Pi 1.0.3 docs for [`parseColor`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/tui/README.md#colors-and-terminal-styles), [`theme.style`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#apply-themes-correctly), `Text` before you use them.
- Every reference frame gets the verdict its state expects.

Next actions: `related({ id: "colour-sentry" })` lists what to read next, and `states({ id: "colour-sentry", width: 40 })` returns every frame and verdict at one width. The [build-pi-tui-component skill](https://pi-tui.ratstack.sh/skills/build-pi-tui-component.md) walks through the whole loop.

## Related

- [Palette Deck](https://pi-tui.ratstack.sh/patterns/palette-deck.md): supplies coordinated override values

## Linked from

- [Palette Deck](https://pi-tui.ratstack.sh/patterns/palette-deck.md): validates values before styling
