

























11/**
22 * Interactive terminal theme loader.
33 *
4- * Validates theme JSON, resolves color variables, watches custom theme files, and exposes Pi TUI theme adapters.
4+ * Validates theme JSON, resolves color variables, watches custom theme files, and exposes terminal styling helpers.
55 */
66import * as fs from "node:fs";
77import * as path from "node:path";
8-import {
9-type EditorTheme,
10-getCapabilities,
11-type MarkdownTheme,
12-type SelectListTheme,
13-type SettingsListTheme,
14-} from "@earendil-works/pi-tui";
8+import { getCapabilities } from "@earendil-works/pi-tui";
159import chalk from "chalk";
1610import { type Static, Type } from "typebox";
1711import { Compile } from "typebox/compile";
@@ -468,59 +462,6 @@ function getBuiltinThemes(): Record<string, ThemeJson> {
468462return BUILTIN_THEMES;
469463}
470464471-export function getAvailableThemes(): string[] {
472-const themes = new Set<string>(Object.keys(getBuiltinThemes()));
473-const customThemesDir = getCustomThemesDir();
474-if (fs.existsSync(customThemesDir)) {
475-const files = fs.readdirSync(customThemesDir);
476-for (const file of files) {
477-if (file.endsWith(".json")) {
478-themes.add(file.slice(0, -5));
479-}
480-}
481-}
482-for (const name of registeredThemes.keys()) {
483-themes.add(name);
484-}
485-return Array.from(themes).toSorted();
486-}
487-488-export interface ThemeInfo {
489-name: string;
490-path: string | undefined;
491-}
492-493-export function getAvailableThemesWithPaths(): ThemeInfo[] {
494-const themesDir = getThemesDir();
495-const customThemesDir = getCustomThemesDir();
496-const result: ThemeInfo[] = [];
497-498-// Built-in themes
499-for (const name of Object.keys(getBuiltinThemes())) {
500-result.push({ name, path: path.join(themesDir, `${name}.json`) });
501-}
502-503-// Custom themes
504-if (fs.existsSync(customThemesDir)) {
505-for (const file of fs.readdirSync(customThemesDir)) {
506-if (file.endsWith(".json")) {
507-const name = file.slice(0, -5);
508-if (!result.some((t) => t.name === name)) {
509-result.push({ name, path: path.join(customThemesDir, file) });
510-}
511-}
512-}
513-}
514-515-for (const [name, theme] of registeredThemes.entries()) {
516-if (!result.some((t) => t.name === name)) {
517-result.push({ name, path: theme.sourcePath });
518-}
519-}
520-521-return result.toSorted((a, b) => a.name.localeCompare(b.name));
522-}
523-524465function parseThemeJson(label: string, json: unknown): ThemeJson {
525466if (!validateThemeJson.Check(json)) {
526467const errors = Array.from(validateThemeJson.Errors(json));
@@ -635,14 +576,6 @@ function loadTheme(name: string, mode?: ColorMode): Theme {
635576return createTheme(themeJson, mode);
636577}
637578638-export function getThemeByName(name: string): Theme | undefined {
639-try {
640-return loadTheme(name);
641-} catch {
642-return undefined;
643-}
644-}
645-646579export type TerminalTheme = "dark" | "light";
647580648581export interface RgbColor {
@@ -685,67 +618,6 @@ function getAnsiColorLuminance(index: number): number {
685618return getRgbColorLuminance(hexToRgb(ansi256ToHex(index)));
686619}
687620688-export function getThemeForRgbColor(rgb: RgbColor): TerminalTheme {
689-return getRgbColorLuminance(rgb) >= 0.5 ? "light" : "dark";
690-}
691-692-function parseOscHexChannel(channel: string): number | undefined {
693-if (!/^[0-9a-f]+$/i.test(channel)) {
694-return undefined;
695-}
696-const max = 16 ** channel.length - 1;
697-if (max <= 0) {
698-return undefined;
699-}
700-return Math.round((Number.parseInt(channel, 16) / max) * 255);
701-}
702-703-export function parseOsc11BackgroundColor(data: string): RgbColor | undefined {
704-const prefix = "\u001B]11;";
705-const belSuffix = "\u0007";
706-const escSuffix = "\u001B\\";
707-if (!data.startsWith(prefix)) {
708-return undefined;
709-}
710-711-const suffixLength = data.endsWith(belSuffix)
712- ? belSuffix.length
713- : data.endsWith(escSuffix)
714- ? escSuffix.length
715- : 0;
716-if (suffixLength === 0) {
717-return undefined;
718-}
719-720-const value = data.slice(prefix.length, -suffixLength).trim();
721-if (value.includes("\u0007") || value.includes("\u001B")) {
722-return undefined;
723-}
724-if (value.startsWith("#")) {
725-const hex = value.slice(1);
726-if (/^[0-9a-f]{6}$/i.test(hex)) {
727-return hexToRgb(value);
728-}
729-if (/^[0-9a-f]{12}$/i.test(hex)) {
730-const r = parseOscHexChannel(hex.slice(0, 4));
731-const g = parseOscHexChannel(hex.slice(4, 8));
732-const b = parseOscHexChannel(hex.slice(8, 12));
733-return r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined;
734-}
735-return undefined;
736-}
737-738-const rgbValue = value.replace(/^rgba?:/i, "");
739-const [red, green, blue] = rgbValue.split("/");
740-if (red === undefined || green === undefined || blue === undefined) {
741-return undefined;
742-}
743-const r = parseOscHexChannel(red);
744-const g = parseOscHexChannel(green);
745-const b = parseOscHexChannel(blue);
746-return r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined;
747-}
748-749621export function detectTerminalBackground(
750622options: TerminalThemeDetectionOptions = {},
751623): TerminalThemeDetection {
@@ -998,83 +870,6 @@ function ansi256ToHex(index: number): string {
998870return `#${grayHex}${grayHex}${grayHex}`;
999871}
10008721001-/**
1002- * Get resolved theme colors as CSS-compatible hex strings.
1003- * Used by HTML export to generate CSS custom properties.
1004- */
1005-export function getResolvedThemeColors(themeName?: string): Record<string, string> {
1006-const name = themeName ?? currentThemeName ?? getDefaultTheme();
1007-const isLight = name === "light";
1008-const themeJson = loadThemeJson(name);
1009-const resolved = resolveThemeColors(themeJson.colors, themeJson.vars);
1010-1011-// Default text color for empty values (terminal uses default fg color)
1012-const defaultText = isLight ? "#000000" : "#e5e5e7";
1013-1014-const cssColors: Record<string, string> = {};
1015-for (const [key, value] of Object.entries(resolved)) {
1016-if (typeof value === "number") {
1017-cssColors[key] = ansi256ToHex(value);
1018-} else if (value === "") {
1019-// Empty means default terminal color - use sensible fallback for HTML
1020-cssColors[key] = defaultText;
1021-} else {
1022-cssColors[key] = value;
1023-}
1024-}
1025-return cssColors;
1026-}
1027-1028-/**
1029- * Check if a theme is a "light" theme (for CSS that needs light/dark variants).
1030- */
1031-export function isLightTheme(themeName?: string): boolean {
1032-// Currently just check the name - could be extended to analyze colors
1033-return themeName === "light";
1034-}
1035-1036-/**
1037- * Get explicit export colors from theme JSON, if specified.
1038- * Returns undefined for each color that isn't explicitly set.
1039- */
1040-export function getThemeExportColors(themeName?: string): {
1041-pageBg?: string;
1042-cardBg?: string;
1043-infoBg?: string;
1044-} {
1045-const name = themeName ?? currentThemeName ?? getDefaultTheme();
1046-try {
1047-const themeJson = loadThemeJson(name);
1048-const exportSection = themeJson.export;
1049-if (!exportSection) {
1050-return {};
1051-}
1052-1053-const vars = themeJson.vars ?? {};
1054-const resolve = (value: ColorValue | undefined): string | undefined => {
1055-if (value === undefined) {
1056-return undefined;
1057-}
1058-const resolved = resolveVarRefs(value, vars);
1059-if (typeof resolved === "number") {
1060-return ansi256ToHex(resolved);
1061-}
1062-if (resolved === "") {
1063-return undefined;
1064-}
1065-return resolved;
1066-};
1067-1068-return {
1069-pageBg: resolve(exportSection.pageBg),
1070-cardBg: resolve(exportSection.cardBg),
1071-infoBg: resolve(exportSection.infoBg),
1072-};
1073-} catch {
1074-return {};
1075-}
1076-}
1077-1078873// ============================================================================
1079874// TUI Helpers
1080875// ============================================================================
@@ -1209,70 +1004,3 @@ export function getLanguageFromPath(filePath: string): string | undefined {
1209100412101005return extToLang[ext];
12111006}
1212-1213-export function getMarkdownTheme(): MarkdownTheme {
1214-return {
1215-heading: (text: string) => theme.fg("mdHeading", text),
1216-link: (text: string) => theme.fg("mdLink", text),
1217-linkUrl: (text: string) => theme.fg("mdLinkUrl", text),
1218-code: (text: string) => theme.fg("mdCode", text),
1219-codeBlock: (text: string) => theme.fg("mdCodeBlock", text),
1220-codeBlockBorder: (text: string) => theme.fg("mdCodeBlockBorder", text),
1221-quote: (text: string) => theme.fg("mdQuote", text),
1222-quoteBorder: (text: string) => theme.fg("mdQuoteBorder", text),
1223-hr: (text: string) => theme.fg("mdHr", text),
1224-listBullet: (text: string) => theme.fg("mdListBullet", text),
1225-bold: (text: string) => theme.bold(text),
1226-italic: (text: string) => theme.italic(text),
1227-underline: (text: string) => theme.underline(text),
1228-strikethrough: (text: string) => chalk.strikethrough(text),
1229-highlightCode: (code: string, lang?: string): string[] => {
1230-// Validate language before highlighting to avoid stderr spam from cli-highlight
1231-const validLang = lang && supportsLanguage(lang) ? lang : undefined;
1232-// Skip highlighting when no valid language is specified. cli-highlight's
1233-// auto-detection is unreliable and can misidentify prose as AppleScript,
1234-// LiveCodeServer, etc., coloring random English words as keywords.
1235-if (!validLang) {
1236-return code.split("\n").map((line) => theme.fg("mdCodeBlock", line));
1237-}
1238-const opts = {
1239-language: validLang,
1240-ignoreIllegals: true,
1241-theme: getCliHighlightTheme(theme),
1242-};
1243-try {
1244-return highlight(code, opts).split("\n");
1245-} catch {
1246-return code.split("\n").map((line) => theme.fg("mdCodeBlock", line));
1247-}
1248-},
1249-};
1250-}
1251-1252-export function getSelectListTheme(): SelectListTheme {
1253-return {
1254-selectedPrefix: (text: string) => theme.fg("accent", text),
1255-selectedText: (text: string) => theme.fg("accent", text),
1256-description: (text: string) => theme.fg("muted", text),
1257-scrollInfo: (text: string) => theme.fg("muted", text),
1258-noMatch: (text: string) => theme.fg("muted", text),
1259-};
1260-}
1261-1262-export function getEditorTheme(): EditorTheme {
1263-return {
1264-borderColor: (text: string) => theme.fg("borderMuted", text),
1265-selectList: getSelectListTheme(),
1266-};
1267-}
1268-1269-export function getSettingsListTheme(): SettingsListTheme {
1270-return {
1271-label: (text: string, selected: boolean) => (selected ? theme.fg("accent", text) : text),
1272-value: (text: string, selected: boolean) =>
1273-selected ? theme.fg("accent", text) : theme.fg("muted", text),
1274-description: (text: string) => theme.fg("dim", text),
1275-cursor: theme.fg("accent", "→ "),
1276-hint: (text: string) => theme.fg("dim", text),
1277-};
1278-}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。