Agent-JAE/default-extensions/theme-system.ts

299 lines
9.2 KiB
TypeScript

import type { ExtensionAPI, ExtensionContext } from "@jaeswift/jae-coding-agent";
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
interface ThemeColors {
primary: string;
secondary: string;
accent: string;
bg: string;
surface: string;
border: string;
text: string;
muted: string;
}
interface Theme {
name: string;
description: string;
colors: ThemeColors;
}
interface ThemeConfig {
active: string;
custom: Theme[];
}
export default function (pi: ExtensionAPI) {
const THEMES: Record<string, Theme> = {
dragon: {
name: "dragon",
description: "Default JAE theme — black with orange accents",
colors: {
primary: "#FF6B00",
secondary: "#FF8C38",
accent: "#FFA500",
bg: "#0a0a0a",
surface: "#111111",
border: "#1e1e1e",
text: "#f8f8f8",
muted: "#888888",
},
},
midnight: {
name: "midnight",
description: "Deep dark blue — calm and focused",
colors: {
primary: "#4A9EFF",
secondary: "#6BB5FF",
accent: "#80CAFF",
bg: "#0a0e1a",
surface: "#111827",
border: "#1e293b",
text: "#e2e8f0",
muted: "#64748b",
},
},
matrix: {
name: "matrix",
description: "Green on black — classic hacker aesthetic",
colors: {
primary: "#00FF41",
secondary: "#00CC33",
accent: "#33FF77",
bg: "#000000",
surface: "#0a0a0a",
border: "#0f3d0f",
text: "#00FF41",
muted: "#00802a",
},
},
frost: {
name: "frost",
description: "Light cool tones — easy on the eyes",
colors: {
primary: "#5B8DEF",
secondary: "#7AA8F2",
accent: "#98C1FF",
bg: "#0c1220",
surface: "#141e30",
border: "#1f2d42",
text: "#dce6f5",
muted: "#7b8da6",
},
},
sunset: {
name: "sunset",
description: "Warm oranges and reds — vibrant energy",
colors: {
primary: "#FF4500",
secondary: "#FF6347",
accent: "#FF8C00",
bg: "#0d0806",
surface: "#1a0f0a",
border: "#2d1810",
text: "#fce4d6",
muted: "#a0735c",
},
},
};
const configDir = join(process.env.HOME || "/root", ".jae");
const configPath = join(configDir, "theme.json");
function loadConfig(): ThemeConfig {
try {
if (existsSync(configPath)) {
return JSON.parse(readFileSync(configPath, "utf-8"));
}
} catch { /* ignore */ }
return { active: "dragon", custom: [] };
}
function saveConfig(config: ThemeConfig): void {
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
}
function getTheme(name: string, config: ThemeConfig): Theme | undefined {
if (THEMES[name]) return THEMES[name];
return config.custom.find((t) => t.name === name);
}
function getAllThemes(config: ThemeConfig): Theme[] {
return [...Object.values(THEMES), ...config.custom];
}
function applyTheme(theme: Theme, ctx: ExtensionContext): void {
const c = theme.colors;
ctx.ui.setStatus(
"theme",
`\u{1F3A8} ${theme.name}`,
);
// Apply theme colors via footer display
ctx.ui.setFooter(
"theme",
`\u{1F3A8} Theme: ${theme.name} | ${c.primary} ${c.secondary}`,
);
}
function formatThemePreview(theme: Theme): string {
const c = theme.colors;
return (
` \u{1F3A8} ${theme.name}${theme.description}\n` +
` Primary: ${c.primary} Secondary: ${c.secondary}\n` +
` Accent: ${c.accent} Background: ${c.bg}\n` +
` Surface: ${c.surface} Border: ${c.border}\n` +
` Text: ${c.text} Muted: ${c.muted}`
);
}
// Apply theme on session start
pi.on("session_start", async (_event, ctx) => {
const config = loadConfig();
const theme = getTheme(config.active, config) || THEMES.dragon;
applyTheme(theme, ctx);
});
pi.registerCommand("theme", {
description: "Manage themes: /theme [list|set <name>|create <name>|show]",
handler: async (args, ctx) => {
const parts = args.trim().split(/\s+/);
const subcommand = (parts[0] || "show").toLowerCase();
const config = loadConfig();
switch (subcommand) {
case "show":
case "": {
const theme = getTheme(config.active, config) || THEMES.dragon;
ctx.ui.notify(
`\u{1F3A8} Current Theme\n${'\u2500'.repeat(40)}\n\n${formatThemePreview(theme)}`,
"info",
);
break;
}
case "list":
case "ls": {
const all = getAllThemes(config);
const list = all
.map((t) => {
const active = t.name === config.active ? " \u2705 (active)" : "";
const isCustom = !THEMES[t.name] ? " [custom]" : "";
return ` \u{25CF} ${t.name}${active}${isCustom}${t.description}`;
})
.join("\n");
ctx.ui.notify(
`\u{1F3A8} Available Themes\n${'\u2500'.repeat(40)}\n\n${list}\n\nUse /theme set <name> to apply a theme.`,
"info",
);
break;
}
case "set":
case "apply": {
const name = parts[1];
if (!name) {
ctx.ui.notify("Usage: /theme set <name>\n\nUse /theme list to see available themes.", "warning");
return;
}
const theme = getTheme(name.toLowerCase(), config);
if (!theme) {
ctx.ui.notify(
`\u274C Theme '${name}' not found.\n\nAvailable: ${getAllThemes(config).map((t) => t.name).join(", ")}`,
"warning",
);
return;
}
config.active = theme.name;
saveConfig(config);
applyTheme(theme, ctx);
ctx.ui.notify(
`\u2705 Theme set to '${theme.name}'\n\n${formatThemePreview(theme)}`,
"info",
);
break;
}
case "create": {
const name = parts[1];
if (!name) {
ctx.ui.notify(
"Usage: /theme create <name>\n\n" +
"Creates a custom theme based on the current theme.\n" +
"Edit ~/.jae/theme.json to customize colors.",
"warning",
);
return;
}
const themeName = name.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
if (THEMES[themeName]) {
ctx.ui.notify(`\u274C Cannot override built-in theme '${themeName}'.`, "warning");
return;
}
const existing = config.custom.find((t) => t.name === themeName);
if (existing) {
ctx.ui.notify(`\u274C Custom theme '${themeName}' already exists. Edit ~/.jae/theme.json to modify.`, "warning");
return;
}
// Clone current theme as base
const base = getTheme(config.active, config) || THEMES.dragon;
const newTheme: Theme = {
name: themeName,
description: `Custom theme based on ${base.name}`,
colors: { ...base.colors },
};
config.custom.push(newTheme);
config.active = themeName;
saveConfig(config);
applyTheme(newTheme, ctx);
ctx.ui.notify(
`\u2705 Custom theme '${themeName}' created and activated!\n\n${formatThemePreview(newTheme)}\n\n\u{1F4DD} Edit colors in ~/.jae/theme.json`,
"info",
);
break;
}
case "reset": {
config.active = "dragon";
saveConfig(config);
applyTheme(THEMES.dragon, ctx);
ctx.ui.notify("\u2705 Theme reset to dragon (default).", "info");
break;
}
case "delete":
case "remove": {
const name = parts[1];
if (!name) {
ctx.ui.notify("Usage: /theme delete <name>", "warning");
return;
}
const themeName = name.toLowerCase();
if (THEMES[themeName]) {
ctx.ui.notify(`\u274C Cannot delete built-in theme '${themeName}'.`, "warning");
return;
}
const idx = config.custom.findIndex((t) => t.name === themeName);
if (idx === -1) {
ctx.ui.notify(`\u274C Custom theme '${themeName}' not found.`, "warning");
return;
}
config.custom.splice(idx, 1);
if (config.active === themeName) config.active = "dragon";
saveConfig(config);
ctx.ui.notify(`\u2705 Custom theme '${themeName}' deleted.`, "info");
break;
}
default:
ctx.ui.notify(
`\u{1F3A8} Theme System\n${'\u2500'.repeat(30)}\n\n` +
"Commands:\n" +
" /theme - Show current theme\n" +
" /theme list - List all themes\n" +
" /theme set <name> - Apply a theme\n" +
" /theme create <name>- Create custom theme\n" +
" /theme delete <name>- Delete custom theme\n" +
" /theme reset - Reset to dragon (default)",
"info",
);
}
},
});
}