Files
clawdbot/src/tui/theme/theme.ts
2026-01-19 05:32:53 +00:00

133 lines
4.4 KiB
TypeScript

import type {
EditorTheme,
MarkdownTheme,
SelectListTheme,
SettingsListTheme,
} from "@mariozechner/pi-tui";
import chalk from "chalk";
import { highlight, supportsLanguage } from "cli-highlight";
import type { SearchableSelectListTheme } from "../components/searchable-select-list.js";
import { createSyntaxTheme } from "./syntax-theme.js";
const palette = {
text: "#E8E3D5",
dim: "#7B7F87",
accent: "#F6C453",
accentSoft: "#F2A65A",
border: "#3C414B",
userBg: "#2B2F36",
userText: "#F3EEE0",
systemText: "#9BA3B2",
toolPendingBg: "#1F2A2F",
toolSuccessBg: "#1E2D23",
toolErrorBg: "#2F1F1F",
toolTitle: "#F6C453",
toolOutput: "#E1DACB",
quote: "#8CC8FF",
quoteBorder: "#3B4D6B",
code: "#F0C987",
codeBlock: "#1E232A",
codeBorder: "#343A45",
link: "#7DD3A5",
error: "#F97066",
success: "#7DD3A5",
};
const fg = (hex: string) => (text: string) => chalk.hex(hex)(text);
const bg = (hex: string) => (text: string) => chalk.bgHex(hex)(text);
const syntaxTheme = createSyntaxTheme(fg(palette.code));
/**
* Highlight code with syntax coloring.
* Returns an array of lines with ANSI escape codes.
*/
function highlightCode(code: string, lang?: string): string[] {
try {
// Auto-detect can be slow for very large blocks; prefer explicit language when available.
// Check if language is supported, fall back to auto-detect
const language = lang && supportsLanguage(lang) ? lang : undefined;
const highlighted = highlight(code, {
language,
theme: syntaxTheme,
ignoreIllegals: true,
});
return highlighted.split("\n");
} catch {
// If highlighting fails, return plain code
return code.split("\n").map((line) => fg(palette.code)(line));
}
}
export const theme = {
fg: fg(palette.text),
dim: fg(palette.dim),
accent: fg(palette.accent),
accentSoft: fg(palette.accentSoft),
success: fg(palette.success),
error: fg(palette.error),
header: (text: string) => chalk.bold(fg(palette.accent)(text)),
system: fg(palette.systemText),
userBg: bg(palette.userBg),
userText: fg(palette.userText),
toolTitle: fg(palette.toolTitle),
toolOutput: fg(palette.toolOutput),
toolPendingBg: bg(palette.toolPendingBg),
toolSuccessBg: bg(palette.toolSuccessBg),
toolErrorBg: bg(palette.toolErrorBg),
border: fg(palette.border),
bold: (text: string) => chalk.bold(text),
italic: (text: string) => chalk.italic(text),
};
export const markdownTheme: MarkdownTheme = {
heading: (text) => chalk.bold(fg(palette.accent)(text)),
link: (text) => fg(palette.link)(text),
linkUrl: (text) => chalk.dim(text),
code: (text) => fg(palette.code)(text),
codeBlock: (text) => fg(palette.code)(text),
codeBlockBorder: (text) => fg(palette.codeBorder)(text),
quote: (text) => fg(palette.quote)(text),
quoteBorder: (text) => fg(palette.quoteBorder)(text),
hr: (text) => fg(palette.border)(text),
listBullet: (text) => fg(palette.accentSoft)(text),
bold: (text) => chalk.bold(text),
italic: (text) => chalk.italic(text),
strikethrough: (text) => chalk.strikethrough(text),
underline: (text) => chalk.underline(text),
highlightCode,
};
export const selectListTheme: SelectListTheme = {
selectedPrefix: (text) => fg(palette.accent)(text),
selectedText: (text) => chalk.bold(fg(palette.accent)(text)),
description: (text) => fg(palette.dim)(text),
scrollInfo: (text) => fg(palette.dim)(text),
noMatch: (text) => fg(palette.dim)(text),
};
export const settingsListTheme: SettingsListTheme = {
label: (text, selected) =>
selected ? chalk.bold(fg(palette.accent)(text)) : fg(palette.text)(text),
value: (text, selected) => (selected ? fg(palette.accentSoft)(text) : fg(palette.dim)(text)),
description: (text) => fg(palette.systemText)(text),
cursor: fg(palette.accent)("→ "),
hint: (text) => fg(palette.dim)(text),
};
export const editorTheme: EditorTheme = {
borderColor: (text) => fg(palette.border)(text),
selectList: selectListTheme,
};
export const searchableSelectListTheme: SearchableSelectListTheme = {
selectedPrefix: (text) => fg(palette.accent)(text),
selectedText: (text) => chalk.bold(fg(palette.accent)(text)),
description: (text) => fg(palette.dim)(text),
scrollInfo: (text) => fg(palette.dim)(text),
noMatch: (text) => fg(palette.dim)(text),
searchPrompt: (text) => fg(palette.accentSoft)(text),
searchInput: (text) => fg(palette.text)(text),
matchHighlight: (text) => chalk.bold(fg(palette.accent)(text)),
};