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:
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user