chore(vendor): import pi 1.3.0

Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
2026-10-15 17:58:48 -04:00
parent 719370c8f7
commit 6081e3e325
1743 changed files with 205460 additions and 131705 deletions

View File

@@ -1,12 +1,22 @@
import type { TUI } from "@earendil-works/pi-tui";
import type { TerminalColors, TUI } from "@earendil-works/pi-tui";
import { afterEach, describe, expect, it, vi } from "vitest";
import { SettingsManager } from "../src/core/settings-manager.ts";
import { initTheme, type TerminalTheme, theme } from "../src/modes/interactive/theme/theme.ts";
import {
initTheme,
setTerminalColorScheme,
setTerminalColors,
type TerminalTheme,
theme,
} from "../src/modes/interactive/theme/theme.ts";
import { InteractiveThemeController } from "../src/modes/interactive/theme/theme-controller.ts";
const DARK: TerminalColors = { foreground: { r: 248, g: 248, b: 242 }, background: { r: 40, g: 42, b: 54 } };
const LIGHT: TerminalColors = { foreground: { r: 30, g: 30, b: 30 }, background: { r: 250, g: 250, b: 250 } };
type ColorQueryOptions = { timeoutMs: number; onLateReply?: (colors: TerminalColors) => void };
function createUi() {
const queryTerminalBackgroundColor = vi.fn();
const queryTerminalColorScheme = vi.fn();
const queryTerminalColors = vi.fn(async (_options: ColorQueryOptions): Promise<TerminalColors> => ({}));
const setTerminalColorSchemeNotifications = vi.fn();
let terminalColorSchemeListener: ((terminalTheme: TerminalTheme) => void) | undefined;
const unsubscribeTerminalColorScheme = vi.fn();
@@ -18,13 +28,11 @@ function createUi() {
terminalColorSchemeListener = listener;
return unsubscribeTerminalColorScheme;
}),
queryTerminalBackgroundColor,
queryTerminalColorScheme,
queryTerminalColors,
} as unknown as TUI;
return {
ui,
queryTerminalBackgroundColor,
queryTerminalColorScheme,
queryTerminalColors,
setTerminalColorSchemeNotifications,
unsubscribeTerminalColorScheme,
emitTerminalColorScheme: (terminalTheme: TerminalTheme) => terminalColorSchemeListener?.(terminalTheme),
@@ -40,51 +48,119 @@ function createController(ui: TUI, getSettingsManager: () => SettingsManager, in
});
}
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
afterEach(() => {
setTerminalColors({});
setTerminalColorScheme(undefined);
initTheme("dark");
vi.unstubAllEnvs();
});
describe("InteractiveThemeController", () => {
it("uses the initial theme without persisting it", async () => {
const { ui, queryTerminalBackgroundColor } = createUi();
const { ui, queryTerminalColors } = createUi();
const manager = SettingsManager.inMemory({ theme: "dark" });
const setTheme = vi.spyOn(manager, "setTheme");
const flush = vi.spyOn(manager, "flush");
const flushSettings = vi.spyOn(manager, "flush");
const controller = createController(ui, () => manager, "light");
expect(theme.name).toBe("light");
expect(controller.getThemeSelection()).toBe("light");
await controller.applyFromSettings();
controller.applyFromSettings();
await flush();
expect(queryTerminalBackgroundColor).not.toHaveBeenCalled();
expect(queryTerminalColors).toHaveBeenCalledOnce();
expect(setTheme).not.toHaveBeenCalled();
expect(flush).not.toHaveBeenCalled();
expect(flushSettings).not.toHaveBeenCalled();
});
it("resolves a theme pair and follows terminal appearance changes", async () => {
vi.stubEnv("COLORFGBG", "15;0");
const { ui, queryTerminalColorScheme, setTerminalColorSchemeNotifications, emitTerminalColorScheme } = createUi();
queryTerminalColorScheme.mockResolvedValue("light");
const manager = SettingsManager.inMemory({ theme: "dark/light" });
const controller = createController(ui, () => manager, "light/dark");
it("applies the theme immediately and lets startup wait for the colors", async () => {
const { ui, queryTerminalColors } = createUi();
let answer: (colors: TerminalColors) => void = () => {};
queryTerminalColors.mockReturnValue(
new Promise((resolve) => {
answer = resolve;
}),
);
const controller = createController(ui, () => SettingsManager.inMemory());
controller.applyFromSettings();
expect(theme.name).toBe("dark");
await controller.applyFromSettings();
expect(theme.name).toBe("light");
// Grayscale until the terminal answers.
expect(theme.name).toBe("system");
expect(theme.getFgAnsi("error")).toBe("\x1b[39m");
answer(DARK);
await controller.waitForTerminalColors();
expect(theme.getFgAnsi("error")).toMatch(/^\x1b\[38;/);
});
it("falls back to palette indices, then applies colors that arrive after the timeout", async () => {
const { ui, queryTerminalColors } = createUi();
let lateReply: (colors: TerminalColors) => void = () => {};
queryTerminalColors.mockImplementation(async (options) => {
lateReply = options.onLateReply!;
return {};
});
const controller = createController(ui, () => SettingsManager.inMemory());
controller.applyFromSettings();
await flush();
expect(theme.getFgAnsi("error")).toBe("\x1b[38;5;1m");
lateReply(DARK);
expect(theme.colors.error.kind).toBe("rgb");
});
it("re-queries the colors on appearance changes and lets them decide", async () => {
const { ui, queryTerminalColors, setTerminalColorSchemeNotifications, emitTerminalColorScheme } = createUi();
queryTerminalColors.mockResolvedValue(LIGHT);
const controller = createController(ui, () => SettingsManager.inMemory(), "light/dark");
controller.applyFromSettings();
expect(setTerminalColorSchemeNotifications).toHaveBeenCalledWith(true);
await flush();
expect(theme.name).toBe("light");
emitTerminalColorScheme("dark");
queryTerminalColors.mockResolvedValue(DARK);
// The report says light, but the terminal renders dark.
emitTerminalColorScheme("light");
await flush();
expect(theme.name).toBe("dark");
});
it("uses the reported scheme for the system theme when the terminal reports no colors", async () => {
vi.stubEnv("COLORFGBG", "");
const { ui, emitTerminalColorScheme } = createUi();
const controller = createController(ui, () => SettingsManager.inMemory());
controller.applyFromSettings();
await flush();
expect(theme.appearance).toBe("dark");
emitTerminalColorScheme("light");
expect(theme.appearance).toBe("light");
expect(controller.getTerminalTheme()).toBe("light");
});
it("re-renders only when the reported colors change", async () => {
const { ui, queryTerminalColors } = createUi();
const controller = createController(ui, () => SettingsManager.inMemory({ theme: "dark" }));
const query = async (colors: TerminalColors) => {
queryTerminalColors.mockResolvedValue(colors);
controller.applyFromSettings();
await flush();
};
await query(DARK);
// A timeout keeps the known colors; erasing them would count as a change and re-render.
await query({});
await query(structuredClone(DARK));
expect(ui.requestRender).toHaveBeenCalledOnce();
});
it("disables terminal appearance updates when disposed", async () => {
const { ui, queryTerminalColorScheme, setTerminalColorSchemeNotifications, unsubscribeTerminalColorScheme } =
createUi();
queryTerminalColorScheme.mockResolvedValue("light");
const manager = SettingsManager.inMemory({ theme: "light/dark" });
const controller = createController(ui, () => manager);
await controller.applyFromSettings();
const { ui, setTerminalColorSchemeNotifications, unsubscribeTerminalColorScheme } = createUi();
const controller = createController(ui, () => SettingsManager.inMemory({ theme: "light/dark" }));
controller.applyFromSettings();
await flush();
controller.dispose();
@@ -92,30 +168,18 @@ describe("InteractiveThemeController", () => {
expect(unsubscribeTerminalColorScheme).toHaveBeenCalledOnce();
});
it("detects the current terminal appearance when selecting a theme pair", async () => {
vi.stubEnv("COLORFGBG", "");
const { ui, queryTerminalColorScheme } = createUi();
queryTerminalColorScheme.mockResolvedValue("light");
const manager = SettingsManager.inMemory({ theme: "dark" });
const controller = createController(ui, () => manager);
expect(theme.name).toBe("dark");
await controller.setThemeSetting("light/dark");
expect(theme.name).toBe("light");
expect(queryTerminalColorScheme).toHaveBeenCalledOnce();
});
it("lets an explicit selection replace the initial theme", async () => {
const { ui } = createUi();
const firstManager = SettingsManager.inMemory({ theme: "dark" });
const secondManager = SettingsManager.inMemory({ theme: "light" });
let manager = firstManager;
const controller = createController(ui, () => manager, "light");
await controller.applyFromSettings();
controller.applyFromSettings();
expect(controller.setThemeName("dark")).toEqual({ success: true });
manager = secondManager;
await controller.applyFromSettings();
controller.applyFromSettings();
await flush();
expect(controller.getThemeSelection()).toBe("dark");
expect(theme.name).toBe("dark");
@@ -127,15 +191,15 @@ describe("InteractiveThemeController", () => {
const secondManager = SettingsManager.inMemory({ theme: "light" });
let manager = firstManager;
const controller = createController(ui, () => manager);
await controller.applyFromSettings();
controller.applyFromSettings();
firstManager.applyOverrides({ theme: "light" });
await controller.applyFromSettings();
controller.applyFromSettings();
expect(theme.name).toBe("light");
secondManager.applyOverrides({ theme: "dark" });
manager = secondManager;
await controller.applyFromSettings();
controller.applyFromSettings();
expect(theme.name).toBe("dark");
});
});