Open History to see the last commit
1import { getResolvedOrResolveTheme, registerCustomTheme, type DiffsTheme } from "@pierre/diffs";
2
3/** pierre-light with every syntax color darkened until it reads on every light code surface. */
4export const READABLE_LIGHT_THEME_NAME = "t3-light";
5
6const MIN_CONTRAST = 4.5;
7// The darkest light code surface is a changed word on a deleted line (#f8d3d4, luminance
8// 0.71), rounded down for margin.
9const DARKEST_SURFACE_LUMINANCE = 0.69;
10
11function channelLuminance(channel: number): number {
12 const value = channel / 255;
13 return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
14}
15
16function contrastOnLightCode(rgb: readonly [number, number, number]): number {
17 const [r, g, b] = rgb.map(channelLuminance) as [number, number, number];
18 return (DARKEST_SURFACE_LUMINANCE + 0.05) / (0.2126 * r + 0.7152 * g + 0.0722 * b + 0.05);
19}
20
21/** Darkens a #rrggbb(aa) color in steps that keep its hue until it reads on light code. */
22export function readableOnLightCode(color: string, minContrast = MIN_CONTRAST): string {
23 const match = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(color);
24 if (!match) return color;
25 let rgb = [0, 2, 4].map((start) => Number.parseInt(match[1]!.slice(start, start + 2), 16)) as [
26 number,
27 number,
28 number,
29 ];
30 if (contrastOnLightCode(rgb) >= minContrast) return color;
31 while (contrastOnLightCode(rgb) < minContrast) {
32 rgb = rgb.map((channel) => Math.floor(channel * 0.96)) as [number, number, number];
33 }
34 return `#${rgb.map((channel) => channel.toString(16).padStart(2, "0")).join("")}${match[2] ?? ""}`;
35}
36
37type ZedTheme = NonNullable<DiffsTheme["zed"]>;
38
39// Pairs that differ only in lightness would merge at one floor. The darker color of each pair
40// goes further, keeping pierre-light's order: numbers stay darker than operators, punctuation
41// darker than comments, and decorators darker than property names.
42const FURTHER_COLORS = new Set(["#1ca1c7", "#636363", "#1a85d4"]);
43const FURTHER_MIN_CONTRAST = 6;
44
45/** The t3-light color for a pierre-light syntax color. */
46export function readableSyntaxColor(color: string): string {
47 return readableOnLightCode(
48 color,
49 FURTHER_COLORS.has(color.toLowerCase()) ? FURTHER_MIN_CONTRAST : MIN_CONTRAST,
50 );
51}
52
53function withReadableSyntax(theme: ZedTheme): ZedTheme {
54 const syntax = Object.fromEntries(
55 Object.entries(theme.style.syntax ?? {}).map(([scope, style]) => [
56 scope,
57 typeof style === "string"
58 ? readableSyntaxColor(style)
59 : style.color
60 ? { ...style, color: readableSyntaxColor(style.color) }
61 : style,
62 ]),
63 );
64 return { ...theme, name: READABLE_LIGHT_THEME_NAME, style: { ...theme.style, syntax } };
65}
66
67// Themes resolve on the main thread and reach the diff workers already resolved.
68registerCustomTheme(
69 READABLE_LIGHT_THEME_NAME,
70 async () => {
71 const base = await getResolvedOrResolveTheme("pierre-light", "highlights");
72 if (!base.zed) throw new Error("pierre-light has no Highlights palette");
73 return withReadableSyntax(base.zed);
74 },
75 "zed",
76);