From a9362d767ebd0f2a44f85e19ee4eeaef12379d26 Mon Sep 17 00:00:00 2001 From: "Thomas G. Lopes" Date: Thu, 19 Feb 2026 23:59:42 +0000 Subject: [PATCH] pi matugen theme --- matugen/files/config.toml | 5 + matugen/files/templates/pi-theme.json | 88 +++++++++++ .../agent/extensions/matugen-theme-watch.ts | 143 ++++++++++++++++++ pi/files/agent/settings.json | 6 +- 4 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 matugen/files/templates/pi-theme.json create mode 100644 pi/files/agent/extensions/matugen-theme-watch.ts diff --git a/matugen/files/config.toml b/matugen/files/config.toml index ad5335e..1a7f010 100644 --- a/matugen/files/config.toml +++ b/matugen/files/config.toml @@ -46,3 +46,8 @@ output_path = '~/.config/jjui/themes/matugen.toml' input_path = '~/.config/matugen/templates/neovim.lua' output_path = '~/.config/nvim/lua/plugins/dankcolors.lua' post_hook = 'nohup ~/.config/matugen/scripts/sync-nvim-mac.sh >/dev/null 2>&1 &' + +[templates.pi] +input_path = '~/.config/matugen/templates/pi-theme.json' +output_path = '~/.pi/agent/themes/matugen.json.tmp' +post_hook = 'cat ~/.pi/agent/themes/matugen.json.tmp > ~/.pi/agent/themes/matugen.json' diff --git a/matugen/files/templates/pi-theme.json b/matugen/files/templates/pi-theme.json new file mode 100644 index 0000000..a31e5ee --- /dev/null +++ b/matugen/files/templates/pi-theme.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json", + "name": "matugen", + "vars": { + "bg": "{{colors.surface.default.hex}}", + "bgAlt": "{{colors.surface_container.default.hex}}", + "fg": "{{colors.on_surface.default.hex}}", + "muted": "{{colors.on_surface_variant.default.hex}}", + "dim": "{{colors.outline.default.hex}}", + "accent": "{{colors.primary.default.hex}}", + "border": "{{colors.primary.default.hex}}", + "borderAccent": "{{colors.secondary.default.hex}}", + "borderMuted": "{{colors.outline_variant.default.hex}}", + "success": "{{colors.tertiary.default.hex}}", + "error": "{{colors.error.default.hex}}", + "warning": "{{colors.primary_fixed.default.hex}}", + "selectedBg": "{{colors.surface_container_high.default.hex}}", + "userMsgBg": "{{colors.surface_container_high.default.hex}}", + "toolPendingBg": "{{colors.surface_container.default.hex}}", + "toolSuccessBg": "{{colors.tertiary_container.default.hex}}", + "toolErrorBg": "{{colors.error_container.default.hex}}", + "customMsgBg": "{{colors.surface_container_high.default.hex}}" + }, + "colors": { + "accent": "accent", + "border": "border", + "borderAccent": "borderAccent", + "borderMuted": "borderMuted", + "success": "success", + "error": "error", + "warning": "warning", + "muted": "muted", + "dim": "dim", + "text": "", + "thinkingText": "muted", + + "selectedBg": "selectedBg", + "userMessageBg": "userMsgBg", + "userMessageText": "", + "customMessageBg": "customMsgBg", + "customMessageText": "", + "customMessageLabel": "{{colors.secondary.default.hex}}", + "toolPendingBg": "toolPendingBg", + "toolSuccessBg": "toolSuccessBg", + "toolErrorBg": "toolErrorBg", + "toolTitle": "", + "toolOutput": "muted", + + "mdHeading": "{{colors.primary.default.hex}}", + "mdLink": "{{colors.secondary.default.hex}}", + "mdLinkUrl": "muted", + "mdCode": "accent", + "mdCodeBlock": "{{colors.tertiary.default.hex}}", + "mdCodeBlockBorder": "muted", + "mdQuote": "muted", + "mdQuoteBorder": "muted", + "mdHr": "muted", + "mdListBullet": "accent", + + "toolDiffAdded": "{{colors.tertiary.default.hex}}", + "toolDiffRemoved": "{{colors.error.default.hex}}", + "toolDiffContext": "muted", + + "syntaxComment": "muted", + "syntaxKeyword": "{{colors.primary.default.hex}}", + "syntaxFunction": "{{colors.secondary.default.hex}}", + "syntaxVariable": "fg", + "syntaxString": "{{colors.tertiary.default.hex}}", + "syntaxNumber": "{{colors.primary_fixed.default.hex}}", + "syntaxType": "{{colors.secondary.default.hex}}", + "syntaxOperator": "{{colors.on_surface_variant.default.hex}}", + "syntaxPunctuation": "{{colors.on_surface_variant.default.hex}}", + + "thinkingOff": "borderMuted", + "thinkingMinimal": "dim", + "thinkingLow": "{{colors.primary.default.hex}}", + "thinkingMedium": "{{colors.secondary.default.hex}}", + "thinkingHigh": "{{colors.tertiary.default.hex}}", + "thinkingXhigh": "{{colors.error.default.hex}}", + + "bashMode": "success" + }, + "export": { + "pageBg": "{{colors.surface.default.hex}}", + "cardBg": "{{colors.surface_container.default.hex}}", + "infoBg": "{{colors.surface_container_high.default.hex}}" + } +} diff --git a/pi/files/agent/extensions/matugen-theme-watch.ts b/pi/files/agent/extensions/matugen-theme-watch.ts new file mode 100644 index 0000000..7447cc7 --- /dev/null +++ b/pi/files/agent/extensions/matugen-theme-watch.ts @@ -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; + colors: Record; +}; + +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, + visited = new Set(), +): 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, + vars: Record = {}, +): Record { + const resolved: Record = {}; + for (const [key, value] of Object.entries(colors)) { + resolved[key] = resolveVarRefs(value, vars); + } + return resolved; +} + +async function loadThemeFromPath(filePath: string): Promise { + 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 = {}; + const bgColors: Record = {}; + 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; + } + }); +} diff --git a/pi/files/agent/settings.json b/pi/files/agent/settings.json index 7122216..93aa2b1 100644 --- a/pi/files/agent/settings.json +++ b/pi/files/agent/settings.json @@ -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" } \ No newline at end of file