feat: detect first-run terminal theme (#5385)

This commit is contained in:
Vegard Stikbakke
2026-06-14 07:14:42 +02:00
committed by GitHub
parent 3fcfb7abf7
commit f0989800cb
10 changed files with 532 additions and 101 deletions

View File

@@ -1,24 +1,24 @@
import { resetCapabilitiesCache, setCapabilities } from "@earendil-works/pi-tui";
import { type RgbColor, resetCapabilitiesCache, setCapabilities } from "@earendil-works/pi-tui";
import { afterEach, describe, expect, it } from "vitest";
import {
detectTerminalBackground,
detectTerminalBackgroundFromEnv,
detectTerminalBackgroundTheme,
getThemeByName,
getThemeForRgbColor,
parseOsc11BackgroundColor,
} from "../src/modes/interactive/theme/theme.ts";
afterEach(() => {
resetCapabilitiesCache();
});
describe("detectTerminalBackground", () => {
describe("detectTerminalBackgroundFromEnv", () => {
it("uses the COLORFGBG background color index", () => {
expect(detectTerminalBackground({ env: { COLORFGBG: "0;15" } })).toMatchObject({
expect(detectTerminalBackgroundFromEnv({ env: { COLORFGBG: "0;15" } })).toMatchObject({
theme: "light",
source: "COLORFGBG",
confidence: "high",
});
expect(detectTerminalBackground({ env: { COLORFGBG: "15;0" } })).toMatchObject({
expect(detectTerminalBackgroundFromEnv({ env: { COLORFGBG: "15;0" } })).toMatchObject({
theme: "dark",
source: "COLORFGBG",
confidence: "high",
@@ -26,11 +26,11 @@ describe("detectTerminalBackground", () => {
});
it("uses the last COLORFGBG field as the background", () => {
expect(detectTerminalBackground({ env: { COLORFGBG: "0;7;15" } }).theme).toBe("light");
expect(detectTerminalBackgroundFromEnv({ env: { COLORFGBG: "0;7;15" } }).theme).toBe("light");
});
it("defaults to dark without terminal background hints", () => {
expect(detectTerminalBackground({ env: {} })).toMatchObject({
expect(detectTerminalBackgroundFromEnv({ env: {} })).toMatchObject({
theme: "dark",
source: "fallback",
confidence: "low",
@@ -38,6 +38,65 @@ describe("detectTerminalBackground", () => {
});
});
describe("detectTerminalBackgroundTheme", () => {
it("uses the queried terminal background before environment hints", async () => {
let queriedTimeoutMs: number | undefined;
const detection = await detectTerminalBackgroundTheme({
env: { COLORFGBG: "15;0" },
timeoutMs: 250,
ui: {
async queryTerminalBackgroundColor({ timeoutMs }: { timeoutMs: number }): Promise<RgbColor | undefined> {
queriedTimeoutMs = timeoutMs;
return { r: 250, g: 250, b: 250 };
},
},
});
expect(queriedTimeoutMs).toBe(250);
expect(detection).toMatchObject({
theme: "light",
source: "terminal background",
confidence: "high",
});
});
it("falls back to environment hints when the terminal query returns no color", async () => {
const detection = await detectTerminalBackgroundTheme({
env: { COLORFGBG: "15;0" },
timeoutMs: 250,
ui: {
async queryTerminalBackgroundColor(): Promise<RgbColor | undefined> {
return undefined;
},
},
});
expect(detection).toMatchObject({
theme: "dark",
source: "COLORFGBG",
confidence: "high",
});
});
it("falls back to environment hints when the terminal query fails", async () => {
const detection = await detectTerminalBackgroundTheme({
env: { COLORFGBG: "0;15" },
timeoutMs: 250,
ui: {
async queryTerminalBackgroundColor(): Promise<RgbColor | undefined> {
throw new Error("terminal write failed");
},
},
});
expect(detection).toMatchObject({
theme: "light",
source: "COLORFGBG",
confidence: "high",
});
});
});
describe("theme color mode", () => {
it("uses terminal capabilities", () => {
setCapabilities({ images: null, trueColor: false, hyperlinks: false });
@@ -54,16 +113,7 @@ describe("theme color mode", () => {
});
});
describe("parseOsc11BackgroundColor", () => {
it("parses 16-bit OSC 11 rgb responses", () => {
expect(parseOsc11BackgroundColor("\x1b]11;rgb:0000/8000/ffff\x07")).toEqual({ r: 0, g: 128, b: 255 });
});
it("parses OSC 11 hex responses", () => {
expect(parseOsc11BackgroundColor("\x1b]11;#ffffff\x1b\\")).toEqual({ r: 255, g: 255, b: 255 });
expect(parseOsc11BackgroundColor("\x1b]11;#000000\x07")).toEqual({ r: 0, g: 0, b: 0 });
});
describe("theme detection from RGB", () => {
it("classifies RGB colors by luminance", () => {
expect(getThemeForRgbColor({ r: 8, g: 8, b: 8 })).toBe("dark");
expect(getThemeForRgbColor({ r: 250, g: 250, b: 250 })).toBe("light");