mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 04:01:56 +00:00
Reorganize file structure: core/, utils/, modes/interactive/components/, modes/interactive/theme/
This commit is contained in:
parent
00982705f2
commit
83a6c26969
56 changed files with 133 additions and 128 deletions
597
packages/coding-agent/src/modes/interactive/theme/theme.ts
Normal file
597
packages/coding-agent/src/modes/interactive/theme/theme.ts
Normal file
|
|
@ -0,0 +1,597 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@mariozechner/pi-tui";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
||||
import chalk from "chalk";
|
||||
import { getCustomThemesDir, getThemesDir } from "../../../utils/config.js";
|
||||
|
||||
// ============================================================================
|
||||
// Types & Schema
|
||||
// ============================================================================
|
||||
|
||||
const ColorValueSchema = Type.Union([
|
||||
Type.String(), // hex "#ff0000", var ref "primary", or empty ""
|
||||
Type.Integer({ minimum: 0, maximum: 255 }), // 256-color index
|
||||
]);
|
||||
|
||||
type ColorValue = Static<typeof ColorValueSchema>;
|
||||
|
||||
const ThemeJsonSchema = Type.Object({
|
||||
$schema: Type.Optional(Type.String()),
|
||||
name: Type.String(),
|
||||
vars: Type.Optional(Type.Record(Type.String(), ColorValueSchema)),
|
||||
colors: Type.Object({
|
||||
// Core UI (10 colors)
|
||||
accent: ColorValueSchema,
|
||||
border: ColorValueSchema,
|
||||
borderAccent: ColorValueSchema,
|
||||
borderMuted: ColorValueSchema,
|
||||
success: ColorValueSchema,
|
||||
error: ColorValueSchema,
|
||||
warning: ColorValueSchema,
|
||||
muted: ColorValueSchema,
|
||||
dim: ColorValueSchema,
|
||||
text: ColorValueSchema,
|
||||
// Backgrounds & Content Text (7 colors)
|
||||
userMessageBg: ColorValueSchema,
|
||||
userMessageText: ColorValueSchema,
|
||||
toolPendingBg: ColorValueSchema,
|
||||
toolSuccessBg: ColorValueSchema,
|
||||
toolErrorBg: ColorValueSchema,
|
||||
toolTitle: ColorValueSchema,
|
||||
toolOutput: ColorValueSchema,
|
||||
// Markdown (10 colors)
|
||||
mdHeading: ColorValueSchema,
|
||||
mdLink: ColorValueSchema,
|
||||
mdLinkUrl: ColorValueSchema,
|
||||
mdCode: ColorValueSchema,
|
||||
mdCodeBlock: ColorValueSchema,
|
||||
mdCodeBlockBorder: ColorValueSchema,
|
||||
mdQuote: ColorValueSchema,
|
||||
mdQuoteBorder: ColorValueSchema,
|
||||
mdHr: ColorValueSchema,
|
||||
mdListBullet: ColorValueSchema,
|
||||
// Tool Diffs (3 colors)
|
||||
toolDiffAdded: ColorValueSchema,
|
||||
toolDiffRemoved: ColorValueSchema,
|
||||
toolDiffContext: ColorValueSchema,
|
||||
// Syntax Highlighting (9 colors)
|
||||
syntaxComment: ColorValueSchema,
|
||||
syntaxKeyword: ColorValueSchema,
|
||||
syntaxFunction: ColorValueSchema,
|
||||
syntaxVariable: ColorValueSchema,
|
||||
syntaxString: ColorValueSchema,
|
||||
syntaxNumber: ColorValueSchema,
|
||||
syntaxType: ColorValueSchema,
|
||||
syntaxOperator: ColorValueSchema,
|
||||
syntaxPunctuation: ColorValueSchema,
|
||||
// Thinking Level Borders (6 colors)
|
||||
thinkingOff: ColorValueSchema,
|
||||
thinkingMinimal: ColorValueSchema,
|
||||
thinkingLow: ColorValueSchema,
|
||||
thinkingMedium: ColorValueSchema,
|
||||
thinkingHigh: ColorValueSchema,
|
||||
thinkingXhigh: ColorValueSchema,
|
||||
// Bash Mode (1 color)
|
||||
bashMode: ColorValueSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
type ThemeJson = Static<typeof ThemeJsonSchema>;
|
||||
|
||||
const validateThemeJson = TypeCompiler.Compile(ThemeJsonSchema);
|
||||
|
||||
export type ThemeColor =
|
||||
| "accent"
|
||||
| "border"
|
||||
| "borderAccent"
|
||||
| "borderMuted"
|
||||
| "success"
|
||||
| "error"
|
||||
| "warning"
|
||||
| "muted"
|
||||
| "dim"
|
||||
| "text"
|
||||
| "userMessageText"
|
||||
| "toolTitle"
|
||||
| "toolOutput"
|
||||
| "mdHeading"
|
||||
| "mdLink"
|
||||
| "mdLinkUrl"
|
||||
| "mdCode"
|
||||
| "mdCodeBlock"
|
||||
| "mdCodeBlockBorder"
|
||||
| "mdQuote"
|
||||
| "mdQuoteBorder"
|
||||
| "mdHr"
|
||||
| "mdListBullet"
|
||||
| "toolDiffAdded"
|
||||
| "toolDiffRemoved"
|
||||
| "toolDiffContext"
|
||||
| "syntaxComment"
|
||||
| "syntaxKeyword"
|
||||
| "syntaxFunction"
|
||||
| "syntaxVariable"
|
||||
| "syntaxString"
|
||||
| "syntaxNumber"
|
||||
| "syntaxType"
|
||||
| "syntaxOperator"
|
||||
| "syntaxPunctuation"
|
||||
| "thinkingOff"
|
||||
| "thinkingMinimal"
|
||||
| "thinkingLow"
|
||||
| "thinkingMedium"
|
||||
| "thinkingHigh"
|
||||
| "thinkingXhigh"
|
||||
| "bashMode";
|
||||
|
||||
export type ThemeBg = "userMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
||||
|
||||
type ColorMode = "truecolor" | "256color";
|
||||
|
||||
// ============================================================================
|
||||
// Color Utilities
|
||||
// ============================================================================
|
||||
|
||||
function detectColorMode(): ColorMode {
|
||||
const colorterm = process.env.COLORTERM;
|
||||
if (colorterm === "truecolor" || colorterm === "24bit") {
|
||||
return "truecolor";
|
||||
}
|
||||
// Windows Terminal supports truecolor
|
||||
if (process.env.WT_SESSION) {
|
||||
return "truecolor";
|
||||
}
|
||||
const term = process.env.TERM || "";
|
||||
if (term.includes("256color")) {
|
||||
return "256color";
|
||||
}
|
||||
return "256color";
|
||||
}
|
||||
|
||||
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
||||
const cleaned = hex.replace("#", "");
|
||||
if (cleaned.length !== 6) {
|
||||
throw new Error(`Invalid hex color: ${hex}`);
|
||||
}
|
||||
const r = parseInt(cleaned.substring(0, 2), 16);
|
||||
const g = parseInt(cleaned.substring(2, 4), 16);
|
||||
const b = parseInt(cleaned.substring(4, 6), 16);
|
||||
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
|
||||
throw new Error(`Invalid hex color: ${hex}`);
|
||||
}
|
||||
return { r, g, b };
|
||||
}
|
||||
|
||||
function rgbTo256(r: number, g: number, b: number): number {
|
||||
const rIndex = Math.round((r / 255) * 5);
|
||||
const gIndex = Math.round((g / 255) * 5);
|
||||
const bIndex = Math.round((b / 255) * 5);
|
||||
return 16 + 36 * rIndex + 6 * gIndex + bIndex;
|
||||
}
|
||||
|
||||
function hexTo256(hex: string): number {
|
||||
const { r, g, b } = hexToRgb(hex);
|
||||
return rgbTo256(r, g, b);
|
||||
}
|
||||
|
||||
function fgAnsi(color: string | number, mode: ColorMode): string {
|
||||
if (color === "") return "\x1b[39m";
|
||||
if (typeof color === "number") return `\x1b[38;5;${color}m`;
|
||||
if (color.startsWith("#")) {
|
||||
if (mode === "truecolor") {
|
||||
const { r, g, b } = hexToRgb(color);
|
||||
return `\x1b[38;2;${r};${g};${b}m`;
|
||||
} else {
|
||||
const index = hexTo256(color);
|
||||
return `\x1b[38;5;${index}m`;
|
||||
}
|
||||
}
|
||||
throw new Error(`Invalid color value: ${color}`);
|
||||
}
|
||||
|
||||
function bgAnsi(color: string | number, mode: ColorMode): string {
|
||||
if (color === "") return "\x1b[49m";
|
||||
if (typeof color === "number") return `\x1b[48;5;${color}m`;
|
||||
if (color.startsWith("#")) {
|
||||
if (mode === "truecolor") {
|
||||
const { r, g, b } = hexToRgb(color);
|
||||
return `\x1b[48;2;${r};${g};${b}m`;
|
||||
} else {
|
||||
const index = hexTo256(color);
|
||||
return `\x1b[48;5;${index}m`;
|
||||
}
|
||||
}
|
||||
throw new Error(`Invalid color value: ${color}`);
|
||||
}
|
||||
|
||||
function resolveVarRefs(
|
||||
value: ColorValue,
|
||||
vars: Record<string, ColorValue>,
|
||||
visited = new Set<string>(),
|
||||
): string | number {
|
||||
if (typeof value === "number" || value === "" || value.startsWith("#")) {
|
||||
return value;
|
||||
}
|
||||
if (visited.has(value)) {
|
||||
throw new Error(`Circular variable reference detected: ${value}`);
|
||||
}
|
||||
if (!(value in vars)) {
|
||||
throw new Error(`Variable reference not found: ${value}`);
|
||||
}
|
||||
visited.add(value);
|
||||
return resolveVarRefs(vars[value], vars, visited);
|
||||
}
|
||||
|
||||
function resolveThemeColors<T extends Record<string, ColorValue>>(
|
||||
colors: T,
|
||||
vars: Record<string, ColorValue> = {},
|
||||
): Record<keyof T, string | number> {
|
||||
const resolved: Record<string, string | number> = {};
|
||||
for (const [key, value] of Object.entries(colors)) {
|
||||
resolved[key] = resolveVarRefs(value, vars);
|
||||
}
|
||||
return resolved as Record<keyof T, string | number>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Theme Class
|
||||
// ============================================================================
|
||||
|
||||
export class Theme {
|
||||
private fgColors: Map<ThemeColor, string>;
|
||||
private bgColors: Map<ThemeBg, string>;
|
||||
private mode: ColorMode;
|
||||
|
||||
constructor(
|
||||
fgColors: Record<ThemeColor, string | number>,
|
||||
bgColors: Record<ThemeBg, string | number>,
|
||||
mode: ColorMode,
|
||||
) {
|
||||
this.mode = mode;
|
||||
this.fgColors = new Map();
|
||||
for (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {
|
||||
this.fgColors.set(key, fgAnsi(value, mode));
|
||||
}
|
||||
this.bgColors = new Map();
|
||||
for (const [key, value] of Object.entries(bgColors) as [ThemeBg, string | number][]) {
|
||||
this.bgColors.set(key, bgAnsi(value, mode));
|
||||
}
|
||||
}
|
||||
|
||||
fg(color: ThemeColor, text: string): string {
|
||||
const ansi = this.fgColors.get(color);
|
||||
if (!ansi) throw new Error(`Unknown theme color: ${color}`);
|
||||
return `${ansi}${text}\x1b[39m`; // Reset only foreground color
|
||||
}
|
||||
|
||||
bg(color: ThemeBg, text: string): string {
|
||||
const ansi = this.bgColors.get(color);
|
||||
if (!ansi) throw new Error(`Unknown theme background color: ${color}`);
|
||||
return `${ansi}${text}\x1b[49m`; // Reset only background color
|
||||
}
|
||||
|
||||
bold(text: string): string {
|
||||
return chalk.bold(text);
|
||||
}
|
||||
|
||||
italic(text: string): string {
|
||||
return chalk.italic(text);
|
||||
}
|
||||
|
||||
underline(text: string): string {
|
||||
return chalk.underline(text);
|
||||
}
|
||||
|
||||
getFgAnsi(color: ThemeColor): string {
|
||||
const ansi = this.fgColors.get(color);
|
||||
if (!ansi) throw new Error(`Unknown theme color: ${color}`);
|
||||
return ansi;
|
||||
}
|
||||
|
||||
getBgAnsi(color: ThemeBg): string {
|
||||
const ansi = this.bgColors.get(color);
|
||||
if (!ansi) throw new Error(`Unknown theme background color: ${color}`);
|
||||
return ansi;
|
||||
}
|
||||
|
||||
getColorMode(): ColorMode {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
getThinkingBorderColor(level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"): (str: string) => string {
|
||||
// Map thinking levels to dedicated theme colors
|
||||
switch (level) {
|
||||
case "off":
|
||||
return (str: string) => this.fg("thinkingOff", str);
|
||||
case "minimal":
|
||||
return (str: string) => this.fg("thinkingMinimal", str);
|
||||
case "low":
|
||||
return (str: string) => this.fg("thinkingLow", str);
|
||||
case "medium":
|
||||
return (str: string) => this.fg("thinkingMedium", str);
|
||||
case "high":
|
||||
return (str: string) => this.fg("thinkingHigh", str);
|
||||
case "xhigh":
|
||||
return (str: string) => this.fg("thinkingXhigh", str);
|
||||
default:
|
||||
return (str: string) => this.fg("thinkingOff", str);
|
||||
}
|
||||
}
|
||||
|
||||
getBashModeBorderColor(): (str: string) => string {
|
||||
return (str: string) => this.fg("bashMode", str);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Theme Loading
|
||||
// ============================================================================
|
||||
|
||||
let BUILTIN_THEMES: Record<string, ThemeJson> | undefined;
|
||||
|
||||
function getBuiltinThemes(): Record<string, ThemeJson> {
|
||||
if (!BUILTIN_THEMES) {
|
||||
const themesDir = getThemesDir();
|
||||
const darkPath = path.join(themesDir, "dark.json");
|
||||
const lightPath = path.join(themesDir, "light.json");
|
||||
BUILTIN_THEMES = {
|
||||
dark: JSON.parse(fs.readFileSync(darkPath, "utf-8")) as ThemeJson,
|
||||
light: JSON.parse(fs.readFileSync(lightPath, "utf-8")) as ThemeJson,
|
||||
};
|
||||
}
|
||||
return BUILTIN_THEMES;
|
||||
}
|
||||
|
||||
export function getAvailableThemes(): string[] {
|
||||
const themes = new Set<string>(Object.keys(getBuiltinThemes()));
|
||||
const customThemesDir = getCustomThemesDir();
|
||||
if (fs.existsSync(customThemesDir)) {
|
||||
const files = fs.readdirSync(customThemesDir);
|
||||
for (const file of files) {
|
||||
if (file.endsWith(".json")) {
|
||||
themes.add(file.slice(0, -5));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(themes).sort();
|
||||
}
|
||||
|
||||
function loadThemeJson(name: string): ThemeJson {
|
||||
const builtinThemes = getBuiltinThemes();
|
||||
if (name in builtinThemes) {
|
||||
return builtinThemes[name];
|
||||
}
|
||||
const customThemesDir = getCustomThemesDir();
|
||||
const themePath = path.join(customThemesDir, `${name}.json`);
|
||||
if (!fs.existsSync(themePath)) {
|
||||
throw new Error(`Theme not found: ${name}`);
|
||||
}
|
||||
const content = fs.readFileSync(themePath, "utf-8");
|
||||
let json: unknown;
|
||||
try {
|
||||
json = JSON.parse(content);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse theme ${name}: ${error}`);
|
||||
}
|
||||
if (!validateThemeJson.Check(json)) {
|
||||
const errors = Array.from(validateThemeJson.Errors(json));
|
||||
const missingColors: string[] = [];
|
||||
const otherErrors: string[] = [];
|
||||
|
||||
for (const e of errors) {
|
||||
// Check for missing required color properties
|
||||
const match = e.path.match(/^\/colors\/(\w+)$/);
|
||||
if (match && e.message.includes("Required")) {
|
||||
missingColors.push(match[1]);
|
||||
} else {
|
||||
otherErrors.push(` - ${e.path}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
let errorMessage = `Invalid theme "${name}":\n`;
|
||||
if (missingColors.length > 0) {
|
||||
errorMessage += `\nMissing required color tokens:\n`;
|
||||
errorMessage += missingColors.map((c) => ` - ${c}`).join("\n");
|
||||
errorMessage += `\n\nPlease add these colors to your theme's "colors" object.`;
|
||||
errorMessage += `\nSee the built-in themes (dark.json, light.json) for reference values.`;
|
||||
}
|
||||
if (otherErrors.length > 0) {
|
||||
errorMessage += `\n\nOther errors:\n${otherErrors.join("\n")}`;
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
return json as ThemeJson;
|
||||
}
|
||||
|
||||
function createTheme(themeJson: ThemeJson, mode?: ColorMode): Theme {
|
||||
const colorMode = mode ?? detectColorMode();
|
||||
const resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars);
|
||||
const fgColors: Record<ThemeColor, string | number> = {} as Record<ThemeColor, string | number>;
|
||||
const bgColors: Record<ThemeBg, string | number> = {} as Record<ThemeBg, string | number>;
|
||||
const bgColorKeys: Set<string> = new Set(["userMessageBg", "toolPendingBg", "toolSuccessBg", "toolErrorBg"]);
|
||||
for (const [key, value] of Object.entries(resolvedColors)) {
|
||||
if (bgColorKeys.has(key)) {
|
||||
bgColors[key as ThemeBg] = value;
|
||||
} else {
|
||||
fgColors[key as ThemeColor] = value;
|
||||
}
|
||||
}
|
||||
return new Theme(fgColors, bgColors, colorMode);
|
||||
}
|
||||
|
||||
function loadTheme(name: string, mode?: ColorMode): Theme {
|
||||
const themeJson = loadThemeJson(name);
|
||||
return createTheme(themeJson, mode);
|
||||
}
|
||||
|
||||
function detectTerminalBackground(): "dark" | "light" {
|
||||
const colorfgbg = process.env.COLORFGBG || "";
|
||||
if (colorfgbg) {
|
||||
const parts = colorfgbg.split(";");
|
||||
if (parts.length >= 2) {
|
||||
const bg = parseInt(parts[1], 10);
|
||||
if (!Number.isNaN(bg)) {
|
||||
const result = bg < 8 ? "dark" : "light";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "dark";
|
||||
}
|
||||
|
||||
function getDefaultTheme(): string {
|
||||
return detectTerminalBackground();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Global Theme Instance
|
||||
// ============================================================================
|
||||
|
||||
export let theme: Theme;
|
||||
let currentThemeName: string | undefined;
|
||||
let themeWatcher: fs.FSWatcher | undefined;
|
||||
let onThemeChangeCallback: (() => void) | undefined;
|
||||
|
||||
export function initTheme(themeName?: string): void {
|
||||
const name = themeName ?? getDefaultTheme();
|
||||
currentThemeName = name;
|
||||
try {
|
||||
theme = loadTheme(name);
|
||||
startThemeWatcher();
|
||||
} catch (error) {
|
||||
// Theme is invalid - fall back to dark theme silently
|
||||
currentThemeName = "dark";
|
||||
theme = loadTheme("dark");
|
||||
// Don't start watcher for fallback theme
|
||||
}
|
||||
}
|
||||
|
||||
export function setTheme(name: string): { success: boolean; error?: string } {
|
||||
currentThemeName = name;
|
||||
try {
|
||||
theme = loadTheme(name);
|
||||
startThemeWatcher();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
// Theme is invalid - fall back to dark theme
|
||||
currentThemeName = "dark";
|
||||
theme = loadTheme("dark");
|
||||
// Don't start watcher for fallback theme
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function onThemeChange(callback: () => void): void {
|
||||
onThemeChangeCallback = callback;
|
||||
}
|
||||
|
||||
function startThemeWatcher(): void {
|
||||
// Stop existing watcher if any
|
||||
if (themeWatcher) {
|
||||
themeWatcher.close();
|
||||
themeWatcher = undefined;
|
||||
}
|
||||
|
||||
// Only watch if it's a custom theme (not built-in)
|
||||
if (!currentThemeName || currentThemeName === "dark" || currentThemeName === "light") {
|
||||
return;
|
||||
}
|
||||
|
||||
const customThemesDir = getCustomThemesDir();
|
||||
const themeFile = path.join(customThemesDir, `${currentThemeName}.json`);
|
||||
|
||||
// Only watch if the file exists
|
||||
if (!fs.existsSync(themeFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
themeWatcher = fs.watch(themeFile, (eventType) => {
|
||||
if (eventType === "change") {
|
||||
// Debounce rapid changes
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// Reload the theme
|
||||
theme = loadTheme(currentThemeName!);
|
||||
// Notify callback (to invalidate UI)
|
||||
if (onThemeChangeCallback) {
|
||||
onThemeChangeCallback();
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors (file might be in invalid state while being edited)
|
||||
}
|
||||
}, 100);
|
||||
} else if (eventType === "rename") {
|
||||
// File was deleted or renamed - fall back to default theme
|
||||
setTimeout(() => {
|
||||
if (!fs.existsSync(themeFile)) {
|
||||
currentThemeName = "dark";
|
||||
theme = loadTheme("dark");
|
||||
if (themeWatcher) {
|
||||
themeWatcher.close();
|
||||
themeWatcher = undefined;
|
||||
}
|
||||
if (onThemeChangeCallback) {
|
||||
onThemeChangeCallback();
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore errors starting watcher
|
||||
}
|
||||
}
|
||||
|
||||
export function stopThemeWatcher(): void {
|
||||
if (themeWatcher) {
|
||||
themeWatcher.close();
|
||||
themeWatcher = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TUI Helpers
|
||||
// ============================================================================
|
||||
|
||||
export function getMarkdownTheme(): MarkdownTheme {
|
||||
return {
|
||||
heading: (text: string) => theme.fg("mdHeading", text),
|
||||
link: (text: string) => theme.fg("mdLink", text),
|
||||
linkUrl: (text: string) => theme.fg("mdLinkUrl", text),
|
||||
code: (text: string) => theme.fg("mdCode", text),
|
||||
codeBlock: (text: string) => theme.fg("mdCodeBlock", text),
|
||||
codeBlockBorder: (text: string) => theme.fg("mdCodeBlockBorder", text),
|
||||
quote: (text: string) => theme.fg("mdQuote", text),
|
||||
quoteBorder: (text: string) => theme.fg("mdQuoteBorder", text),
|
||||
hr: (text: string) => theme.fg("mdHr", text),
|
||||
listBullet: (text: string) => theme.fg("mdListBullet", text),
|
||||
bold: (text: string) => theme.bold(text),
|
||||
italic: (text: string) => theme.italic(text),
|
||||
underline: (text: string) => theme.underline(text),
|
||||
strikethrough: (text: string) => chalk.strikethrough(text),
|
||||
};
|
||||
}
|
||||
|
||||
export function getSelectListTheme(): SelectListTheme {
|
||||
return {
|
||||
selectedPrefix: (text: string) => theme.fg("accent", text),
|
||||
selectedText: (text: string) => theme.fg("accent", text),
|
||||
description: (text: string) => theme.fg("muted", text),
|
||||
scrollInfo: (text: string) => theme.fg("muted", text),
|
||||
noMatch: (text: string) => theme.fg("muted", text),
|
||||
};
|
||||
}
|
||||
|
||||
export function getEditorTheme(): EditorTheme {
|
||||
return {
|
||||
borderColor: (text: string) => theme.fg("borderMuted", text),
|
||||
selectList: getSelectListTheme(),
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue