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>
206 lines
6.6 KiB
TypeScript
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]!,
|
|
};
|
|
}
|