import type { Rgb } from "./ghostty.ts"; import { TERMINAL_PALETTE } from "@berry/util/palette.ts"; const rgb = (hex: string): Rgb => [ parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16), ]; /** berry's colors, as programs see them when they ask, and as the view draws their defaults. */ export const COLORS = { foreground: rgb("#f3eef2"), background: rgb("#100e11"), cursor: rgb("#ff9df5"), palette: TERMINAL_PALETTE.map(rgb), }; /** Below this, text is hard to read on its background: ghostty's suggestion for a `minimum-contrast` that ensures it. */ const MINIMUM_CONTRAST = 3; const luminance = (color: Rgb) => { const [r, g, b] = color.map((c) => { const s = c / 255; return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4; }); return 0.2126 * r + 0.7152 * g + 0.0722 * b; }; /** WCAG 2's contrast ratio between two colors, from 1 for none to 21. */ export function contrast(a: Rgb, b: Rgb): number { const [lighter, darker] = [luminance(a), luminance(b)].sort((x, y) => y - x); return (lighter + 0.05) / (darker + 0.05); } /** * The color to draw text of `color` in on `background`: itself if it reads there, else moved toward white or black, * whichever stands out more there, just far enough that it does, so black on a dark background is a dark gray. */ export function readable(color: Rgb, background: Rgb): Rgb { if (contrast(color, background) >= MINIMUM_CONTRAST) return color; const toward = contrast([255, 255, 255], background) >= contrast([0, 0, 0], background) ? 255 : 0; const mixed = (t: number): Rgb => color.map((c) => Math.round(c + (toward - c) * t)) as never; // the least mix that reads, found by halving let [low, high] = [0, 1]; for (let i = 0; i < 16; i++) { const middle = (low + high) / 2; if (contrast(mixed(middle), background) >= MINIMUM_CONTRAST) high = middle; else low = middle; } return mixed(high); }