It started as "add some themes" and ended with me reading a font-licensing statement a designer emailed someone in 2014. This is how Localhost Explorer learned to dress as the machines it descends from — Norton Commander blue, one-bit System 7, four shades of Game Boy olive — all the way down to the window toolbar, and how the same identities then travelled into a Go TUI that has to live inside whatever terminal you give it.
The ambition
A dark-mode toggle changes colors. That was never the bar. The bar was: if you pick MS-DOS, nothing on screen is allowed to disagree with 1989. The canvas is CGA blue #0000AA because that's the byte the VGA card got. Corners are sharp because rounded rects cost silicon nobody had. The signal colors are #55FF55 / #FFFF55 / #FF5555 — the bright half of the CGA palette, not "a green that fits our brand." And the type is the actual IBM VGA 9×16 text-mode ROM font, not a monospace that squints like one. Same deal for System 7: black on white, one bit, Chicago — the real Chicago.
Ten themes shipped: MS-DOS, System 7, Game Boy, two CRT phosphors (green and amber), e-ink Paper, cyanotype Blueprint, Dracula, Nord, Vaporwave. The interesting part isn't the list — it's what "whole app" turned out to mean.
An engine, not ten skins
SwiftUI has no theme system. What it has is an environment, and a design system that — if you've been disciplined — already routes every visual decision through a handful of choke points. So a theme here is one struct: canvas, two text colors, an accent, three signal hues, a chip fill, two corner radii, a typeface. The engine trick that keeps ten themes tractable: every theme forces its own ColorScheme, so .primary, .secondary, and the system materials adapt for free, and the theme only overrides the tokens the design system already funnels through. The native theme overrides nothing — it is the current look, which means the themed code path can never drift from the default one.
Corner radius turned out to be identity, not decoration. MS-DOS and System 7 are radius 0 everywhere — buttons, chips, stat tiles, count pills that were capsules a theme ago. Dracula keeps its capsules. You notice sharpness before you consciously notice color, and nothing says "this is a modern app cosplaying" like a rounded pill on a DOS screen.
Obstacle one: explicit fonts win
The theme sets its typeface as the surface's default font, so every unstyled label picks it up for free. The trap: a default only styles what nobody styled. Every .font(.caption) in the codebase — and a stats dashboard has a lot of captions — was a silent opt-out. The result was surreal: rows in genuine Chicago next to section headers in San Francisco, and a user screenshot circling the parts that didn't get the memo.
The fix is boring, which is what makes it worth writing down: every explicit font goes through the theme (theme.statFont(10, native: .caption) — native look preserved exactly, themed look substituted), and now an explicit size is a decision the theme gets to translate rather than a hole it can't see. If you're building whole-surface theming: grep for .font( first. That list is your real surface area.
Obstacle two: decoration versus signal
The stats dashboard had a rainbow — blue Processes, mint Endpoints, green Projects — and exactly one color that means something: the orange on wildcard listeners, the "this port is open to your whole network" warning. A theme has to eat the decoration and preserve the signal, and no theme can tell the difference by looking at a Color.
So the difference is declared at the one choke point every tint already flowed through, which now has two axes: the Signal row style collapses decorative color to grey (that shipped months ago), and a non-native theme collapses it to the theme's accent — while genuine signal tints pass through both, pre-mapped to the theme's own risk hues. Under MS-DOS the dashboard is cyan-on-blue with one yellow warning; under System 7 it's black-on-white with one dark-amber warning. The rule that fell out: if a color means nothing, the theme owns it; if it means risk, the theme translates it.
Obstacle three: the real fonts are a licensing problem
For a while the themes used installed approximations — Silom squints like Chicago, Monaco is at least a 1984 Mac face. Then someone put a Wikipedia screenshot of Norton Commander next to the app and the approximation stopped being charming.
The authentic faces exist, and the work is licensing archaeology, not typography. The DOS face comes from VileR's Ultimate Oldschool PC Font Pack — pixel-perfect outline conversions of actual ROM fonts, CC BY-SA 4.0, so it can ship inside the app with attribution. Px437_IBM_VGA_9x16 is the 9×16 glyph grid every DOS screenshot you've ever seen was drawn in. Chicago is trickier — the Apple original is off limits — but ChicagoFLF is public domain, with a 2014 email from its designer Robin Casady in the readme saying so. Both licenses ship next to the fonts in the bundle.
The bundling itself is the easiest step on macOS, and barely documented: one Info.plist key, ATSApplicationFontsPath, pointing at a folder in Resources. CoreText activates everything in it before main() runs — no CTFontManager calls, no registration code. The themes resolve through a fallback stack (firstInstalled("Px437 IBM VGA 9x16", "Monaco")) so a bare binary outside the bundle degrades to the approximation instead of to nothing.
Obstacle four: the toolbar SwiftUI can't see
With all of that done, a user sent a screenshot of six grey toolbar icons. Correctly grey, infuriatingly grey: the standalone window's toolbar is a real NSToolbar — AppKit, outside the SwiftUI hierarchy, deaf to the environment the whole theme engine lives in. The cascade that styles ninety-five percent of the app simply stops at that boundary.
So that boundary gets hand fed: the toolbar's SF Symbols are re-created with palette symbol configurations in the theme's accent, and the UserDefaults observer that already rebuilt the toolbar when the appearance flipped now keys on appearance and theme, so switching to Dracula in Settings retints the toolbar purple in the same frame — no relaunch. The lesson generalizes: whole-surface theming means the surfaces you forgot are surfaces. The menu bar extra, the toolbar, the window title — each one either gets the memo by hand or stands there in system grey telling your users you didn't finish.
Making it travel: ten themes, seven colors
Then the identities had to reach the CLI — le renders the same listener table in a terminal, and "the same table" is a promise about identity, not just columns.
A terminal inverts the whole problem. The app owns everything and the challenge was reaching all of it; a TUI owns almost nothing. Your terminal decides the canvas color and the font — the two loudest parts of the identity stay home. What's left is exactly seven semantic roles in a lipgloss palette: brand, dim text, three risk colors, selection background, foreground. A theme port is those seven hexes, chosen honestly:
{name: "msdos", brand: "#00AAAA", subtle: "#55FFFF",
green: "#55FF55", yellow: "#FFFF55", red: "#FF5555",
selBG: "#00AAAA", fg: "#FFFFFF"}
The selBG is the detail I'd defend in a code review: Norton Commander's cursor bar was cyan, so the selection bar is #00AAAA — the one place the port can quote the original directly. Some things don't survive the crossing: a seven-role palette can't express System 7's inverse-video selection, so that became a grey compromise; and system7, gameboy, and paper are dark-on-light themes that want a light terminal — the same caveat the light theme has carried since it shipped. The port also can't fake the raster fonts, and shouldn't try: if you want the DOS face in your terminal, that's your terminal profile's job — set it there and le inherits it, which is more authentic than anything a TUI could do anyway.
Parity is enforced, not hoped for: both sides carry a test asserting the shared theme names exist, so a rename in either codebase fails CI loudly instead of drifting quietly. And the loop closes in a pleasing way — the app's Terminal appearance, which ports the CLI's palettes back into native SwiftUI, picked up all eight retro themes by adding rows to one array. The identity round-trips: app → CLI → app.
What shipped
In the app: ten native themes that own the canvas, the type (bundled, licensed, with fallbacks), the signal colors, the radii, and yes, the toolbar. In the CLI: t now cycles fourteen themes — the original six plus the eight ports — and theme = msdos in ~/.config/le/config makes it stick, over SSH, on a box that has never heard of your Mac.
The ambition, in one sentence: a theme isn't a color swap, it's a claim about what machine you're on — and the app should never break character.
Break character never.
The CLI is MIT-licensed — fourteen themes included. The mac app's retro themes ship in the next release.