// 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; }; const HEX = /#[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].slice(1).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> = {}; 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, 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 = {}; 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]!, }; }