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>
This commit is contained in:
2026-08-22 01:48:21 +00:00
co-authored by Claude Sonnet 5
parent a58f4d2b32
commit 861fc4a58b
57 changed files with 4859 additions and 124 deletions
+36 -14
View File
@@ -3,8 +3,10 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { execSync } from 'node:child_process';
import { join } from 'node:path';
import { mapPalette } from './themes/map';
import type { Palette } from './themes/palette';
import { paletteFromRecord, parseTomlText } from './themes/palette';
import { deriveOmarchyPalette } from './themes/from-ansi';
import { parseAlacrittyToml, parseGhosttyConf } from './themes/ansi-source';
import { parseAlacrittyToml, parseColor16Toml, parseGhosttyConf } from './themes/ansi-source';
import { contrast, ensureContrast, TEXT, UI } from './themes/contrast';
// Port community Omarchy themes from omarchythemes.com into the same 84-token shape the official
@@ -36,7 +38,7 @@ type ManifestEntry = {
id: string;
name: string;
repo: string;
kind: 'alacritty.toml' | 'ghostty.conf';
kind: string;
dark: boolean;
};
@@ -105,12 +107,29 @@ async function discoverRepo(slug: string): Promise<string | undefined> {
return m ? `${m[1]}/${m[2]}` : undefined;
}
async function sourceFor(
repo: string,
): Promise<{ ansi: NonNullable<ReturnType<typeof parseAlacrittyToml>>; kind: ManifestEntry['kind'] } | undefined> {
async function sourceFor(repo: string): Promise<{ palette: Palette; kind: string; derived: boolean } | undefined> {
const branch = ghApiDefaultBranch(repo);
const raw = (p: string) => `https://raw.githubusercontent.com/${repo}/${branch}/${p}`;
// `colors.toml` is ambiguous across community repos: it might genuinely be Omarchy 4's own semantic
// schema (in which case no derivation is needed at all — same quality as the official 22), or it might
// reuse the filename for an unrelated flat `color0..color15` ANSI scheme. Try the real schema first;
// only fall through to the ANSI reading if the required keys aren't there.
const tomlText = await fetchText(raw('colors.toml'));
if (tomlText) {
const kv = parseTomlText(tomlText);
try {
return {
palette: paletteFromRecord(kv, `${repo}/colors.toml`),
kind: 'colors.toml (Omarchy schema)',
derived: false,
};
} catch {
const ansi = parseColor16Toml(kv);
if (ansi) return { palette: deriveOmarchyPalette(ansi), kind: 'colors.toml (16-colour ANSI)', derived: true };
}
}
for (const path of [
'alacritty.toml',
'.config/alacritty.toml',
@@ -120,13 +139,13 @@ async function sourceFor(
const text = await fetchText(raw(path));
if (!text) continue;
const ansi = parseAlacrittyToml(text);
if (ansi) return { ansi, kind: 'alacritty.toml' };
if (ansi) return { palette: deriveOmarchyPalette(ansi), kind: 'alacritty.toml', derived: true };
}
for (const path of ['ghostty.conf', 'ghostty-theme', '.config/ghostty/config']) {
const text = await fetchText(raw(path));
if (!text) continue;
const ansi = parseGhosttyConf(text);
if (ansi) return { ansi, kind: 'ghostty.conf' };
if (ansi) return { palette: deriveOmarchyPalette(ansi), kind: 'ghostty.conf', derived: true };
}
return undefined;
}
@@ -136,9 +155,9 @@ async function portOne(slug: string): Promise<{ entry: ManifestEntry; css: strin
if (!repo) return { skip: 'no github repo link found on its omarchythemes.com page' };
const source = await sourceFor(repo);
if (!source) return { skip: `no parseable alacritty.toml/ghostty.conf in ${repo}` };
if (!source) return { skip: `no parseable colors.toml/alacritty.toml/ghostty.conf in ${repo}` };
const palette = deriveOmarchyPalette(source.ansi);
const palette = source.palette;
const tokens = mapPalette(palette);
// `--input` carries a real accessibility requirement (a field boundary you can't see) where `--border`
@@ -161,16 +180,19 @@ async function portOne(slug: string): Promise<{ entry: ManifestEntry; css: strin
const body = Object.entries(tokens)
.map(([k, v]) => ` ${k}: ${v};`)
.join('\n');
const modeLine = source.kind.startsWith('colors.toml (Omarchy')
? `${palette.mode === 'dark' ? 'Dark theme' : 'Light theme'} (${palette.mode}) — the source repo's own colors.toml says so directly.`
: `${palette.mode === 'dark' ? 'Dark theme' : 'Light theme'} (${palette.mode}), by luminance of its own background — the source repo carries no mode flag of its own to read.`;
const provenanceLine = source.derived
? `went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because the source has no Omarchy 4\n colors.toml — some tokens here are reconstructed, not authored.`
: `came straight from the repo's own colors.toml — the same schema and reader the official 22 themes\n use, so nothing here is reconstructed.`;
const css = `/*
${name} — ported from a community Omarchy theme, not from basecamp/omarchy's own set.
${palette.mode === 'dark' ? 'Dark theme' : 'Light theme'} (${palette.mode}), by luminance of its own
background — the source repo carries no mode flag of its own to read.
${modeLine}
Source: https://github.com/${repo} (${source.kind}), read by scripts/port-community-themes.ts. Values
went through deriveOmarchyPalette (scripts/themes/from-ansi.ts) because a single-theme repo ships
per-app configs, not Omarchy 4's own colors.toml shape — some tokens here are reconstructed, not
authored. Every text/surface pair is contrast-checked the same way the official themes are.
${provenanceLine} Every text/surface pair is contrast-checked the same way the official themes are.
DO NOT EDIT BY HAND. Re-run the script to refresh from the source repo.
*/
+40 -2
View File
@@ -32,12 +32,14 @@ export type AnsiSource = {
brightWhite: string;
};
const HEX = /#[0-9a-fA-F]{3,8}/;
// `#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].slice(1).toLowerCase();
let h = m[0].replace(/^(#|0x)/, '').toLowerCase();
if (h.length === 3)
h = h
.split('')
@@ -165,3 +167,39 @@ export function parseGhosttyConf(text: string): AnsiSource | undefined {
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]!,
};
}
+308
View File
@@ -55,6 +55,13 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "andromeda",
"name": "Andromeda",
"repo": "signaldirective/andromeda",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "anonymous",
"name": "Anonymous",
@@ -97,6 +104,20 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "ash",
"name": "Ash",
"repo": "bjarneo/omarchy-ash-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "astrodark",
"name": "Astrodark",
"repo": "JamsMendez/omarchy-astrodark-theme",
"kind": "colors.toml (Omarchy schema)",
"dark": true
},
{
"id": "atari",
"name": "Atari",
@@ -195,6 +216,13 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "biscuit-de-mar-dark",
"name": "Biscuit DE Mar Dark",
"repo": "OldJobobo/omarchy-biscuit-de-mar-dark-theme",
"kind": "colors.toml (Omarchy schema)",
"dark": true
},
{
"id": "black-arch",
"name": "Black Arch",
@@ -202,6 +230,27 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "black-gold",
"name": "Black Gold",
"repo": "HANCORE-linux/omarchy-blackgold-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "black-money",
"name": "Black Money",
"repo": "HANCORE-linux/omarchy-blackmoney-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "blackturq",
"name": "Blackturq",
"repo": "HANCORE-linux/omarchy-blackturq-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "blueridge",
"name": "Blueridge",
@@ -230,6 +279,13 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "cattpuccin-glass",
"name": "Cattpuccin Glass",
"repo": "Luquatic/omarchy-catppuccin-glass",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "city-783",
"name": "City 783",
@@ -244,6 +300,13 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "corporate",
"name": "Corporate",
"repo": "defer/omarchy-corporate-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": false
},
{
"id": "cpunk",
"name": "Cpunk",
@@ -258,6 +321,20 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "crimson-gold",
"name": "Crimson Gold",
"repo": "knappkevin/omarchy-crimson-gold-theme",
"kind": "colors.toml (Omarchy schema)",
"dark": true
},
{
"id": "cyberpunk-2",
"name": "Cyberpunk 2",
"repo": "SeanAnd/omarchy-cyberpunk-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "dark-xp",
"name": "Dark XP",
@@ -307,6 +384,20 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "desert-gleam",
"name": "Desert Gleam",
"repo": "avis3nna/desert-gleam",
"kind": "colors.toml (16-colour ANSI)",
"dark": false
},
{
"id": "desert-twilight",
"name": "Desert Twilight",
"repo": "avis3nna/desert-twilight",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "dotrb",
"name": "Dotrb",
@@ -342,6 +433,13 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "elysian",
"name": "Elysian",
"repo": "bjarneo/omarchy-elysian-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "ember-n-ash",
"name": "Ember N Ash",
@@ -370,6 +468,13 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "evergarden",
"name": "Evergarden",
"repo": "celsobenedetti/omarchy-evergarden",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "farline",
"name": "Farline",
@@ -384,6 +489,27 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "fiery-ocean",
"name": "Fiery Ocean",
"repo": "bjarneo/omarchy-fiery-ocean-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "fireside",
"name": "Fireside",
"repo": "bjarneo/omarchy-fireside-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "firesky",
"name": "Firesky",
"repo": "bjarneo/omarchy-firesky-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "flat-dracula",
"name": "Flat Dracula",
@@ -391,6 +517,13 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "flexoki-dark",
"name": "Flexoki Dark",
"repo": "euandeas/omarchy-flexoki-dark-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "florida-man",
"name": "Florida Man",
@@ -412,6 +545,27 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "frost",
"name": "Frost",
"repo": "bjarneo/omarchy-frost-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "futurism",
"name": "Futurism",
"repo": "bjarneo/omarchy-futurism-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "ghost-pastel",
"name": "Ghost Pastel",
"repo": "row-huh/omarchy-ghost-pastel-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "glory-antic",
"name": "Glory Antic",
@@ -475,6 +629,13 @@
"kind": "alacritty.toml",
"dark": false
},
{
"id": "harbor-dark",
"name": "Harbor Dark",
"repo": "HANCORE-linux/omarchy-harbordark-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "hex",
"name": "Hex",
@@ -496,6 +657,13 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "hollow-knight",
"name": "Hollow Knight",
"repo": "bjarneo/omarchy-hollow-knight-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "infernium",
"name": "Infernium",
@@ -510,6 +678,20 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "inkypinky",
"name": "Inkypinky",
"repo": "HANCORE-linux/omarchy-inkypinky-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "kanso",
"name": "Kanso",
"repo": "HANCORE-linux/omarchy-kanso-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "killer-klowns",
"name": "Killer Klowns",
@@ -517,6 +699,20 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "kimiko",
"name": "Kimiko",
"repo": "krymzonn/omarchy-kimiko-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "koyanagi",
"name": "Koyanagi",
"repo": "YutaKoyanagi10/omarchy-koyanagi-theme",
"kind": "colors.toml (Omarchy schema)",
"dark": true
},
{
"id": "kurayami",
"name": "Kurayami",
@@ -650,6 +846,20 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "monolith-2",
"name": "Monolith 2",
"repo": "grvsnh/omarchy-monolith-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "moodpeak",
"name": "Moodpeak",
"repo": "HANCORE-linux/omarchy-moodpeak-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "nagai-poolside",
"name": "Nagai Poolside",
@@ -692,6 +902,13 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "nes",
"name": "Nes",
"repo": "bjarneo/omarchy-nes-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "nyc",
"name": "Nyc",
@@ -699,6 +916,13 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "one-dark-pro",
"name": "One Dark Pro",
"repo": "sc0ttman/omarchy-one-dark-pro",
"kind": "colors.toml (Omarchy schema)",
"dark": true
},
{
"id": "oxford",
"name": "Oxford",
@@ -720,6 +944,13 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "pina",
"name": "Pina",
"repo": "bjarneo/omarchy-pina-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "pink-blood",
"name": "Pink Blood",
@@ -727,6 +958,13 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "pmndrs",
"name": "Pmndrs",
"repo": "leweyse/omarchy-pmndrs-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "pulsar",
"name": "Pulsar",
@@ -776,6 +1014,13 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "reverie",
"name": "Reverie",
"repo": "bjarneo/omarchy-reverie-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "robco",
"name": "Robco",
@@ -790,6 +1035,13 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "rose-pine-dark",
"name": "Rose Pine Dark",
"repo": "guilhermetk/omarchy-rose-pine-dark",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "roseofdune",
"name": "Roseofdune",
@@ -811,6 +1063,48 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "saga",
"name": "Saga",
"repo": "HANCORE-linux/omarchy-saga-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "sakura",
"name": "Sakura",
"repo": "bjarneo/omarchy-sakura-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "sapphire",
"name": "Sapphire",
"repo": "HANCORE-linux/omarchy-sapphire-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "serenity",
"name": "Serenity",
"repo": "bjarneo/omarchy-serenity-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "shadesofjade",
"name": "Shadesofjade",
"repo": "HANCORE-linux/omarchy-shadesofjade-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "snow",
"name": "Snow",
"repo": "bjarneo/omarchy-snow-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": false
},
{
"id": "snow-black",
"name": "Snow Black",
@@ -860,6 +1154,13 @@
"kind": "alacritty.toml",
"dark": true
},
{
"id": "sunkissed",
"name": "Sunkissed",
"repo": "loeclos/omarchy-sunkissed-theme",
"kind": "alacritty.toml",
"dark": true
},
{
"id": "sunset",
"name": "Sunset",
@@ -1021,6 +1322,13 @@
"kind": "alacritty.toml",
"dark": false
},
{
"id": "x-1632",
"name": "X 1632",
"repo": "OldJobobo/omarchy-x-1632-theme",
"kind": "colors.toml (16-colour ANSI)",
"dark": true
},
{
"id": "yuugure",
"name": "Yuugure",
+18 -7
View File
@@ -38,21 +38,28 @@ export type Palette = {
brightMagenta: string;
};
/** A deliberately small TOML reader: this file is flat `key = "value"` and nothing else. */
function readToml(path: string): Record<string, string> {
/**
* A deliberately small TOML reader: this file is flat `key = "value"` and nothing else.
*
* Exported (not just used by `loadPalette` below) because a community theme can ship this exact shape
* too — see `port-community-themes.ts`, which reads it from a fetched string rather than a file path.
*/
export function parseTomlText(text: string): Record<string, string> {
const out: Record<string, string> = {};
for (const line of readFileSync(path, 'utf8').split('\n')) {
for (const line of text.split('\n')) {
const m = line.match(/^\s*([a-z_0-9]+)\s*=\s*"([^"]*)"/i);
if (m) out[m[1]!] = m[2]!;
// basecamp/omarchy's own files are already lowercase hex; a community `colors.toml` isn't guaranteed
// to be, so normalize here rather than leaving the two sources visibly inconsistent.
if (m) out[m[1]!] = m[2]!.toLowerCase();
}
return out;
}
export function loadPalette(tomlPath: string): Palette {
const t = readToml(tomlPath);
/** The 24-key contract, given an already-parsed record rather than a path — see `parseTomlText`. */
export function paletteFromRecord(t: Record<string, string>, label: string): Palette {
const need = (key: string): string => {
const v = t[key];
if (!v) throw new Error(`${tomlPath}: missing "${key}"`);
if (!v) throw new Error(`${label}: missing "${key}"`);
return v;
};
@@ -84,3 +91,7 @@ export function loadPalette(tomlPath: string): Palette {
brightMagenta: need('bright_magenta'),
};
}
export function loadPalette(tomlPath: string): Palette {
return paletteFromRecord(parseTomlText(readFileSync(tomlPath, 'utf8')), tomlPath);
}