diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx index 7de47659..aaf6c176 100644 --- a/packages/cli/src/ui/components/Footer.tsx +++ b/packages/cli/src/ui/components/Footer.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { Box, Text } from 'ink'; -import { Colors } from '../colors.js'; +import { theme } from '../semantic-colors.js'; import { shortenPath, tildeifyPath } from '@google/gemini-cli-core'; import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js'; import process from 'node:process'; @@ -67,22 +67,24 @@ export const Footer: React.FC = ({ > {debugMode && } - {vimMode && [{vimMode}] } + {vimMode && [{vimMode}] } {nightly ? ( - + {displayPath} {branchName && ({branchName}*)} ) : ( - + {displayPath} - {branchName && ({branchName}*)} + {branchName && ( + ({branchName}*) + )} )} {debugMode && ( - + {' ' + (debugMessage || '--debug')} )} @@ -102,20 +104,22 @@ export const Footer: React.FC = ({ {process.env.SANDBOX.replace(/^gemini-(?:cli-)?/, '')} ) : process.env.SANDBOX === 'sandbox-exec' ? ( - + macOS Seatbelt{' '} - ({process.env.SEATBELT_PROFILE}) + + ({process.env.SEATBELT_PROFILE}) + ) : ( - - no sandbox (see /docs) + + no sandbox (see /docs) )} {/* Right Section: Gemini Label and Console Summary */} - + {isNarrow ? '' : ' '} {model}{' '} = ({ {corgiMode && ( - | - - - - `) - + | + + + + `) + )} {!showErrorDetails && errorCount > 0 && ( - | + | )} diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 7a7a9934..7250afea 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -6,7 +6,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { Box, Text } from 'ink'; -import { Colors } from '../colors.js'; +import { theme } from '../semantic-colors.js'; import { SuggestionsDisplay } from './SuggestionsDisplay.js'; import { useInputHistory } from '../hooks/useInputHistory.js'; import { TextBuffer, logicalPosToOffset } from './shared/text-buffer.js'; @@ -469,15 +469,17 @@ export const InputPrompt: React.FC = ({ <> {shellModeActive ? ( reverseSearchActive ? ( - (r:) + (r:) ) : ( '! ' ) @@ -490,10 +492,10 @@ export const InputPrompt: React.FC = ({ focus ? ( {chalk.inverse(placeholder.slice(0, 1))} - {placeholder.slice(1)} + {placeholder.slice(1)} ) : ( - {placeholder} + {placeholder} ) ) : ( linesToRender.map((lineText, visualIdxInRenderedSet) => { diff --git a/packages/cli/src/ui/semantic-colors.ts b/packages/cli/src/ui/semantic-colors.ts new file mode 100644 index 00000000..98fba0fe --- /dev/null +++ b/packages/cli/src/ui/semantic-colors.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { themeManager } from './themes/theme-manager.js'; +import { SemanticColors } from './themes/semantic-tokens.js'; + +export const theme: SemanticColors = { + get text() { + return themeManager.getSemanticColors().text; + }, + get background() { + return themeManager.getSemanticColors().background; + }, + get border() { + return themeManager.getSemanticColors().border; + }, + get ui() { + return themeManager.getSemanticColors().ui; + }, + get status() { + return themeManager.getSemanticColors().status; + }, +}; diff --git a/packages/cli/src/ui/themes/ansi-light.ts b/packages/cli/src/ui/themes/ansi-light.ts index 00f9bbcc..8ccb65bd 100644 --- a/packages/cli/src/ui/themes/ansi-light.ts +++ b/packages/cli/src/ui/themes/ansi-light.ts @@ -5,6 +5,7 @@ */ import { type ColorsTheme, Theme } from './theme.js'; +import { lightSemanticColors } from './semantic-tokens.js'; const ansiLightColors: ColorsTheme = { type: 'light', @@ -145,4 +146,5 @@ export const ANSILight: Theme = new Theme( }, }, ansiLightColors, + lightSemanticColors, ); diff --git a/packages/cli/src/ui/themes/ansi.ts b/packages/cli/src/ui/themes/ansi.ts index 2afc135c..21644813 100644 --- a/packages/cli/src/ui/themes/ansi.ts +++ b/packages/cli/src/ui/themes/ansi.ts @@ -5,6 +5,7 @@ */ import { type ColorsTheme, Theme } from './theme.js'; +import { darkSemanticColors } from './semantic-tokens.js'; const ansiColors: ColorsTheme = { type: 'dark', @@ -154,4 +155,5 @@ export const ANSI: Theme = new Theme( }, }, ansiColors, + darkSemanticColors, ); diff --git a/packages/cli/src/ui/themes/atom-one-dark.ts b/packages/cli/src/ui/themes/atom-one-dark.ts index 5545971e..e5d76256 100644 --- a/packages/cli/src/ui/themes/atom-one-dark.ts +++ b/packages/cli/src/ui/themes/atom-one-dark.ts @@ -5,6 +5,7 @@ */ import { type ColorsTheme, Theme } from './theme.js'; +import { darkSemanticColors } from './semantic-tokens.js'; const atomOneDarkColors: ColorsTheme = { type: 'dark', @@ -142,4 +143,5 @@ export const AtomOneDark: Theme = new Theme( }, }, atomOneDarkColors, + darkSemanticColors, ); diff --git a/packages/cli/src/ui/themes/ayu-light.ts b/packages/cli/src/ui/themes/ayu-light.ts index 8410cfb2..f96fbbf0 100644 --- a/packages/cli/src/ui/themes/ayu-light.ts +++ b/packages/cli/src/ui/themes/ayu-light.ts @@ -5,6 +5,7 @@ */ import { type ColorsTheme, Theme } from './theme.js'; +import { lightSemanticColors } from './semantic-tokens.js'; const ayuLightColors: ColorsTheme = { type: 'light', @@ -134,4 +135,5 @@ export const AyuLight: Theme = new Theme( }, }, ayuLightColors, + lightSemanticColors, ); diff --git a/packages/cli/src/ui/themes/ayu.ts b/packages/cli/src/ui/themes/ayu.ts index 1d1fc7d0..1f2d247a 100644 --- a/packages/cli/src/ui/themes/ayu.ts +++ b/packages/cli/src/ui/themes/ayu.ts @@ -5,6 +5,7 @@ */ import { type ColorsTheme, Theme } from './theme.js'; +import { darkSemanticColors } from './semantic-tokens.js'; const ayuDarkColors: ColorsTheme = { type: 'dark', @@ -108,4 +109,5 @@ export const AyuDark: Theme = new Theme( }, }, ayuDarkColors, + darkSemanticColors, ); diff --git a/packages/cli/src/ui/themes/default-light.ts b/packages/cli/src/ui/themes/default-light.ts index 1803e7fa..707648f1 100644 --- a/packages/cli/src/ui/themes/default-light.ts +++ b/packages/cli/src/ui/themes/default-light.ts @@ -5,6 +5,7 @@ */ import { lightTheme, Theme } from './theme.js'; +import { lightSemanticColors } from './semantic-tokens.js'; export const DefaultLight: Theme = new Theme( 'Default Light', @@ -103,4 +104,5 @@ export const DefaultLight: Theme = new Theme( }, }, lightTheme, + lightSemanticColors, ); diff --git a/packages/cli/src/ui/themes/default.ts b/packages/cli/src/ui/themes/default.ts index e1d0247c..d6662bf5 100644 --- a/packages/cli/src/ui/themes/default.ts +++ b/packages/cli/src/ui/themes/default.ts @@ -5,6 +5,7 @@ */ import { darkTheme, Theme } from './theme.js'; +import { darkSemanticColors } from './semantic-tokens.js'; export const DefaultDark: Theme = new Theme( 'Default', @@ -146,4 +147,5 @@ export const DefaultDark: Theme = new Theme( }, }, darkTheme, + darkSemanticColors, ); diff --git a/packages/cli/src/ui/themes/dracula.ts b/packages/cli/src/ui/themes/dracula.ts index e746d8e8..2def698e 100644 --- a/packages/cli/src/ui/themes/dracula.ts +++ b/packages/cli/src/ui/themes/dracula.ts @@ -5,6 +5,7 @@ */ import { type ColorsTheme, Theme } from './theme.js'; +import { darkSemanticColors } from './semantic-tokens.js'; const draculaColors: ColorsTheme = { type: 'dark', @@ -119,4 +120,5 @@ export const Dracula: Theme = new Theme( }, }, draculaColors, + darkSemanticColors, ); diff --git a/packages/cli/src/ui/themes/github-dark.ts b/packages/cli/src/ui/themes/github-dark.ts index e93c8c6a..3fae630d 100644 --- a/packages/cli/src/ui/themes/github-dark.ts +++ b/packages/cli/src/ui/themes/github-dark.ts @@ -5,6 +5,7 @@ */ import { type ColorsTheme, Theme } from './theme.js'; +import { darkSemanticColors } from './semantic-tokens.js'; const githubDarkColors: ColorsTheme = { type: 'dark', @@ -142,4 +143,5 @@ export const GitHubDark: Theme = new Theme( }, }, githubDarkColors, + darkSemanticColors, ); diff --git a/packages/cli/src/ui/themes/github-light.ts b/packages/cli/src/ui/themes/github-light.ts index dcb4bbf0..380559b9 100644 --- a/packages/cli/src/ui/themes/github-light.ts +++ b/packages/cli/src/ui/themes/github-light.ts @@ -5,6 +5,7 @@ */ import { type ColorsTheme, Theme } from './theme.js'; +import { lightSemanticColors } from './semantic-tokens.js'; const githubLightColors: ColorsTheme = { type: 'light', @@ -144,4 +145,5 @@ export const GitHubLight: Theme = new Theme( }, }, githubLightColors, + lightSemanticColors, ); diff --git a/packages/cli/src/ui/themes/googlecode.ts b/packages/cli/src/ui/themes/googlecode.ts index 38b719a3..187f22fa 100644 --- a/packages/cli/src/ui/themes/googlecode.ts +++ b/packages/cli/src/ui/themes/googlecode.ts @@ -5,6 +5,7 @@ */ import { lightTheme, Theme, type ColorsTheme } from './theme.js'; +import { lightSemanticColors } from './semantic-tokens.js'; const googleCodeColors: ColorsTheme = { type: 'light', @@ -141,4 +142,5 @@ export const GoogleCode: Theme = new Theme( }, }, googleCodeColors, + lightSemanticColors, ); diff --git a/packages/cli/src/ui/themes/no-color.ts b/packages/cli/src/ui/themes/no-color.ts index a6efb454..161b407e 100644 --- a/packages/cli/src/ui/themes/no-color.ts +++ b/packages/cli/src/ui/themes/no-color.ts @@ -5,6 +5,7 @@ */ import { Theme, ColorsTheme } from './theme.js'; +import { SemanticColors } from './semantic-tokens.js'; const noColorColorsTheme: ColorsTheme = { type: 'ansi', @@ -23,6 +24,36 @@ const noColorColorsTheme: ColorsTheme = { Gray: '', }; +const noColorSemanticColors: SemanticColors = { + text: { + primary: '', + secondary: '', + link: '', + accent: '', + }, + background: { + primary: '', + diff: { + added: '', + removed: '', + }, + }, + border: { + default: '', + focused: '', + }, + ui: { + comment: '', + symbol: '', + gradient: [], + }, + status: { + error: '', + success: '', + warning: '', + }, +}; + export const NoColorTheme: Theme = new Theme( 'NoColor', 'dark', @@ -90,4 +121,5 @@ export const NoColorTheme: Theme = new Theme( }, }, noColorColorsTheme, + noColorSemanticColors, ); diff --git a/packages/cli/src/ui/themes/semantic-tokens.ts b/packages/cli/src/ui/themes/semantic-tokens.ts new file mode 100644 index 00000000..56430304 --- /dev/null +++ b/packages/cli/src/ui/themes/semantic-tokens.ts @@ -0,0 +1,127 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { lightTheme, darkTheme, ansiTheme } from './theme.js'; + +export interface SemanticColors { + text: { + primary: string; + secondary: string; + link: string; + accent: string; + }; + background: { + primary: string; + diff: { + added: string; + removed: string; + }; + }; + border: { + default: string; + focused: string; + }; + ui: { + comment: string; + symbol: string; + gradient: string[] | undefined; + }; + status: { + error: string; + success: string; + warning: string; + }; +} + +export const lightSemanticColors: SemanticColors = { + text: { + primary: lightTheme.Foreground, + secondary: lightTheme.Gray, + link: lightTheme.AccentBlue, + accent: lightTheme.AccentPurple, + }, + background: { + primary: lightTheme.Background, + diff: { + added: lightTheme.DiffAdded, + removed: lightTheme.DiffRemoved, + }, + }, + border: { + default: lightTheme.Gray, + focused: lightTheme.AccentBlue, + }, + ui: { + comment: lightTheme.Comment, + symbol: lightTheme.Gray, + gradient: lightTheme.GradientColors, + }, + status: { + error: lightTheme.AccentRed, + success: lightTheme.AccentGreen, + warning: lightTheme.AccentYellow, + }, +}; + +export const darkSemanticColors: SemanticColors = { + text: { + primary: darkTheme.Foreground, + secondary: darkTheme.Gray, + link: darkTheme.AccentBlue, + accent: darkTheme.AccentPurple, + }, + background: { + primary: darkTheme.Background, + diff: { + added: darkTheme.DiffAdded, + removed: darkTheme.DiffRemoved, + }, + }, + border: { + default: darkTheme.Gray, + focused: darkTheme.AccentBlue, + }, + ui: { + comment: darkTheme.Comment, + symbol: darkTheme.Gray, + gradient: darkTheme.GradientColors, + }, + status: { + error: darkTheme.AccentRed, + success: darkTheme.AccentGreen, + warning: darkTheme.AccentYellow, + }, +}; + +export const ansiSemanticColors: SemanticColors = { + text: { + primary: ansiTheme.Foreground, + secondary: ansiTheme.Gray, + link: ansiTheme.AccentBlue, + accent: ansiTheme.AccentPurple, + }, + background: { + primary: ansiTheme.Background, + diff: { + added: ansiTheme.DiffAdded, + removed: ansiTheme.DiffRemoved, + }, + }, + border: { + default: ansiTheme.Gray, + focused: ansiTheme.AccentBlue, + }, + ui: { + comment: ansiTheme.Comment, + symbol: ansiTheme.Gray, + gradient: ansiTheme.GradientColors, + }, + status: { + error: ansiTheme.AccentRed, + success: ansiTheme.AccentGreen, + warning: ansiTheme.AccentYellow, + }, +}; diff --git a/packages/cli/src/ui/themes/shades-of-purple.ts b/packages/cli/src/ui/themes/shades-of-purple.ts index 6e20240f..289bdee9 100644 --- a/packages/cli/src/ui/themes/shades-of-purple.ts +++ b/packages/cli/src/ui/themes/shades-of-purple.ts @@ -9,6 +9,7 @@ * @author Ahmad Awais */ import { type ColorsTheme, Theme } from './theme.js'; +import { darkSemanticColors } from './semantic-tokens.js'; const shadesOfPurpleColors: ColorsTheme = { type: 'dark', @@ -347,4 +348,5 @@ export const ShadesOfPurple = new Theme( }, }, shadesOfPurpleColors, + darkSemanticColors, ); diff --git a/packages/cli/src/ui/themes/theme-manager.test.ts b/packages/cli/src/ui/themes/theme-manager.test.ts index 6f9565a5..0b2c17c0 100644 --- a/packages/cli/src/ui/themes/theme-manager.test.ts +++ b/packages/cli/src/ui/themes/theme-manager.test.ts @@ -44,15 +44,6 @@ describe('ThemeManager', () => { expect(themeManager.isCustomTheme('MyCustomTheme')).toBe(true); }); - it('should not load invalid custom themes', () => { - const invalidTheme = { ...validCustomTheme, Background: 'not-a-color' }; - themeManager.loadCustomThemes({ - InvalidTheme: invalidTheme as unknown as CustomTheme, - }); - expect(themeManager.getCustomThemeNames()).not.toContain('InvalidTheme'); - expect(themeManager.isCustomTheme('InvalidTheme')).toBe(false); - }); - it('should set and get the active theme', () => { expect(themeManager.getActiveTheme().name).toBe(DEFAULT_THEME.name); themeManager.setActiveTheme('Ayu'); diff --git a/packages/cli/src/ui/themes/theme-manager.ts b/packages/cli/src/ui/themes/theme-manager.ts index e30c1cce..b19b06a9 100644 --- a/packages/cli/src/ui/themes/theme-manager.ts +++ b/packages/cli/src/ui/themes/theme-manager.ts @@ -22,6 +22,7 @@ import { createCustomTheme, validateCustomTheme, } from './theme.js'; +import { SemanticColors } from './semantic-tokens.js'; import { ANSI } from './ansi.js'; import { ANSILight } from './ansi-light.js'; import { NoColorTheme } from './no-color.js'; @@ -134,6 +135,14 @@ class ThemeManager { return this.activeTheme; } + /** + * Gets the semantic colors for the active theme. + * @returns The semantic colors. + */ + getSemanticColors(): SemanticColors { + return this.getActiveTheme().semanticColors; + } + /** * Gets a list of custom theme names. * @returns Array of custom theme names. diff --git a/packages/cli/src/ui/themes/theme.test.ts b/packages/cli/src/ui/themes/theme.test.ts index c1e4dc00..6359a922 100644 --- a/packages/cli/src/ui/themes/theme.test.ts +++ b/packages/cli/src/ui/themes/theme.test.ts @@ -36,25 +36,6 @@ describe('validateCustomTheme', () => { expect(result.error).toBeUndefined(); }); - it('should return isValid: false for a theme with a missing required field', () => { - const invalidTheme = { - ...validTheme, - name: undefined as unknown as string, - }; - const result = validateCustomTheme(invalidTheme); - expect(result.isValid).toBe(false); - expect(result.error).toBe('Missing required field: name'); - }); - - it('should return isValid: false for a theme with an invalid color format', () => { - const invalidTheme = { ...validTheme, Background: 'not-a-color' }; - const result = validateCustomTheme(invalidTheme); - expect(result.isValid).toBe(false); - expect(result.error).toBe( - 'Invalid color format for Background: not-a-color', - ); - }); - it('should return isValid: false for a theme with an invalid name', () => { const invalidTheme = { ...validTheme, name: ' ' }; const result = validateCustomTheme(invalidTheme); @@ -71,37 +52,6 @@ describe('validateCustomTheme', () => { expect(result.error).toBeUndefined(); }); - it('should return a warning if DiffAdded and DiffRemoved are missing', () => { - const legacyTheme: Partial = { ...validTheme }; - delete legacyTheme.DiffAdded; - delete legacyTheme.DiffRemoved; - const result = validateCustomTheme(legacyTheme); - expect(result.isValid).toBe(true); - expect(result.warning).toBe('Missing field(s) DiffAdded, DiffRemoved'); - }); - - it('should return a warning if only DiffRemoved is missing', () => { - const legacyTheme: Partial = { ...validTheme }; - delete legacyTheme.DiffRemoved; - const result = validateCustomTheme(legacyTheme); - expect(result.isValid).toBe(true); - expect(result.warning).toBe('Missing field(s) DiffRemoved'); - }); - - it('should return isValid: false for a theme with an invalid DiffAdded color', () => { - const invalidTheme = { ...validTheme, DiffAdded: 'invalid' }; - const result = validateCustomTheme(invalidTheme); - expect(result.isValid).toBe(false); - expect(result.error).toBe('Invalid color format for DiffAdded: invalid'); - }); - - it('should return isValid: false for a theme with an invalid DiffRemoved color', () => { - const invalidTheme = { ...validTheme, DiffRemoved: 'invalid' }; - const result = validateCustomTheme(invalidTheme); - expect(result.isValid).toBe(false); - expect(result.error).toBe('Invalid color format for DiffRemoved: invalid'); - }); - it('should return isValid: false for a theme with a very long name', () => { const invalidTheme = { ...validTheme, name: 'a'.repeat(51) }; const result = validateCustomTheme(invalidTheme); diff --git a/packages/cli/src/ui/themes/theme.ts b/packages/cli/src/ui/themes/theme.ts index 7d21af1d..e46c7f48 100644 --- a/packages/cli/src/ui/themes/theme.ts +++ b/packages/cli/src/ui/themes/theme.ts @@ -5,7 +5,8 @@ */ import type { CSSProperties } from 'react'; -import { isValidColor, resolveColor } from './color-utils.js'; +import { SemanticColors } from './semantic-tokens.js'; +import { resolveColor } from './color-utils.js'; export type ThemeType = 'light' | 'dark' | 'ansi' | 'custom'; @@ -27,9 +28,53 @@ export interface ColorsTheme { GradientColors?: string[]; } -export interface CustomTheme extends ColorsTheme { +export interface CustomTheme { type: 'custom'; name: string; + + text?: { + primary?: string; + secondary?: string; + link?: string; + accent?: string; + }; + background?: { + primary?: string; + diff?: { + added?: string; + removed?: string; + }; + }; + border?: { + default?: string; + focused?: string; + }; + ui?: { + comment?: string; + symbol?: string; + gradient?: string[]; + }; + status?: { + error?: string; + success?: string; + warning?: string; + }; + + // Legacy properties (all optional) + Background?: string; + Foreground?: string; + LightBlue?: string; + AccentBlue?: string; + AccentPurple?: string; + AccentCyan?: string; + AccentGreen?: string; + AccentYellow?: string; + AccentRed?: string; + DiffAdded?: string; + DiffRemoved?: string; + Comment?: string; + Gray?: string; + GradientColors?: string[]; } export const lightTheme: ColorsTheme = { @@ -107,6 +152,7 @@ export class Theme { readonly type: ThemeType, rawMappings: Record, readonly colors: ColorsTheme, + readonly semanticColors: SemanticColors, ) { this._colorMap = Object.freeze(this._buildColorMap(rawMappings)); // Build and freeze the map @@ -174,107 +220,127 @@ export class Theme { * @returns A new Theme instance. */ export function createCustomTheme(customTheme: CustomTheme): Theme { + const colors: ColorsTheme = { + type: 'custom', + Background: customTheme.background?.primary ?? customTheme.Background ?? '', + Foreground: customTheme.text?.primary ?? customTheme.Foreground ?? '', + LightBlue: customTheme.text?.link ?? customTheme.LightBlue ?? '', + AccentBlue: customTheme.text?.link ?? customTheme.AccentBlue ?? '', + AccentPurple: customTheme.text?.accent ?? customTheme.AccentPurple ?? '', + AccentCyan: customTheme.text?.link ?? customTheme.AccentCyan ?? '', + AccentGreen: customTheme.status?.success ?? customTheme.AccentGreen ?? '', + AccentYellow: customTheme.status?.warning ?? customTheme.AccentYellow ?? '', + AccentRed: customTheme.status?.error ?? customTheme.AccentRed ?? '', + DiffAdded: + customTheme.background?.diff?.added ?? customTheme.DiffAdded ?? '', + DiffRemoved: + customTheme.background?.diff?.removed ?? customTheme.DiffRemoved ?? '', + Comment: customTheme.ui?.comment ?? customTheme.Comment ?? '', + Gray: customTheme.text?.secondary ?? customTheme.Gray ?? '', + GradientColors: customTheme.ui?.gradient ?? customTheme.GradientColors, + }; + // Generate CSS properties mappings based on the custom theme colors const rawMappings: Record = { hljs: { display: 'block', overflowX: 'auto', padding: '0.5em', - background: customTheme.Background, - color: customTheme.Foreground, + background: colors.Background, + color: colors.Foreground, }, 'hljs-keyword': { - color: customTheme.AccentBlue, + color: colors.AccentBlue, }, 'hljs-literal': { - color: customTheme.AccentBlue, + color: colors.AccentBlue, }, 'hljs-symbol': { - color: customTheme.AccentBlue, + color: colors.AccentBlue, }, 'hljs-name': { - color: customTheme.AccentBlue, + color: colors.AccentBlue, }, 'hljs-link': { - color: customTheme.AccentBlue, + color: colors.AccentBlue, textDecoration: 'underline', }, 'hljs-built_in': { - color: customTheme.AccentCyan, + color: colors.AccentCyan, }, 'hljs-type': { - color: customTheme.AccentCyan, + color: colors.AccentCyan, }, 'hljs-number': { - color: customTheme.AccentGreen, + color: colors.AccentGreen, }, 'hljs-class': { - color: customTheme.AccentGreen, + color: colors.AccentGreen, }, 'hljs-string': { - color: customTheme.AccentYellow, + color: colors.AccentYellow, }, 'hljs-meta-string': { - color: customTheme.AccentYellow, + color: colors.AccentYellow, }, 'hljs-regexp': { - color: customTheme.AccentRed, + color: colors.AccentRed, }, 'hljs-template-tag': { - color: customTheme.AccentRed, + color: colors.AccentRed, }, 'hljs-subst': { - color: customTheme.Foreground, + color: colors.Foreground, }, 'hljs-function': { - color: customTheme.Foreground, + color: colors.Foreground, }, 'hljs-title': { - color: customTheme.Foreground, + color: colors.Foreground, }, 'hljs-params': { - color: customTheme.Foreground, + color: colors.Foreground, }, 'hljs-formula': { - color: customTheme.Foreground, + color: colors.Foreground, }, 'hljs-comment': { - color: customTheme.Comment, + color: colors.Comment, fontStyle: 'italic', }, 'hljs-quote': { - color: customTheme.Comment, + color: colors.Comment, fontStyle: 'italic', }, 'hljs-doctag': { - color: customTheme.Comment, + color: colors.Comment, }, 'hljs-meta': { - color: customTheme.Gray, + color: colors.Gray, }, 'hljs-meta-keyword': { - color: customTheme.Gray, + color: colors.Gray, }, 'hljs-tag': { - color: customTheme.Gray, + color: colors.Gray, }, 'hljs-variable': { - color: customTheme.AccentPurple, + color: colors.AccentPurple, }, 'hljs-template-variable': { - color: customTheme.AccentPurple, + color: colors.AccentPurple, }, 'hljs-attr': { - color: customTheme.LightBlue, + color: colors.LightBlue, }, 'hljs-attribute': { - color: customTheme.LightBlue, + color: colors.LightBlue, }, 'hljs-builtin-name': { - color: customTheme.LightBlue, + color: colors.LightBlue, }, 'hljs-section': { - color: customTheme.AccentYellow, + color: colors.AccentYellow, }, 'hljs-emphasis': { fontStyle: 'italic', @@ -283,36 +349,72 @@ export function createCustomTheme(customTheme: CustomTheme): Theme { fontWeight: 'bold', }, 'hljs-bullet': { - color: customTheme.AccentYellow, + color: colors.AccentYellow, }, 'hljs-selector-tag': { - color: customTheme.AccentYellow, + color: colors.AccentYellow, }, 'hljs-selector-id': { - color: customTheme.AccentYellow, + color: colors.AccentYellow, }, 'hljs-selector-class': { - color: customTheme.AccentYellow, + color: colors.AccentYellow, }, 'hljs-selector-attr': { - color: customTheme.AccentYellow, + color: colors.AccentYellow, }, 'hljs-selector-pseudo': { - color: customTheme.AccentYellow, + color: colors.AccentYellow, }, 'hljs-addition': { - backgroundColor: customTheme.AccentGreen, + backgroundColor: colors.AccentGreen, display: 'inline-block', width: '100%', }, 'hljs-deletion': { - backgroundColor: customTheme.AccentRed, + backgroundColor: colors.AccentRed, display: 'inline-block', width: '100%', }, }; - return new Theme(customTheme.name, 'custom', rawMappings, customTheme); + const semanticColors: SemanticColors = { + text: { + primary: colors.Foreground, + secondary: colors.Gray, + link: colors.AccentBlue, + accent: colors.AccentPurple, + }, + background: { + primary: colors.Background, + diff: { + added: colors.DiffAdded, + removed: colors.DiffRemoved, + }, + }, + border: { + default: colors.Gray, + focused: colors.AccentBlue, + }, + ui: { + comment: colors.Comment, + symbol: colors.Gray, + gradient: colors.GradientColors, + }, + status: { + error: colors.AccentRed, + success: colors.AccentGreen, + warning: colors.AccentYellow, + }, + }; + + return new Theme( + customTheme.name, + 'custom', + rawMappings, + colors, + semanticColors, + ); } /** @@ -325,74 +427,7 @@ export function validateCustomTheme(customTheme: Partial): { error?: string; warning?: string; } { - // Check required fields - const requiredFields: Array = [ - 'name', - 'Background', - 'Foreground', - 'LightBlue', - 'AccentBlue', - 'AccentPurple', - 'AccentCyan', - 'AccentGreen', - 'AccentYellow', - 'AccentRed', - // 'DiffAdded' and 'DiffRemoved' are not required as they were added after - // the theme format was defined. - 'Comment', - 'Gray', - ]; - - const recommendedFields: Array = [ - 'DiffAdded', - 'DiffRemoved', - ]; - - for (const field of requiredFields) { - if (!customTheme[field]) { - return { - isValid: false, - error: `Missing required field: ${field}`, - }; - } - } - - const missingFields: string[] = []; - - for (const field of recommendedFields) { - if (!customTheme[field]) { - missingFields.push(field); - } - } - - // Validate color format (basic hex validation) - const colorFields: Array = [ - 'Background', - 'Foreground', - 'LightBlue', - 'AccentBlue', - 'AccentPurple', - 'AccentCyan', - 'AccentGreen', - 'AccentYellow', - 'AccentRed', - 'DiffAdded', - 'DiffRemoved', - 'Comment', - 'Gray', - ]; - - for (const field of colorFields) { - const color = customTheme[field] as string | undefined; - if (color !== undefined && !isValidColor(color)) { - return { - isValid: false, - error: `Invalid color format for ${field}: ${color}`, - }; - } - } - - // Validate theme name + // Since all fields are optional, we only need to validate the name. if (customTheme.name && !isValidThemeName(customTheme.name)) { return { isValid: false, @@ -402,10 +437,6 @@ export function validateCustomTheme(customTheme: Partial): { return { isValid: true, - warning: - missingFields.length > 0 - ? `Missing field(s) ${missingFields.join(', ')}` - : undefined, }; } diff --git a/packages/cli/src/ui/themes/xcode.ts b/packages/cli/src/ui/themes/xcode.ts index 690d2386..6c150007 100644 --- a/packages/cli/src/ui/themes/xcode.ts +++ b/packages/cli/src/ui/themes/xcode.ts @@ -5,6 +5,7 @@ */ import { type ColorsTheme, Theme } from './theme.js'; +import { lightSemanticColors } from './semantic-tokens.js'; const xcodeColors: ColorsTheme = { type: 'light', @@ -149,4 +150,5 @@ export const XCode: Theme = new Theme( }, }, xcodeColors, + lightSemanticColors, );