summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/themes
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/themes')
-rw-r--r--packages/cli/src/ui/themes/theme-manager.ts29
-rw-r--r--packages/cli/src/ui/themes/theme.ts278
-rw-r--r--packages/cli/src/ui/themes/vs2015.ts144
3 files changed, 451 insertions, 0 deletions
diff --git a/packages/cli/src/ui/themes/theme-manager.ts b/packages/cli/src/ui/themes/theme-manager.ts
new file mode 100644
index 00000000..e083575c
--- /dev/null
+++ b/packages/cli/src/ui/themes/theme-manager.ts
@@ -0,0 +1,29 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { VS2015 } from './vs2015.js';
+import { Theme } from './theme.js';
+
+class ThemeManager {
+ private static readonly DEFAULT_THEME: Theme = VS2015;
+ private readonly availableThemes: Theme[];
+ private activeTheme: Theme;
+
+ constructor() {
+ this.availableThemes = [VS2015];
+ this.activeTheme = ThemeManager.DEFAULT_THEME;
+ }
+
+ /**
+ * Returns the currently active theme object.
+ */
+ getActiveTheme(): Theme {
+ return this.activeTheme;
+ }
+}
+
+// Export an instance of the ThemeManager
+export const themeManager = new ThemeManager();
diff --git a/packages/cli/src/ui/themes/theme.ts b/packages/cli/src/ui/themes/theme.ts
new file mode 100644
index 00000000..f7bd1cd0
--- /dev/null
+++ b/packages/cli/src/ui/themes/theme.ts
@@ -0,0 +1,278 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type { CSSProperties } from 'react';
+
+export class Theme {
+ /**
+ * The user-facing name of the theme.
+ */
+ readonly name: string;
+
+ /**
+ * The default foreground color for text when no specific highlight rule applies.
+ * This is an Ink-compatible color string (hex or name).
+ */
+ readonly defaultColor: string;
+
+ /**
+ * Stores the mapping from highlight.js class names (e.g., 'hljs-keyword')
+ * to Ink-compatible color strings (hex or name).
+ */
+ protected readonly _colorMap: Readonly<Record<string, string>>;
+
+ // --- Static Helper Data ---
+
+ // Mapping from common CSS color names (lowercase) to hex codes (lowercase)
+ // Excludes names directly supported by Ink
+ private static readonly cssNameToHexMap: Readonly<Record<string, string>> = {
+ aliceblue: '#f0f8ff',
+ antiquewhite: '#faebd7',
+ aqua: '#00ffff',
+ aquamarine: '#7fffd4',
+ azure: '#f0ffff',
+ beige: '#f5f5dc',
+ bisque: '#ffe4c4',
+ blanchedalmond: '#ffebcd',
+ blueviolet: '#8a2be2',
+ brown: '#a52a2a',
+ burlywood: '#deb887',
+ cadetblue: '#5f9ea0',
+ chartreuse: '#7fff00',
+ chocolate: '#d2691e',
+ coral: '#ff7f50',
+ cornflowerblue: '#6495ed',
+ cornsilk: '#fff8dc',
+ crimson: '#dc143c',
+ darkblue: '#00008b',
+ darkcyan: '#008b8b',
+ darkgoldenrod: '#b8860b',
+ darkgray: '#a9a9a9',
+ darkgrey: '#a9a9a9',
+ darkgreen: '#006400',
+ darkkhaki: '#bdb76b',
+ darkmagenta: '#8b008b',
+ darkolivegreen: '#556b2f',
+ darkorange: '#ff8c00',
+ darkorchid: '#9932cc',
+ darkred: '#8b0000',
+ darksalmon: '#e9967a',
+ darkseagreen: '#8fbc8f',
+ darkslateblue: '#483d8b',
+ darkslategray: '#2f4f4f',
+ darkslategrey: '#2f4f4f',
+ darkturquoise: '#00ced1',
+ darkviolet: '#9400d3',
+ deeppink: '#ff1493',
+ deepskyblue: '#00bfff',
+ dimgray: '#696969',
+ dimgrey: '#696969',
+ dodgerblue: '#1e90ff',
+ firebrick: '#b22222',
+ floralwhite: '#fffaf0',
+ forestgreen: '#228b22',
+ fuchsia: '#ff00ff',
+ gainsboro: '#dcdcdc',
+ ghostwhite: '#f8f8ff',
+ gold: '#ffd700',
+ goldenrod: '#daa520',
+ greenyellow: '#adff2f',
+ honeydew: '#f0fff0',
+ hotpink: '#ff69b4',
+ indianred: '#cd5c5c',
+ indigo: '#4b0082',
+ ivory: '#fffff0',
+ khaki: '#f0e68c',
+ lavender: '#e6e6fa',
+ lavenderblush: '#fff0f5',
+ lawngreen: '#7cfc00',
+ lemonchiffon: '#fffacd',
+ lightblue: '#add8e6',
+ lightcoral: '#f08080',
+ lightcyan: '#e0ffff',
+ lightgoldenrodyellow: '#fafad2',
+ lightgray: '#d3d3d3',
+ lightgrey: '#d3d3d3',
+ lightgreen: '#90ee90',
+ lightpink: '#ffb6c1',
+ lightsalmon: '#ffa07a',
+ lightseagreen: '#20b2aa',
+ lightskyblue: '#87cefa',
+ lightslategray: '#778899',
+ lightslategrey: '#778899',
+ lightsteelblue: '#b0c4de',
+ lightyellow: '#ffffe0',
+ lime: '#00ff00',
+ limegreen: '#32cd32',
+ linen: '#faf0e6',
+ maroon: '#800000',
+ mediumaquamarine: '#66cdaa',
+ mediumblue: '#0000cd',
+ mediumorchid: '#ba55d3',
+ mediumpurple: '#9370db',
+ mediumseagreen: '#3cb371',
+ mediumslateblue: '#7b68ee',
+ mediumspringgreen: '#00fa9a',
+ mediumturquoise: '#48d1cc',
+ mediumvioletred: '#c71585',
+ midnightblue: '#191970',
+ mintcream: '#f5fffa',
+ mistyrose: '#ffe4e1',
+ moccasin: '#ffe4b5',
+ navajowhite: '#ffdead',
+ navy: '#000080',
+ oldlace: '#fdf5e6',
+ olive: '#808000',
+ olivedrab: '#6b8e23',
+ orange: '#ffa500',
+ orangered: '#ff4500',
+ orchid: '#da70d6',
+ palegoldenrod: '#eee8aa',
+ palegreen: '#98fb98',
+ paleturquoise: '#afeeee',
+ palevioletred: '#db7093',
+ papayawhip: '#ffefd5',
+ peachpuff: '#ffdab9',
+ peru: '#cd853f',
+ pink: '#ffc0cb',
+ plum: '#dda0dd',
+ powderblue: '#b0e0e6',
+ purple: '#800080',
+ rebeccapurple: '#663399',
+ rosybrown: '#bc8f8f',
+ royalblue: '#4169e1',
+ saddlebrown: '#8b4513',
+ salmon: '#fa8072',
+ sandybrown: '#f4a460',
+ seagreen: '#2e8b57',
+ seashell: '#fff5ee',
+ sienna: '#a0522d',
+ silver: '#c0c0c0',
+ skyblue: '#87ceeb',
+ slateblue: '#6a5acd',
+ slategray: '#708090',
+ slategrey: '#708090',
+ snow: '#fffafa',
+ springgreen: '#00ff7f',
+ steelblue: '#4682b4',
+ tan: '#d2b48c',
+ teal: '#008080',
+ thistle: '#d8bfd8',
+ tomato: '#ff6347',
+ turquoise: '#40e0d0',
+ violet: '#ee82ee',
+ wheat: '#f5deb3',
+ whitesmoke: '#f5f5f5',
+ yellowgreen: '#9acd32',
+ };
+
+ // Define the set of Ink's named colors for quick lookup
+ private static readonly inkSupportedNames = new Set([
+ 'black',
+ 'red',
+ 'green',
+ 'yellow',
+ 'blue',
+ 'cyan',
+ 'magenta',
+ 'white',
+ 'gray',
+ 'grey',
+ 'blackbright',
+ 'redbright',
+ 'greenbright',
+ 'yellowbright',
+ 'bluebright',
+ 'cyanbright',
+ 'magentabright',
+ 'whitebright',
+ ]);
+
+ /**
+ * Creates a new Theme instance.
+ * @param name The name of the theme.
+ * @param rawMappings The raw CSSProperties mappings from a react-syntax-highlighter theme object.
+ */
+ constructor(name: string, rawMappings: Record<string, CSSProperties>) {
+ this.name = name;
+ this._colorMap = Object.freeze(this._buildColorMap(rawMappings)); // Build and freeze the map
+
+ // Determine the default foreground color
+ const rawDefaultColor = rawMappings['hljs']?.color;
+ this.defaultColor =
+ (rawDefaultColor ? Theme._resolveColor(rawDefaultColor) : undefined) ??
+ ''; // Default to empty string if not found or resolvable
+ }
+
+ /**
+ * Gets the Ink-compatible color string for a given highlight.js class name.
+ * @param hljsClass The highlight.js class name (e.g., 'hljs-keyword', 'hljs-string').
+ * @returns The corresponding Ink color string (hex or name) if it exists.
+ */
+ getInkColor(hljsClass: string): string | undefined {
+ return this._colorMap[hljsClass];
+ }
+
+ /**
+ * Resolves a CSS color value (name or hex) into an Ink-compatible color string.
+ * @param colorValue The raw color string (e.g., 'blue', '#ff0000', 'darkkhaki').
+ * @returns An Ink-compatible color string (hex or name), or undefined if not resolvable.
+ */
+ private static _resolveColor(colorValue: string): string | undefined {
+ const lowerColor = colorValue.toLowerCase();
+
+ // 1. Check if it's already a hex code
+ if (lowerColor.startsWith('#')) {
+ return lowerColor; // Use hex directly
+ }
+ // 2. Check if it's an Ink supported name (lowercase)
+ else if (Theme.inkSupportedNames.has(lowerColor)) {
+ return lowerColor; // Use Ink name directly
+ }
+ // 3. Check if it's a known CSS name we can map to hex
+ else if (Theme.cssNameToHexMap[lowerColor]) {
+ return Theme.cssNameToHexMap[lowerColor]; // Use mapped hex
+ }
+
+ // 4. Could not resolve
+ console.warn(
+ `[Theme] Could not resolve color "${colorValue}" to an Ink-compatible format.`,
+ );
+ return undefined;
+ }
+
+ /**
+ * Builds the internal map from highlight.js class names to Ink-compatible color strings.
+ * This method is protected and primarily intended for use by the constructor.
+ * @param hljsTheme The raw CSSProperties mappings from a react-syntax-highlighter theme object.
+ * @returns An Ink-compatible theme map (Record<string, string>).
+ */
+ protected _buildColorMap(
+ hljsTheme: Record<string, CSSProperties>,
+ ): Record<string, string> {
+ const inkTheme: Record<string, string> = {};
+ for (const key in hljsTheme) {
+ // Ensure the key starts with 'hljs-' or is 'hljs' for the base style
+ if (!key.startsWith('hljs-') && key !== 'hljs') {
+ continue; // Skip keys not related to highlighting classes
+ }
+
+ const style = hljsTheme[key];
+ if (style?.color) {
+ const resolvedColor = Theme._resolveColor(style.color);
+ if (resolvedColor !== undefined) {
+ // Use the original key from the hljsTheme (e.g., 'hljs-keyword')
+ inkTheme[key] = resolvedColor;
+ }
+ // If color is not resolvable, it's omitted from the map,
+ // allowing fallback to the default foreground color.
+ }
+ // We currently only care about the 'color' property for Ink rendering.
+ // Other properties like background, fontStyle, etc., are ignored.
+ }
+ return inkTheme;
+ }
+}
diff --git a/packages/cli/src/ui/themes/vs2015.ts b/packages/cli/src/ui/themes/vs2015.ts
new file mode 100644
index 00000000..fa4ec1ee
--- /dev/null
+++ b/packages/cli/src/ui/themes/vs2015.ts
@@ -0,0 +1,144 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Theme } from './theme.js';
+
+export const VS2015: Theme = new Theme('VS2015', {
+ hljs: {
+ display: 'block',
+ overflowX: 'auto',
+ padding: '0.5em',
+ background: '#1E1E1E',
+ color: '#DCDCDC',
+ },
+ 'hljs-keyword': {
+ color: '#569CD6',
+ },
+ 'hljs-literal': {
+ color: '#569CD6',
+ },
+ 'hljs-symbol': {
+ color: '#569CD6',
+ },
+ 'hljs-name': {
+ color: '#569CD6',
+ },
+ 'hljs-link': {
+ color: '#569CD6',
+ textDecoration: 'underline',
+ },
+ 'hljs-built_in': {
+ color: '#4EC9B0',
+ },
+ 'hljs-type': {
+ color: '#4EC9B0',
+ },
+ 'hljs-number': {
+ color: '#B8D7A3',
+ },
+ 'hljs-class': {
+ color: '#B8D7A3',
+ },
+ 'hljs-string': {
+ color: '#D69D85',
+ },
+ 'hljs-meta-string': {
+ color: '#D69D85',
+ },
+ 'hljs-regexp': {
+ color: '#9A5334',
+ },
+ 'hljs-template-tag': {
+ color: '#9A5334',
+ },
+ 'hljs-subst': {
+ color: '#DCDCDC',
+ },
+ 'hljs-function': {
+ color: '#DCDCDC',
+ },
+ 'hljs-title': {
+ color: '#DCDCDC',
+ },
+ 'hljs-params': {
+ color: '#DCDCDC',
+ },
+ 'hljs-formula': {
+ color: '#DCDCDC',
+ },
+ 'hljs-comment': {
+ color: '#57A64A',
+ fontStyle: 'italic',
+ },
+ 'hljs-quote': {
+ color: '#57A64A',
+ fontStyle: 'italic',
+ },
+ 'hljs-doctag': {
+ color: '#608B4E',
+ },
+ 'hljs-meta': {
+ color: '#9B9B9B',
+ },
+ 'hljs-meta-keyword': {
+ color: '#9B9B9B',
+ },
+ 'hljs-tag': {
+ color: '#9B9B9B',
+ },
+ 'hljs-variable': {
+ color: '#BD63C5',
+ },
+ 'hljs-template-variable': {
+ color: '#BD63C5',
+ },
+ 'hljs-attr': {
+ color: '#9CDCFE',
+ },
+ 'hljs-attribute': {
+ color: '#9CDCFE',
+ },
+ 'hljs-builtin-name': {
+ color: '#9CDCFE',
+ },
+ 'hljs-section': {
+ color: 'gold',
+ },
+ 'hljs-emphasis': {
+ fontStyle: 'italic',
+ },
+ 'hljs-strong': {
+ fontWeight: 'bold',
+ },
+ 'hljs-bullet': {
+ color: '#D7BA7D',
+ },
+ 'hljs-selector-tag': {
+ color: '#D7BA7D',
+ },
+ 'hljs-selector-id': {
+ color: '#D7BA7D',
+ },
+ 'hljs-selector-class': {
+ color: '#D7BA7D',
+ },
+ 'hljs-selector-attr': {
+ color: '#D7BA7D',
+ },
+ 'hljs-selector-pseudo': {
+ color: '#D7BA7D',
+ },
+ 'hljs-addition': {
+ backgroundColor: '#144212',
+ display: 'inline-block',
+ width: '100%',
+ },
+ 'hljs-deletion': {
+ backgroundColor: '#600',
+ display: 'inline-block',
+ width: '100%',
+ },
+});