import type { Palette } from './palette'; import type { AnsiSource } from './ansi-source'; import { UI, contrast, ensureContrast, luminance, mix, parseHex } from './contrast'; // An `AnsiSource` (16 ANSI slots + primary bg/fg) has no notion of "three background depths" or "an // accent" — Omarchy 4's `colors.toml` is a richer, hand-curated shape that a sixteen-slot terminal scheme // was never designed to fill. This derives one anyway, by role rather than by guessing a fixed value: // // - accent is whichever blue-ish slot actually stands out from the background (every official palette // picks blue too — this just guards against a scheme that reused the background for it). // - the three depths are the background pushed a fixed distance toward black or white, in the direction // each mode's surfaces already move (dark themes rise lighter, light themes recede darker — see the // ratios comment below, calibrated against Nord and Rosé Pine's real colors.toml). // - the three foreground weights are the foreground pulled toward the background by a fixed fraction, // standing in for "faint", "muted" and "strong" text. // // None of this has to be exact. `mapPalette`'s `ensureContrast` re-checks every text/surface pair // afterward and nudges anything that fails — the approximation only has to be plausible, not correct. function pickAccent(a: AnsiSource): string { const candidates = [a.blue, a.brightBlue, a.cyan, a.brightCyan]; const best = candidates.reduce((hi, c) => (contrast(c, a.background) > contrast(hi, a.background) ? c : hi)); // `--ring` is this colour laid directly on `--background` (see `map.ts`), and it's in the contract's // PAIRS check same as the official themes — so guarantee it the way `ensureContrast` guarantees text, // rather than only guarding against a background-coloured accent. return ensureContrast(best, a.background, UI); } export function deriveOmarchyPalette(a: AnsiSource): Palette { const dark = luminance(parseHex(a.background)) < 0.5; const mode: Palette['mode'] = dark ? 'dark' : 'light'; const darkerBackground = dark ? mix(a.background, '#000000', 0.45) : mix(a.background, '#000000', 0.1); const darkBackground = dark ? mix(a.background, '#000000', 0.22) : mix(a.background, '#000000', 0.06); const lighterBackground = dark ? mix(a.background, '#ffffff', 0.07) : mix(a.background, '#000000', 0.035); // Faintest → strongest, all pulled toward the background rather than toward a fixed grey, so a warm // palette's dim text stays warm instead of turning neutral. const darkForeground = mix(a.foreground, a.background, 0.55); const lightForeground = mix(a.foreground, a.background, 0.3); const accent = pickAccent(a); const muted = contrast(a.brightBlack, a.background) >= 1.15 ? a.brightBlack : mix(a.foreground, a.background, 0.6); return { mode, accent, // Unused by `mapPalette` today (grep confirms `p.selection` has no reader) — kept only because the // `Palette` type requires it. selection: mix(a.background, a.foreground, 0.2), muted, background: a.background, darkBackground, darkerBackground, lighterBackground, foreground: a.foreground, darkForeground, lightForeground, brightForeground: a.foreground, red: a.red, yellow: a.yellow, green: a.green, cyan: a.cyan, blue: a.blue, magenta: a.magenta, // Standard ANSI has no orange slot. Mixing red and yellow reads better as "busy" than falling back to // plain yellow the way `loadPalette` does for the 3 official themes that omit it. orange: mix(a.red, a.yellow, 0.5), brightRed: a.brightRed, brightYellow: a.brightYellow, brightGreen: a.brightGreen, brightCyan: a.brightCyan, brightBlue: a.brightBlue, brightMagenta: a.brightMagenta, }; }