summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/cli/src/ui/components/Footer.tsx40
-rw-r--r--packages/cli/src/ui/components/InputPrompt.tsx14
-rw-r--r--packages/cli/src/ui/semantic-colors.ts26
-rw-r--r--packages/cli/src/ui/themes/ansi-light.ts2
-rw-r--r--packages/cli/src/ui/themes/ansi.ts2
-rw-r--r--packages/cli/src/ui/themes/atom-one-dark.ts2
-rw-r--r--packages/cli/src/ui/themes/ayu-light.ts2
-rw-r--r--packages/cli/src/ui/themes/ayu.ts2
-rw-r--r--packages/cli/src/ui/themes/default-light.ts2
-rw-r--r--packages/cli/src/ui/themes/default.ts2
-rw-r--r--packages/cli/src/ui/themes/dracula.ts2
-rw-r--r--packages/cli/src/ui/themes/github-dark.ts2
-rw-r--r--packages/cli/src/ui/themes/github-light.ts2
-rw-r--r--packages/cli/src/ui/themes/googlecode.ts2
-rw-r--r--packages/cli/src/ui/themes/no-color.ts32
-rw-r--r--packages/cli/src/ui/themes/semantic-tokens.ts127
-rw-r--r--packages/cli/src/ui/themes/shades-of-purple.ts2
-rw-r--r--packages/cli/src/ui/themes/theme-manager.test.ts9
-rw-r--r--packages/cli/src/ui/themes/theme-manager.ts9
-rw-r--r--packages/cli/src/ui/themes/theme.test.ts50
-rw-r--r--packages/cli/src/ui/themes/theme.ts261
-rw-r--r--packages/cli/src/ui/themes/xcode.ts2
22 files changed, 396 insertions, 198 deletions
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<FooterProps> = ({
>
<Box>
{debugMode && <DebugProfiler />}
- {vimMode && <Text color={Colors.Gray}>[{vimMode}] </Text>}
+ {vimMode && <Text color={theme.text.secondary}>[{vimMode}] </Text>}
{nightly ? (
- <Gradient colors={Colors.GradientColors}>
+ <Gradient colors={theme.ui.gradient}>
<Text>
{displayPath}
{branchName && <Text> ({branchName}*)</Text>}
</Text>
</Gradient>
) : (
- <Text color={Colors.LightBlue}>
+ <Text color={theme.text.link}>
{displayPath}
- {branchName && <Text color={Colors.Gray}> ({branchName}*)</Text>}
+ {branchName && (
+ <Text color={theme.text.secondary}> ({branchName}*)</Text>
+ )}
</Text>
)}
{debugMode && (
- <Text color={Colors.AccentRed}>
+ <Text color={theme.status.error}>
{' ' + (debugMessage || '--debug')}
</Text>
)}
@@ -102,20 +104,22 @@ export const Footer: React.FC<FooterProps> = ({
{process.env.SANDBOX.replace(/^gemini-(?:cli-)?/, '')}
</Text>
) : process.env.SANDBOX === 'sandbox-exec' ? (
- <Text color={Colors.AccentYellow}>
+ <Text color={theme.status.warning}>
macOS Seatbelt{' '}
- <Text color={Colors.Gray}>({process.env.SEATBELT_PROFILE})</Text>
+ <Text color={theme.text.secondary}>
+ ({process.env.SEATBELT_PROFILE})
+ </Text>
</Text>
) : (
- <Text color={Colors.AccentRed}>
- no sandbox <Text color={Colors.Gray}>(see /docs)</Text>
+ <Text color={theme.status.error}>
+ no sandbox <Text color={theme.text.secondary}>(see /docs)</Text>
</Text>
)}
</Box>
{/* Right Section: Gemini Label and Console Summary */}
<Box alignItems="center" paddingTop={isNarrow ? 1 : 0}>
- <Text color={Colors.AccentBlue}>
+ <Text color={theme.text.accent}>
{isNarrow ? '' : ' '}
{model}{' '}
<ContextUsageDisplay
@@ -125,17 +129,17 @@ export const Footer: React.FC<FooterProps> = ({
</Text>
{corgiMode && (
<Text>
- <Text color={Colors.Gray}>| </Text>
- <Text color={Colors.AccentRed}>▼</Text>
- <Text color={Colors.Foreground}>(´</Text>
- <Text color={Colors.AccentRed}>ᴥ</Text>
- <Text color={Colors.Foreground}>`)</Text>
- <Text color={Colors.AccentRed}>▼ </Text>
+ <Text color={theme.ui.symbol}>| </Text>
+ <Text color={theme.status.error}>▼</Text>
+ <Text color={theme.text.primary}>(´</Text>
+ <Text color={theme.status.error}>ᴥ</Text>
+ <Text color={theme.text.primary}>`)</Text>
+ <Text color={theme.status.error}>▼ </Text>
</Text>
)}
{!showErrorDetails && errorCount > 0 && (
<Box>
- <Text color={Colors.Gray}>| </Text>
+ <Text color={theme.ui.symbol}>| </Text>
<ConsoleSummaryDisplay errorCount={errorCount} />
</Box>
)}
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<InputPromptProps> = ({
<>
<Box
borderStyle="round"
- borderColor={shellModeActive ? Colors.AccentYellow : Colors.AccentBlue}
+ borderColor={
+ shellModeActive ? theme.status.warning : theme.border.focused
+ }
paddingX={1}
>
<Text
- color={shellModeActive ? Colors.AccentYellow : Colors.AccentPurple}
+ color={shellModeActive ? theme.status.warning : theme.text.accent}
>
{shellModeActive ? (
reverseSearchActive ? (
- <Text color={Colors.AccentCyan}>(r:) </Text>
+ <Text color={theme.text.link}>(r:) </Text>
) : (
'! '
)
@@ -490,10 +492,10 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
focus ? (
<Text>
{chalk.inverse(placeholder.slice(0, 1))}
- <Text color={Colors.Gray}>{placeholder.slice(1)}</Text>
+ <Text color={theme.text.secondary}>{placeholder.slice(1)}</Text>
</Text>
) : (
- <Text color={Colors.Gray}>{placeholder}</Text>
+ <Text color={theme.text.secondary}>{placeholder}</Text>
)
) : (
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 <https://twitter.com/mrahmadawais/>
*/
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';
@@ -135,6 +136,14 @@ class ThemeManager {
}
/**
+ * 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<CustomTheme> = { ...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<CustomTheme> = { ...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<string, CSSProperties>,
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<string, CSSProperties> = {
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<CustomTheme>): {
error?: string;
warning?: string;
} {
- // Check required fields
- const requiredFields: Array<keyof CustomTheme> = [
- '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<keyof CustomTheme> = [
- '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<keyof CustomTheme> = [
- '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<CustomTheme>): {
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,
);