09 TextMode – Color Themes

12 min read

One call repaints every view, retints the text already on screen, and moves the ANSI table with it, so a player can restyle your whole display without you redrawing anything.

Summary

A theme in TextMode is a named color palette, and switching one is a single call. That call does three things at once: it pushes the new colors into every view that follows the theme, it rewrites the sixteen ANSI colors so indexed content means the new palette, and it walks the cells already on screen translating old colors to new ones. The screen restyles without anyone redrawing it.

The reason it works that way is that a theme is not a struct of thirteen color fields living in TextMode. It is a Core ColorPalette tagged for TextMode, resolved by name through the shared color asset registry, exactly as the Console module resolves its own themes. That has a few practical consequences worth knowing up front. A theme is expressible as a single string, so it survives a JSON config file and a save game. A project can add its own by dropping a palette asset into the project with the right tag and the right entries, with no code at all. And the same palette can be authored in Chroma, the color and palette tool, rather than in a C# file.

Eight themes ship: Classic, Green Phosphor, Amber Phosphor, Lightman, Midnight, Paper, WordPerfect and Amiga. The two phosphor themes are what they sound like, Lightman is a monochrome pale blue on near-black, Midnight is a cool dark scheme, and Paper is the inverted light one. WordPerfect reproduces the light gray on blue of DOS word processors in the DOS text mode colors, and Amiga reproduces the white on blue of the Workbench 1.3 screen with its orange cursor. The others are Scylla’s own designs.

A palette has to earn the right to be a theme. It must lead with the sixteen ANSI colors in ANSI order, because position is the contract there, and it must define every semantic entry the module needs. A palette that fails either check is rejected and the module falls back to Classic with a warning rather than rendering with half a theme.

Use color themes for:

  • Letting players pick a display style, including high-contrast or light options.
  • Shipping a retro look without hard-coding phosphor green through your drawing code.
  • Restyling an entire screen at runtime without a redraw pass.
  • Keeping your game’s colors consistent between your own drawing and terminal output.

Features

  • A theme is a palette by name. Resolved through the Core color asset registry, so it is a string in a config file rather than a code-defined structure.
  • Eight shipped themes. Classic, Green Phosphor, Amber Phosphor, Lightman, Midnight, Paper, WordPerfect and Amiga.
  • Project themes with no code. Drop in a tagged palette asset with the right entries and it appears alongside the built-ins.
  • Retint on switch. Cells already on screen are translated from the old palette to the new one, so a switch does not require a redraw.
  • ANSI table follows. Indexed color moves with the theme, so escape sequences keep painting from the current palette rather than a stale sixteen.
  • Semantic colors. Beyond the ANSI sixteen, a scheme carries foreground, background, accent, border, cursor, selection and status colors, so game code can reference intent rather than a specific hue.
  • Per-view opt-out. A view can decline to follow the theme, which is how a display styled deliberately by its host keeps its colors.
  • Validation with fallback. An unresolvable name or an incomplete palette falls back to Classic and logs, rather than failing startup or rendering wrongly.

The color scheme

TextModeColorScheme is the resolved form of a theme: the values a view and your drawing code actually read.

Entry IDWhat it is for
ForegroundOrdinary text. What a cleared cell and a default print use.
BackgroundThe surface behind everything. A transparent value lets the game show through.
BrightForegroundEmphasized text, headings, the thing you want read first.
DimForegroundDe-emphasized text: hints, disabled entries, background chatter.
AccentThe theme’s signature color, for highlights and selection marks.
BorderFrames and rules, so a panel outline is not competing with its contents.
CursorThe insertion cursor, kept separate so it can contrast against text.
SelectionBackground, SelectionForegroundThe selected-text pair. Selected cells are drawn in these two colors instead of their own. The built-in themes use their own background on their foreground, so a selection reads as inverse text.
ErrorForeground, WarningForeground, InfoForeground, SuccessForegroundStatus colors, so a log line’s severity does not need a palette lookup in your code.

Drawing with semantic colors rather than named ANSI ones is why a screen restyles sensibly. ANSIColor.Green is green in every theme; TextModeThemes.Active.SuccessForeground is whatever that theme considers a success color, which in Paper is a dark green on white and in Green Phosphor is a brighter shade of the same phosphor.

Recipes

Let the player pick a theme

Problem. Your options menu should offer the display styles you ship plus anything the player has added, and applying one should take effect immediately.

Solution. Ask for the available names and set by name.

/* Includes the shipped themes and any project palette tagged for TextMode that satisfies the
   contract, so a player-supplied palette shows up without a code change. */
var names = TextModeThemes.GetAvailableThemeNames();

/* Setting a theme repaints every following view, moves the ANSI table, and retints what is
   already on screen. Returns false when the name could not be resolved. */
if (!TextModeThemes.SetTheme(names[selectedIndex]))
{
    /* The module has fallen back to Classic and logged why. */
}

Cycle themes from a key

Problem. You want a quick way to flip through themes while tuning your screens, or a player-facing shortcut.

Solution. CycleTheme steps to the next available theme and returns the one it landed on.

/* Walks the available list in order and wraps. Handy for a debug key and for a demo. */
var applied = TextModeThemes.CycleTheme();
_console.PrintAligned(0, applied, TextAlignment.Center,
    TextModeThemes.Active.DimForeground, TextModeThemes.Active.Background);

Invert the theme’s colors

Problem. You want a negative of the current look, for example to flash the screen on a hit or to offer a light version of a dark theme.

Solution. SetInverted shows every color TextMode draws with its complement: theme colors, indexed colors and 24-bit colors alike, on every view at once, including a view shown through the CRT. The inversion stays on when the theme changes, so every theme switched to afterwards is shown inverted too, until SetInverted(false) turns it off.

/* Toggles the negative. The theme, its palette and the text already on screen keep their
   own colors; only the display is inverted, so switching back is exact. */
TextModeThemes.SetInverted(!TextModeThemes.Inverted);

Anything you draw next to a view in a theme color, such as a panel behind the grid, can follow along with TextModeThemes.DisplayColor, which returns the color as it currently appears. The terminal demo exposes the inversion as theme inverse.

Draw in colors that follow the theme

Problem. Your status panel uses green for good and red for bad, and under the Paper theme those hard-coded colors are unreadable on a white background.

Solution. Draw with the scheme’s semantic colors instead of named ANSI ones.

var theme = TextModeThemes.Active;

/* Intent rather than hue. Under Paper these resolve to dark tones on a light background;
   under Green Phosphor they resolve to shades of the same phosphor. */
_console.Print(2, 1, "REACTOR", theme.Foreground, theme.Background);
_console.Print(12, 1, "NOMINAL", theme.SuccessForeground, theme.Background);
_console.Print(2, 2, "COOLANT", theme.Foreground, theme.Background);
_console.Print(12, 2, "LOW",     theme.WarningForeground, theme.Background);

/* Frames use the border color so they recede rather than competing with the text. */
_console.DrawBox(0, 0, PANEL_W, PANEL_H, BoxStyle.Single, theme.Border, theme.Background);

Switch without retinting what is on screen

Problem. You are about to redraw the whole screen anyway, and the retint pass is wasted work.

Solution. Every theme call takes a flag for it.

/* Skips the walk over existing cells. New drawing uses the new colors; the old content keeps
   the old ones until you redraw it, which you are about to do. */
TextModeThemes.SetTheme(themeName, remapExisting: false);

RedrawEverything();

Add a project theme with no code

Problem. Your game wants a bespoke palette that matches its art direction, and you would rather not maintain it in C#.

Solution. Author a palette asset and satisfy the contract. The registry finds it.

  1. Duplicate one of the shipped TextMode palettes, or author a new one in Chroma.
  2. Keep the sixteen ANSI colors first and in ANSI order. Position is the contract there, not the entry ID.
  3. Define every semantic entry. A palette missing one is rejected rather than partially applied.
  4. Tag it for TextMode so the registry offers it as a theme.
  5. Set its name in the configuration’s Theme group, or pass the name to SetTheme.

Reset to the configured theme

Problem. A cutscene switched to a special theme and you want to return to whatever the player chose without knowing what it was.

Solution. ResetTheme returns to the configured startup theme.

/* Goes back to the theme named in the configuration, whatever the player set it to. */
TextModeThemes.ResetTheme();

API Sketch

static class TextModeThemes {
    string ActiveThemeName { get; }
    ColorPalette ActivePalette { get; }
    TextModeColorScheme Active { get; }
    bool Inverted { get; }

    bool SetTheme(string themeName, bool remapExisting = true);
    bool SetInverted(bool inverted);
    Color DisplayColor(Color color);
    Color32 DisplayColor(Color32 color);
    bool RefreshActiveTheme(bool remapExisting = true);
    string CycleTheme(bool remapExisting = true);
    void ResetTheme();
    List<string> GetAvailableThemeNames();
}

readonly struct TextModeColorScheme {
    Color32 Foreground, Background, BrightForeground, DimForeground;
    Color32 Accent, Border, Cursor;
    Color32 SelectionBackground, SelectionForeground;
    Color32 ErrorForeground, WarningForeground, InfoForeground, SuccessForeground;
}

static class TextModeColorDefaults {
    ColorPalette Resolve(string themeName);      /* falls back to Classic */
    ColorPalette Fallback { get; }               /* the hard-coded classic set */
    bool HasRequiredEntries(ColorPalette palette);
    ReadOnlySpan<string> BuiltInNames;
    ReadOnlySpan<string> ANSIEntryNames;
    ReadOnlySpan<string> SemanticEntryNames;
}

static class TextModeThemeRemapper {
    int BuildMap(ColorPalette from, ColorPalette to, Dictionary<uint, uint> map);
    int Remap(CellGrid grid, IReadOnlyDictionary<uint, uint> map);
}

See the API reference for full signatures, remarks, and exception contracts on every member.

Shipped themes

ThemeCharacterSuits
Scylla TextMode ClassicLight gray on black, full-color ANSIThe default. A neutral terminal look that does not impose a period or a mood.
Scylla TextMode Green PhosphorMonochrome green, brightening rather than desaturating at the top of its rampEarly terminals, hacker fiction, anything wanting a CRT feel.
Scylla TextMode Amber PhosphorMonochrome amber, same ramp constructionThe warmer phosphor alternative; easier on the eye for long reading.
Scylla TextMode LightmanMonochrome pale blue on near-black, with a deeper blue accentEarly home computer screens, and anything that wants the calm of a monochrome terminal without green or amber.
Scylla TextMode MidnightCool blues on near-blackA modern dark scheme for interfaces that are not pretending to be old hardware.
Scylla TextMode PaperDark text on a light ground, with an inverted bright rampLight mode, and the accessibility option for players who find dark screens hard to read.
Scylla TextMode WordPerfectLight gray on blue in the DOS text mode colors, with bright white for emphasis and blue on gray for selectionsWord processors, editors and other applications that should look like late 1980s DOS software.
Scylla TextMode AmigaWhite on blue with an orange cursor and accent, in the four Workbench 1.3 colors, with vivid Amiga colors for the rest of the ANSI setShells, file managers and anything that should look like a late 1980s Amiga.

Best Practices

  • Draw with semantic colors when intent matters. SuccessForeground restyles sensibly across every theme; a hard-coded green does not survive a light theme.
  • Draw with named ANSI colors when terminal consistency matters. They follow the palette too, and they keep your drawing consistent with anything the terminal renders.
  • Reserve truecolor for what must never change. It is invisible to theme changes, which is right for brand colors and wrong for interface chrome.
  • Skip the retint when you are about to redraw. The remap walks every cell; there is no point paying for it twice.
  • Let the view follow the theme unless its host owns the colors. A display styled deliberately by another system should opt out. That is exactly what the debug console does, by naming no theme.
  • Validate a project palette before shipping it. The contract is the sixteen ANSI colors in order plus every semantic entry; a palette missing one silently becomes Classic at runtime.
  • Keep the ANSI sixteen faithful for terminal compatibility. Positions are addressed by escape sequences, so creative reordering breaks hosted programs.
  • Store the theme by name. It is a string, which means a save file, a config file and a settings menu can all round-trip it without a custom serializer.
  • Consider a light theme an accessibility feature, not a novelty. Paper exists so a player who cannot comfortably read light-on-dark has an option.
  • Check the return value of SetTheme. False means the name did not resolve and the module fell back, which is the difference between a working theme picker and one that silently does nothing.

Pitfalls