145 lines
4.0 KiB
TypeScript
145 lines
4.0 KiB
TypeScript
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 matugenThemeWatch(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 () => {
|
|
// i wish this fucking worked
|
|
// pi.sendUserMessage("/reload", { deliverAs: "followUp" });
|
|
const theme = await loadThemeFromPath(THEME_PATH);
|
|
const res = ctx.ui.setTheme(theme);
|
|
if (res.success) {
|
|
ctx.ui.notify("Background changed. Colors: ", "info");
|
|
} else {
|
|
ctx.ui.notify("Theme update failed, fuck", "error");
|
|
}
|
|
}, 150);
|
|
},
|
|
);
|
|
} catch (error) {
|
|
ctx.ui.notify(`Theme watch failed: ${String(error)}`, "error");
|
|
}
|
|
});
|
|
|
|
pi.on("session_shutdown", () => {
|
|
if (watcher) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- weird types
|
|
fs.unwatchFile(THEME_PATH, watcher as any);
|
|
watcher = null;
|
|
}
|
|
if (debounce) {
|
|
clearTimeout(debounce);
|
|
debounce = null;
|
|
}
|
|
});
|
|
}
|