Files
platform/scripts/themes/ansi-source.ts
T
pastilhasandClaude Sonnet 5 861fc4a58b add colors.toml and 0x-hex support, rescuing 44 more community themes
Poked at the 62 "no parseable source" skips from the first pass and found two things
the pipeline didn't handle: several community repos genuinely ship colors.toml too — some
in Omarchy 4's own semantic schema (best case, no derivation needed at all, same quality
as the official 22), others reusing the filename for an unrelated flat color0..color15
scheme — and a handful of alacritty.toml files use 0xrrggbb instead of #rrggbb.

palette.ts's loadPalette is now a thin wrapper over two exported pieces (parseTomlText,
paletteFromRecord) so the community pipeline can attempt the real schema first without
duplicating the reader; falls through to the color16 or ANSI-derived paths otherwise.
generate-themes.ts --check still reports 0 changes for the official set — the refactor
is behavior-preserving there, the lowercase-hex normalization it picked up along the way
touched 7 official theme files cosmetically (their source colors.toml has uppercase hex),
regenerated for real rather than left inconsistent with their own source.

147 -> 191 community themes ported. Of the original 62 unparseable repos, most turned out
to be simply gone (404 on GitHub — repos deleted or renamed since the site listed them);
the rest needed one of the two format additions above. 20 themes remain stuck: 16 dead
repos, 2 with no repo link at all, and the same handful of genuinely low-contrast source
palettes (mostly Solarized variants) where fixing contrast would mean changing the
author's actual colours rather than porting them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 01:48:21 +00:00

206 lines
6.6 KiB
TypeScript

// Reading a community theme's actual shipped format.
//
// Omarchy's own 22 themes carry a `colors.toml` in the semantic shape `palette.ts` expects — three
// background depths, four foreground weights, an accent, a selection, a muted. A community theme on
// omarchythemes.com ships none of that: it's per-app config files in the classic 16-ANSI-slot style,
// because that's what predates Omarchy 4's own format and what most single-theme repos still use.
//
// This file reads the two shapes that cover almost every repo: `alacritty.toml` (structured, typed,
// preferred) and `ghostty.conf`/`ghostty-theme` (flat `key = value` lines, fallback). Either becomes an
// `AnsiSource` — the 16 ANSI slots plus primary background/foreground — which `from-ansi.ts` turns into
// the same `Palette` shape the official pipeline consumes, so every downstream guarantee (contrast
// checking, the 84-token mapping) applies unchanged.
export type AnsiSource = {
background: string;
foreground: string;
black: string;
red: string;
green: string;
yellow: string;
blue: string;
magenta: string;
cyan: string;
white: string;
brightBlack: string;
brightRed: string;
brightGreen: string;
brightYellow: string;
brightBlue: string;
brightMagenta: string;
brightCyan: string;
brightWhite: string;
};
// `#rrggbb` is standard; `0xrrggbb` shows up in a handful of community alacritty.toml files (their
// authors apparently ported from a scheme that used C-style hex literals).
const HEX = /#[0-9a-fA-F]{3,8}|0x[0-9a-fA-F]{3,8}/;
function normalizeHex(v: string): string | undefined {
const m = v.match(HEX);
if (!m) return undefined;
let h = m[0].replace(/^(#|0x)/, '').toLowerCase();
if (h.length === 3)
h = h
.split('')
.map((c) => c + c)
.join('');
if (h.length >= 6) h = h.slice(0, 6);
return `#${h}`;
}
/** A deliberately small TOML reader: flat `key = "value"` lines plus `[section]` headers. */
export function parseAlacrittyToml(text: string): AnsiSource | undefined {
let section = '';
const rows: Record<string, Record<string, string>> = {};
for (const rawLine of text.split('\n')) {
const line = rawLine.trim();
const sectionMatch = line.match(/^\[([\w.]+)]$/);
if (sectionMatch) {
section = sectionMatch[1]!;
rows[section] ??= {};
continue;
}
// Quoted specifically, not "anything but a comment" — a hex value starts with `#`, the same
// character that opens a trailing comment, so the two can only be told apart by the quotes.
const kv = line.match(/^([\w-]+)\s*=\s*(['"])(.*?)\2/);
if (kv && section) {
const hex = normalizeHex(kv[3]!);
if (hex) rows[section]![kv[1]!] = hex;
}
}
const primary = rows['colors.primary'];
const normal = rows['colors.normal'];
const bright = rows['colors.bright'];
if (!primary?.background || !primary?.foreground || !normal || !bright) return undefined;
const need = (r: Record<string, string>, k: string): string | undefined => r[k];
const n = (k: string) => need(normal, k);
const b = (k: string) => need(bright, k);
if (
!n('black') ||
!n('red') ||
!n('green') ||
!n('yellow') ||
!n('blue') ||
!n('magenta') ||
!n('cyan') ||
!n('white')
)
return undefined;
if (
!b('black') ||
!b('red') ||
!b('green') ||
!b('yellow') ||
!b('blue') ||
!b('magenta') ||
!b('cyan') ||
!b('white')
)
return undefined;
return {
background: primary.background,
foreground: primary.foreground,
black: n('black')!,
red: n('red')!,
green: n('green')!,
yellow: n('yellow')!,
blue: n('blue')!,
magenta: n('magenta')!,
cyan: n('cyan')!,
white: n('white')!,
brightBlack: b('black')!,
brightRed: b('red')!,
brightGreen: b('green')!,
brightYellow: b('yellow')!,
brightBlue: b('blue')!,
brightMagenta: b('magenta')!,
brightCyan: b('cyan')!,
brightWhite: b('white')!,
};
}
/** Ghostty's config: flat `key = value`, ANSI palette as sixteen `palette = N=#hex` lines. */
export function parseGhosttyConf(text: string): AnsiSource | undefined {
let background: string | undefined;
let foreground: string | undefined;
const palette: Record<number, string> = {};
for (const rawLine of text.split('\n')) {
const line = rawLine.trim();
if (line.startsWith('#')) continue;
const bg = line.match(/^background\s*=\s*(#[0-9a-fA-F]{3,8})/);
if (bg) background = normalizeHex(bg[1]!);
const fg = line.match(/^foreground\s*=\s*(#[0-9a-fA-F]{3,8})/);
if (fg) foreground = normalizeHex(fg[1]!);
const pal = line.match(/^palette\s*=\s*(\d+)\s*=\s*(#[0-9a-fA-F]{3,8})/);
if (pal) {
const hex = normalizeHex(pal[2]!);
if (hex) palette[Number(pal[1])] = hex;
}
}
if (!background || !foreground) return undefined;
for (let i = 0; i <= 15; i++) if (!palette[i]) return undefined;
return {
background,
foreground,
black: palette[0]!,
red: palette[1]!,
green: palette[2]!,
yellow: palette[3]!,
blue: palette[4]!,
magenta: palette[5]!,
cyan: palette[6]!,
white: palette[7]!,
brightBlack: palette[8]!,
brightRed: palette[9]!,
brightGreen: palette[10]!,
brightYellow: palette[11]!,
brightBlue: palette[12]!,
brightMagenta: palette[13]!,
brightCyan: palette[14]!,
brightWhite: palette[15]!,
};
}
/**
* A third shape some community repos name `colors.toml` too, unrelated to Omarchy 4's own — flat
* `color0`..`color15` in standard ANSI order, plus `background`/`foreground`. Try this only after
* `paletteFromRecord` (the real Omarchy schema) has already failed on the same file: the two are
* distinguished only by which keys are present, and a file can't be both.
*/
export function parseColor16Toml(t: Record<string, string>): AnsiSource | undefined {
const at = (i: number): string | undefined => (t[`color${i}`] ? normalizeHex(t[`color${i}`]!) : undefined);
const background = t.background ? normalizeHex(t.background) : undefined;
const foreground = t.foreground ? normalizeHex(t.foreground) : undefined;
if (!background || !foreground) return undefined;
const slots = Array.from({ length: 16 }, (_, i) => at(i));
if (slots.some((s) => !s)) return undefined;
return {
background,
foreground,
black: slots[0]!,
red: slots[1]!,
green: slots[2]!,
yellow: slots[3]!,
blue: slots[4]!,
magenta: slots[5]!,
cyan: slots[6]!,
white: slots[7]!,
brightBlack: slots[8]!,
brightRed: slots[9]!,
brightGreen: slots[10]!,
brightYellow: slots[11]!,
brightBlue: slots[12]!,
brightMagenta: slots[13]!,
brightCyan: slots[14]!,
brightWhite: slots[15]!,
};
}