import type { Theme } from "@earendil-works/pi-coding-agent"; type HudForegroundStyle = { name: string; open: string; close: string; }; const ANSI_FOREGROUND_STYLES: Record = { context: ansiFgStyle("green", "32"), usage: ansiFgStyle("brightBlue", "94"), critical: ansiFgStyle("red", "31"), label: ansiTextStyle("dim", "2", "22"), model: ansiFgStyle("cyan", "36"), accent: ansiFgStyle("cyan", "36"), cyan: ansiFgStyle("cyan", "36"), project: ansiFgStyle("yellow", "33"), custom: ansiFgStyle("208", "38;5;208"), warning: ansiFgStyle("yellow", "33"), git: ansiFgStyle("magenta", "35"), gitBranch: ansiFgStyle("cyan", "36"), success: ansiFgStyle("green", "32"), error: ansiFgStyle("red", "31"), dim: ansiTextStyle("dim", "2", "22"), muted: ansiTextStyle("dim", "2", "22"), usageBar: ansiFgStyle("brightBlue", "94"), usageWarning: ansiFgStyle("brightMagenta", "95"), text: ansiFgStyle("default", "39"), }; const SOLARIZED_LIGHT_HUD_BACKGROUND_COLORS: Record = { contextBarEmpty: "#EEEACB", barEmpty: "#EEEAD9", }; const SOLARIZED_DARK_HUD_BACKGROUND_COLORS: Record = { contextBarEmpty: "#073642", barEmpty: "#073642", }; export function fg(theme: Theme, color: string, text: string): string { const style = ANSI_FOREGROUND_STYLES[color]; if (style && hudColorEnabled()) { return `${style.open}${text}${style.close}`; } const mapped = mapColor(color); try { return theme.fg(mapped as any, text); } catch { return text; } } export function bg(theme: Theme, color: string, text: string): string { const hex = resolveHudBackgroundColorForTheme(theme, color); if (hex && hudColorEnabled()) { return ansiBg(hex, text); } return text; } export function label(theme: Theme, text: string): string { return fg(theme, "label", text); } export function resolveHudColorForTheme(theme: Theme, color: string): string | undefined { return ANSI_FOREGROUND_STYLES[color]?.name ?? resolveHudBackgroundColorForTheme(theme, color); } export function resolveUsageColor(percent: number): string { if (percent >= 90) return "critical"; if (percent >= 75) return "usageWarning"; return "usage"; } export function resolveContextColor(percent: number | null): string { if (percent === null) return "label"; if (percent >= 85) return "critical"; if (percent >= 70) return "warning"; return "context"; } export function bar(theme: Theme, kind: "context" | "usage", percent: number | null, width: number): string { const safeWidth = Math.max(0, Math.round(width)); if (percent === null) { return label(theme, "░".repeat(safeWidth)); } const safePercent = Math.max(0, Math.min(100, percent)); const filled = Math.round((safePercent / 100) * safeWidth); const empty = safeWidth - filled; const filledColor = kind === "context" ? resolveContextColor(safePercent) : resolveUsageColor(safePercent); return `${fg(theme, filledColor, "█".repeat(filled))}${label(theme, "░".repeat(empty))}`; } function resolveHudBackgroundColorForTheme(theme: Theme, color: string): string | undefined { return selectHudBackgroundPalette(theme)[color]; } function selectHudBackgroundPalette(theme: Theme): Record { return isLightHudTheme(theme) ? SOLARIZED_LIGHT_HUD_BACKGROUND_COLORS : SOLARIZED_DARK_HUD_BACKGROUND_COLORS; } function isLightHudTheme(theme: Theme): boolean { const themeName = String((theme as { name?: string }).name ?? "").toLowerCase(); if (themeName.includes("dark")) return false; if (themeName.includes("light")) return true; const backgroundRgb = getThemeBackgroundRgb(theme); if (backgroundRgb) return relativeLuminance(backgroundRgb) >= 0.5; const terminalBackground = getColorFgBgBackgroundRgb(process.env.COLORFGBG); if (terminalBackground) return relativeLuminance(terminalBackground) >= 0.5; return false; } function getThemeBackgroundRgb(theme: Theme): { r: number; g: number; b: number } | null { const getBgAnsi = (theme as { getBgAnsi?: (color: string) => string }).getBgAnsi; if (!getBgAnsi) return null; for (const color of ["userMessageBg", "selectedBg", "toolPendingBg"]) { try { const rgb = ansiBackgroundToRgb(getBgAnsi.call(theme, color)); if (rgb) return rgb; } catch { // Ignore themes that do not expose a given background token. } } return null; } function ansiBackgroundToRgb(ansi: string): { r: number; g: number; b: number } | null { const trueColor = ansi.match(/\x1b\[48;2;(\d+);(\d+);(\d+)m/); if (trueColor) { return { r: clampChannel(Number.parseInt(trueColor[1] ?? "0", 10)), g: clampChannel(Number.parseInt(trueColor[2] ?? "0", 10)), b: clampChannel(Number.parseInt(trueColor[3] ?? "0", 10)), }; } const indexed = ansi.match(/\x1b\[48;5;(\d+)m/); if (indexed) return xterm256ToRgb(Number.parseInt(indexed[1] ?? "0", 10)); return null; } function getColorFgBgBackgroundRgb(value: string | undefined): { r: number; g: number; b: number } | null { const rawBackground = value?.split(/[;:]/).filter(Boolean).at(-1); if (!rawBackground) return null; const background = Number.parseInt(rawBackground, 10); if (!Number.isInteger(background)) return null; return xterm256ToRgb(background); } function xterm256ToRgb(color: number): { r: number; g: number; b: number } | null { const index = clampChannel(color); const ansi16: Array<{ r: number; g: number; b: number }> = [ { r: 0, g: 0, b: 0 }, { r: 128, g: 0, b: 0 }, { r: 0, g: 128, b: 0 }, { r: 128, g: 128, b: 0 }, { r: 0, g: 0, b: 128 }, { r: 128, g: 0, b: 128 }, { r: 0, g: 128, b: 128 }, { r: 192, g: 192, b: 192 }, { r: 128, g: 128, b: 128 }, { r: 255, g: 0, b: 0 }, { r: 0, g: 255, b: 0 }, { r: 255, g: 255, b: 0 }, { r: 0, g: 0, b: 255 }, { r: 255, g: 0, b: 255 }, { r: 0, g: 255, b: 255 }, { r: 255, g: 255, b: 255 }, ]; if (index < 16) return ansi16[index] ?? null; if (index >= 16 && index <= 231) { const cubeIndex = index - 16; const r = Math.floor(cubeIndex / 36); const g = Math.floor((cubeIndex % 36) / 6); const b = cubeIndex % 6; return { r: xtermCubeChannel(r), g: xtermCubeChannel(g), b: xtermCubeChannel(b) }; } const gray = 8 + (index - 232) * 10; return { r: gray, g: gray, b: gray }; } function xtermCubeChannel(value: number): number { return value === 0 ? 0 : 55 + value * 40; } function relativeLuminance(rgb: { r: number; g: number; b: number }): number { return (0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b) / 255; } function clampChannel(value: number): number { if (!Number.isFinite(value)) return 0; return Math.max(0, Math.min(255, value)); } function hudColorEnabled(): boolean { return process.env.NO_COLOR !== "1" && process.env.FORCE_COLOR !== "0"; } function ansiFgStyle(name: string, code: string): HudForegroundStyle { return ansiTextStyle(name, code, "39"); } function ansiTextStyle(name: string, code: string, resetCode: string): HudForegroundStyle { return { name, open: `\x1b[${code}m`, close: `\x1b[${resetCode}m`, }; } function ansiBg(hex: string, text: string): string { const rgb = hexToRgb(hex); if (!rgb) return text; return `\x1b[48;2;${rgb.r};${rgb.g};${rgb.b}m${text}\x1b[49m`; } function hexToRgb(hex: string): { r: number; g: number; b: number } | null { const match = hex.match(/^#?([0-9a-f]{6})$/i); if (!match) return null; const value = match[1]; return { r: Number.parseInt(value.slice(0, 2), 16), g: Number.parseInt(value.slice(2, 4), 16), b: Number.parseInt(value.slice(4, 6), 16), }; } function mapColor(color: string): string { if (color === "model") return "accent"; if (color === "project") return "warning"; if (color === "git") return "magenta"; if (color === "gitBranch") return "accent"; if (color === "cyan") return "accent"; if (color === "context") return "success"; if (color === "usage") return "dim"; if (color === "critical") return "error"; if (color === "label") return "dim"; if (color === "usageBar") return "dim"; if (color === "usageWarning") return "warning"; if (color === "contextBarEmpty") return "dim"; if (color === "barEmpty") return "dim"; return color; }