pi matugen theme

This commit is contained in:
2026-02-19 23:59:42 +00:00
parent 371ab008ae
commit a9362d767e
4 changed files with 239 additions and 3 deletions

View File

@@ -0,0 +1,143 @@
import fs from "node:fs";
import fsp from "node:fs/promises";
import path from "node:path";
import type {
ExtensionAPI,
Theme as PiTheme,
} from "@mariozechner/pi-coding-agent";
import { Theme } from "@mariozechner/pi-coding-agent";
const THEME_NAME = "matugen";
const THEME_PATH = path.join(
process.env.HOME ?? "",
".pi/agent/themes/matugen.json",
);
const POLL_INTERVAL_MS = 500;
type ColorValue = string | number;
type ThemeJson = {
name: string;
vars?: Record<string, ColorValue>;
colors: Record<string, ColorValue>;
};
const BG_KEYS = new Set([
"selectedBg",
"userMessageBg",
"customMessageBg",
"toolPendingBg",
"toolSuccessBg",
"toolErrorBg",
]);
function detectColorMode(): "truecolor" | "256color" {
const colorterm = process.env.COLORTERM;
if (colorterm === "truecolor" || colorterm === "24bit") return "truecolor";
if (process.env.WT_SESSION) return "truecolor";
const term = process.env.TERM || "";
if (term === "dumb" || term === "" || term === "linux") return "256color";
if (process.env.TERM_PROGRAM === "Apple_Terminal") return "256color";
return "truecolor";
}
function resolveVarRefs(
value: ColorValue,
vars: Record<string, ColorValue>,
visited = new Set<string>(),
): ColorValue {
if (
typeof value === "number" ||
value === "" ||
(typeof value === "string" && value.startsWith("#"))
) {
return value;
}
if (typeof value !== "string") {
throw new Error(`Invalid color value: ${String(value)}`);
}
if (visited.has(value)) {
throw new Error(`Circular variable reference: ${value}`);
}
if (!(value in vars)) {
throw new Error(`Variable reference not found: ${value}`);
}
visited.add(value);
return resolveVarRefs(vars[value], vars, visited);
}
function resolveThemeColors(
colors: Record<string, ColorValue>,
vars: Record<string, ColorValue> = {},
): Record<string, ColorValue> {
const resolved: Record<string, ColorValue> = {};
for (const [key, value] of Object.entries(colors)) {
resolved[key] = resolveVarRefs(value, vars);
}
return resolved;
}
async function loadThemeFromPath(filePath: string): Promise<PiTheme> {
const content = await fsp.readFile(filePath, "utf-8");
const parsed = JSON.parse(content) as ThemeJson;
const resolved = resolveThemeColors(parsed.colors, parsed.vars ?? {});
const fgColors: Record<string, ColorValue> = {};
const bgColors: Record<string, ColorValue> = {};
for (const [key, value] of Object.entries(resolved)) {
if (BG_KEYS.has(key)) {
bgColors[key] = value;
} else {
fgColors[key] = value;
}
}
return new Theme(fgColors as never, bgColors as never, detectColorMode(), {
name: parsed.name ?? THEME_NAME,
sourcePath: filePath,
});
}
export default function (pi: ExtensionAPI) {
let watcher: fs.StatWatcher | null = null;
let debounce: NodeJS.Timeout | null = null;
pi.on("session_start", (_event, ctx) => {
try {
watcher = fs.watchFile(
THEME_PATH,
{ interval: POLL_INTERVAL_MS },
(curr, prev) => {
if (curr.mtimeMs === prev.mtimeMs) return;
if (debounce) {
clearTimeout(debounce);
}
debounce = setTimeout(async () => {
try {
const theme = await loadThemeFromPath(THEME_PATH);
const result = ctx.ui.setTheme(theme);
if (!result.success && result.error) {
ctx.ui.notify(`Theme reload failed: ${result.error}`, "error");
}
} catch (error) {
ctx.ui.notify(`Theme reload failed: ${String(error)}`, "error");
}
}, 150);
},
);
} catch (error) {
ctx.ui.notify(`Theme watch failed: ${String(error)}`, "error");
}
});
pi.on("session_shutdown", () => {
if (watcher) {
fs.unwatchFile(THEME_PATH, watcher);
watcher = null;
}
if (debounce) {
clearTimeout(debounce);
debounce = null;
}
});
}

View File

@@ -1,7 +1,7 @@
{
"lastChangelogVersion": "0.53.0",
"lastChangelogVersion": "0.54.0",
"defaultProvider": "openrouter",
"defaultModel": "openai/gpt-5.2-codex",
"defaultThinkingLevel": "minimal",
"theme": "dark"
"defaultThinkingLevel": "high",
"theme": "matugen"
}