summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/ui/App.tsx64
-rw-r--r--packages/cli/src/ui/components/Header.tsx2
-rw-r--r--packages/cli/src/ui/components/ThemeDialog.tsx49
-rw-r--r--packages/cli/src/ui/hooks/useThemeCommand.ts40
-rw-r--r--packages/cli/src/ui/themes/atom-one-dark.ts122
-rw-r--r--packages/cli/src/ui/themes/dracula.ts99
-rw-r--r--packages/cli/src/ui/themes/github.ts124
-rw-r--r--packages/cli/src/ui/themes/googlecode.ts121
-rw-r--r--packages/cli/src/ui/themes/theme-manager.ts47
-rw-r--r--packages/cli/src/ui/themes/vs.ts101
-rw-r--r--packages/cli/src/ui/themes/xcode.ts129
11 files changed, 876 insertions, 22 deletions
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index 43a1d1e6..48184cea 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -4,17 +4,19 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React, { useState, useMemo } from 'react';
+import React, { useState, useMemo, useCallback } from 'react';
import { Box, Text } from 'ink';
import { StreamingState, type HistoryItem } from './types.js';
import { useGeminiStream } from './hooks/useGeminiStream.js';
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
import { useInputHistory } from './hooks/useInputHistory.js';
+import { useThemeCommand } from './hooks/useThemeCommand.js';
import { Header } from './components/Header.js';
import { HistoryDisplay } from './components/HistoryDisplay.js';
import { LoadingIndicator } from './components/LoadingIndicator.js';
import { InputPrompt } from './components/InputPrompt.js';
import { Footer } from './components/Footer.js';
+import { ThemeDialog } from './components/ThemeDialog.js';
import { ITermDetectionWarning } from './utils/itermDetection.js';
import {
useStartupWarnings,
@@ -22,13 +24,13 @@ import {
} from './hooks/useAppEffects.js';
import { shortenPath, type Config } from '@gemini-code/server';
import { Colors } from './colors.js';
+import { Tips } from './components/Tips.js';
interface AppProps {
config: Config;
}
export const App = ({ config }: AppProps) => {
- // Destructured prop
const [history, setHistory] = useState<HistoryItem[]>([]);
const [startupWarnings, setStartupWarnings] = useState<string[]>([]);
const { streamingState, submitQuery, initError, debugMessage } =
@@ -36,9 +38,24 @@ export const App = ({ config }: AppProps) => {
const { elapsedTime, currentLoadingPhrase } =
useLoadingIndicator(streamingState);
+ const { isThemeDialogOpen, openThemeDialog, handleThemeSelect } =
+ useThemeCommand();
+
useStartupWarnings(setStartupWarnings);
useInitializationErrorEffect(initError, history, setHistory);
+ const handleFinalSubmit = useCallback(
+ (submittedValue: string) => {
+ const trimmedValue = submittedValue.trim();
+ if (trimmedValue === '/theme') {
+ openThemeDialog();
+ } else if (trimmedValue.length > 0) {
+ submitQuery(submittedValue);
+ }
+ },
+ [openThemeDialog, submitQuery],
+ );
+
const userMessages = useMemo(
() =>
history
@@ -56,13 +73,16 @@ export const App = ({ config }: AppProps) => {
const { query, handleSubmit: handleHistorySubmit } = useInputHistory({
userMessages,
- onSubmit: submitQuery,
+ onSubmit: handleFinalSubmit,
isActive: isInputActive,
});
+ // --- Render Logic ---
+
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<Header />
+ <Tips />
{startupWarnings.length > 0 && (
<Box
@@ -112,25 +132,31 @@ export const App = ({ config }: AppProps) => {
</Box>
)}
- <Box flexDirection="column">
- <HistoryDisplay history={history} onSubmit={submitQuery} />
- <LoadingIndicator
- isLoading={streamingState === StreamingState.Responding}
- currentLoadingPhrase={currentLoadingPhrase}
- elapsedTime={elapsedTime}
- />
- </Box>
-
- {isInputActive && (
+ {isThemeDialogOpen ? (
+ <ThemeDialog onSelect={handleThemeSelect} />
+ ) : (
<>
- <Box>
- <Text color={Colors.SubtleComment}>cwd: </Text>
- <Text color={Colors.LightBlue}>
- {shortenPath(config.getTargetDir(), /*maxLength*/ 70)}
- </Text>
+ <Box flexDirection="column">
+ <HistoryDisplay history={history} onSubmit={submitQuery} />
+ <LoadingIndicator
+ isLoading={streamingState === StreamingState.Responding}
+ currentLoadingPhrase={currentLoadingPhrase}
+ elapsedTime={elapsedTime}
+ />
</Box>
- <InputPrompt onSubmit={handleHistorySubmit} />
+ {isInputActive && (
+ <>
+ <Box>
+ <Text color={Colors.SubtleComment}>cwd: </Text>
+ <Text color={Colors.LightBlue}>
+ {shortenPath(config.getTargetDir(), /*maxLength*/ 70)}
+ </Text>
+ </Box>
+
+ <InputPrompt onSubmit={handleHistorySubmit} />
+ </>
+ )}
</>
)}
diff --git a/packages/cli/src/ui/components/Header.tsx b/packages/cli/src/ui/components/Header.tsx
index 8861389b..5706cee6 100644
--- a/packages/cli/src/ui/components/Header.tsx
+++ b/packages/cli/src/ui/components/Header.tsx
@@ -7,7 +7,6 @@
import React from 'react';
import { Box, Text } from 'ink';
import Gradient from 'ink-gradient';
-import { Tips } from './Tips.js';
const gradientColors = ['#4796E4', '#847ACE', '#C3677F'];
@@ -32,6 +31,5 @@ export const Header: React.FC = () => (
`}</Text>
</Gradient>
</Box>
- <Tips />
</>
);
diff --git a/packages/cli/src/ui/components/ThemeDialog.tsx b/packages/cli/src/ui/components/ThemeDialog.tsx
new file mode 100644
index 00000000..b3e4f063
--- /dev/null
+++ b/packages/cli/src/ui/components/ThemeDialog.tsx
@@ -0,0 +1,49 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+import { themeManager } from '../themes/theme-manager.js';
+import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
+
+interface ThemeDialogProps {
+ /** Callback function when a theme is selected */
+ onSelect: (themeName: string) => void;
+}
+
+export function ThemeDialog({ onSelect }: ThemeDialogProps): React.JSX.Element {
+ const themeItems = themeManager.getAvailableThemes().map((theme) => ({
+ label: theme.active ? `${theme.name} (Active)` : theme.name,
+ value: theme.name,
+ }));
+ const initialIndex = themeItems.findIndex(
+ (item) => item.value === themeManager.getActiveTheme().name,
+ );
+ return (
+ <Box
+ borderStyle="round"
+ borderColor={Colors.AccentCyan}
+ flexDirection="column"
+ padding={1}
+ width="50%"
+ >
+ <Box marginBottom={1}>
+ <Text bold>Select Theme</Text>
+ </Box>
+ <RadioButtonSelect
+ items={themeItems}
+ initialIndex={initialIndex}
+ onSelect={onSelect}
+ />
+ <Box marginTop={1}>
+ <Text color={Colors.SubtleComment}>
+ (Use ↑/↓ arrows and Enter to select)
+ </Text>
+ </Box>
+ </Box>
+ );
+}
diff --git a/packages/cli/src/ui/hooks/useThemeCommand.ts b/packages/cli/src/ui/hooks/useThemeCommand.ts
new file mode 100644
index 00000000..85bd4906
--- /dev/null
+++ b/packages/cli/src/ui/hooks/useThemeCommand.ts
@@ -0,0 +1,40 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { useState, useCallback } from 'react';
+import { themeManager } from '../themes/theme-manager.js';
+
+interface UseThemeCommandReturn {
+ isThemeDialogOpen: boolean;
+ openThemeDialog: () => void;
+ handleThemeSelect: (themeName: string) => void;
+}
+
+export const useThemeCommand = (): UseThemeCommandReturn => {
+ const [isThemeDialogOpen, setIsThemeDialogOpen] = useState(false);
+ const [, setForceRender] = useState(0);
+
+ const openThemeDialog = useCallback(() => {
+ setIsThemeDialogOpen(true);
+ }, []);
+
+ const handleThemeSelect = useCallback((themeName: string) => {
+ try {
+ themeManager.setActiveTheme(themeName);
+ setForceRender((v) => v + 1); // Trigger potential re-render
+ } catch (error) {
+ console.error(`Error setting theme: ${error}`);
+ } finally {
+ setIsThemeDialogOpen(false); // Close the dialog
+ }
+ }, []);
+
+ return {
+ isThemeDialogOpen,
+ openThemeDialog,
+ handleThemeSelect,
+ };
+};
diff --git a/packages/cli/src/ui/themes/atom-one-dark.ts b/packages/cli/src/ui/themes/atom-one-dark.ts
new file mode 100644
index 00000000..316b9048
--- /dev/null
+++ b/packages/cli/src/ui/themes/atom-one-dark.ts
@@ -0,0 +1,122 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Theme } from './theme.js';
+
+export const AtomOneDark: Theme = new Theme('Atom One Dark', {
+ hljs: {
+ display: 'block',
+ overflowX: 'auto',
+ padding: '0.5em',
+ color: '#abb2bf',
+ background: '#282c34',
+ },
+ 'hljs-comment': {
+ color: '#5c6370',
+ fontStyle: 'italic',
+ },
+ 'hljs-quote': {
+ color: '#5c6370',
+ fontStyle: 'italic',
+ },
+ 'hljs-doctag': {
+ color: '#c678dd',
+ },
+ 'hljs-keyword': {
+ color: '#c678dd',
+ },
+ 'hljs-formula': {
+ color: '#c678dd',
+ },
+ 'hljs-section': {
+ color: '#e06c75',
+ },
+ 'hljs-name': {
+ color: '#e06c75',
+ },
+ 'hljs-selector-tag': {
+ color: '#e06c75',
+ },
+ 'hljs-deletion': {
+ color: '#e06c75',
+ },
+ 'hljs-subst': {
+ color: '#e06c75',
+ },
+ 'hljs-literal': {
+ color: '#56b6c2',
+ },
+ 'hljs-string': {
+ color: '#98c379',
+ },
+ 'hljs-regexp': {
+ color: '#98c379',
+ },
+ 'hljs-addition': {
+ color: '#98c379',
+ },
+ 'hljs-attribute': {
+ color: '#98c379',
+ },
+ 'hljs-meta-string': {
+ color: '#98c379',
+ },
+ 'hljs-built_in': {
+ color: '#e6c07b',
+ },
+ 'hljs-class .hljs-title': {
+ color: '#e6c07b',
+ },
+ 'hljs-attr': {
+ color: '#d19a66',
+ },
+ 'hljs-variable': {
+ color: '#d19a66',
+ },
+ 'hljs-template-variable': {
+ color: '#d19a66',
+ },
+ 'hljs-type': {
+ color: '#d19a66',
+ },
+ 'hljs-selector-class': {
+ color: '#d19a66',
+ },
+ 'hljs-selector-attr': {
+ color: '#d19a66',
+ },
+ 'hljs-selector-pseudo': {
+ color: '#d19a66',
+ },
+ 'hljs-number': {
+ color: '#d19a66',
+ },
+ 'hljs-symbol': {
+ color: '#61aeee',
+ },
+ 'hljs-bullet': {
+ color: '#61aeee',
+ },
+ 'hljs-link': {
+ color: '#61aeee',
+ textDecoration: 'underline',
+ },
+ 'hljs-meta': {
+ color: '#61aeee',
+ },
+ 'hljs-selector-id': {
+ color: '#61aeee',
+ },
+ 'hljs-title': {
+ color: '#61aeee',
+ },
+ 'hljs-emphasis': {
+ fontStyle: 'italic',
+ },
+ 'hljs-strong': {
+ fontWeight: 'bold',
+ },
+});
diff --git a/packages/cli/src/ui/themes/dracula.ts b/packages/cli/src/ui/themes/dracula.ts
new file mode 100644
index 00000000..0b55435a
--- /dev/null
+++ b/packages/cli/src/ui/themes/dracula.ts
@@ -0,0 +1,99 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Theme } from './theme.js';
+
+export const Dracula: Theme = new Theme('Dracula', {
+ hljs: {
+ display: 'block',
+ overflowX: 'auto',
+ padding: '0.5em',
+ background: '#282a36',
+ color: '#f8f8f2',
+ },
+ 'hljs-keyword': {
+ color: '#8be9fd',
+ fontWeight: 'bold',
+ },
+ 'hljs-selector-tag': {
+ color: '#8be9fd',
+ fontWeight: 'bold',
+ },
+ 'hljs-literal': {
+ color: '#8be9fd',
+ fontWeight: 'bold',
+ },
+ 'hljs-section': {
+ color: '#8be9fd',
+ fontWeight: 'bold',
+ },
+ 'hljs-link': {
+ color: '#8be9fd',
+ },
+ 'hljs-function .hljs-keyword': {
+ color: '#ff79c6',
+ },
+ 'hljs-subst': {
+ color: '#f8f8f2',
+ },
+ 'hljs-string': {
+ color: '#f1fa8c',
+ },
+ 'hljs-title': {
+ color: '#f1fa8c',
+ fontWeight: 'bold',
+ },
+ 'hljs-name': {
+ color: '#f1fa8c',
+ fontWeight: 'bold',
+ },
+ 'hljs-type': {
+ color: '#f1fa8c',
+ fontWeight: 'bold',
+ },
+ 'hljs-attribute': {
+ color: '#f1fa8c',
+ },
+ 'hljs-symbol': {
+ color: '#f1fa8c',
+ },
+ 'hljs-bullet': {
+ color: '#f1fa8c',
+ },
+ 'hljs-addition': {
+ color: '#f1fa8c',
+ },
+ 'hljs-variable': {
+ color: '#f1fa8c',
+ },
+ 'hljs-template-tag': {
+ color: '#f1fa8c',
+ },
+ 'hljs-template-variable': {
+ color: '#f1fa8c',
+ },
+ 'hljs-comment': {
+ color: '#6272a4',
+ },
+ 'hljs-quote': {
+ color: '#6272a4',
+ },
+ 'hljs-deletion': {
+ color: '#6272a4',
+ },
+ 'hljs-meta': {
+ color: '#6272a4',
+ },
+ 'hljs-doctag': {
+ fontWeight: 'bold',
+ },
+ 'hljs-strong': {
+ fontWeight: 'bold',
+ },
+ 'hljs-emphasis': {
+ fontStyle: 'italic',
+ },
+});
diff --git a/packages/cli/src/ui/themes/github.ts b/packages/cli/src/ui/themes/github.ts
new file mode 100644
index 00000000..fb4080e6
--- /dev/null
+++ b/packages/cli/src/ui/themes/github.ts
@@ -0,0 +1,124 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Theme } from './theme.js';
+
+export const GitHub: Theme = new Theme('GitHub', {
+ hljs: {
+ display: 'block',
+ overflowX: 'auto',
+ padding: '0.5em',
+ color: '#333',
+ background: '#f8f8f8',
+ },
+ 'hljs-comment': {
+ color: '#998',
+ fontStyle: 'italic',
+ },
+ 'hljs-quote': {
+ color: '#998',
+ fontStyle: 'italic',
+ },
+ 'hljs-keyword': {
+ color: '#333',
+ fontWeight: 'bold',
+ },
+ 'hljs-selector-tag': {
+ color: '#333',
+ fontWeight: 'bold',
+ },
+ 'hljs-subst': {
+ color: '#333',
+ fontWeight: 'normal',
+ },
+ 'hljs-number': {
+ color: '#008080',
+ },
+ 'hljs-literal': {
+ color: '#008080',
+ },
+ 'hljs-variable': {
+ color: '#008080',
+ },
+ 'hljs-template-variable': {
+ color: '#008080',
+ },
+ 'hljs-tag .hljs-attr': {
+ color: '#008080',
+ },
+ 'hljs-string': {
+ color: '#d14',
+ },
+ 'hljs-doctag': {
+ color: '#d14',
+ },
+ 'hljs-title': {
+ color: '#900',
+ fontWeight: 'bold',
+ },
+ 'hljs-section': {
+ color: '#900',
+ fontWeight: 'bold',
+ },
+ 'hljs-selector-id': {
+ color: '#900',
+ fontWeight: 'bold',
+ },
+ 'hljs-type': {
+ color: '#458',
+ fontWeight: 'bold',
+ },
+ 'hljs-class .hljs-title': {
+ color: '#458',
+ fontWeight: 'bold',
+ },
+ 'hljs-tag': {
+ color: '#000080',
+ fontWeight: 'normal',
+ },
+ 'hljs-name': {
+ color: '#000080',
+ fontWeight: 'normal',
+ },
+ 'hljs-attribute': {
+ color: '#000080',
+ fontWeight: 'normal',
+ },
+ 'hljs-regexp': {
+ color: '#009926',
+ },
+ 'hljs-link': {
+ color: '#009926',
+ },
+ 'hljs-symbol': {
+ color: '#990073',
+ },
+ 'hljs-bullet': {
+ color: '#990073',
+ },
+ 'hljs-built_in': {
+ color: '#0086b3',
+ },
+ 'hljs-builtin-name': {
+ color: '#0086b3',
+ },
+ 'hljs-meta': {
+ color: '#999',
+ fontWeight: 'bold',
+ },
+ 'hljs-deletion': {
+ background: '#fdd',
+ },
+ 'hljs-addition': {
+ background: '#dfd',
+ },
+ 'hljs-emphasis': {
+ fontStyle: 'italic',
+ },
+ 'hljs-strong': {
+ fontWeight: 'bold',
+ },
+});
diff --git a/packages/cli/src/ui/themes/googlecode.ts b/packages/cli/src/ui/themes/googlecode.ts
new file mode 100644
index 00000000..ff01d8b6
--- /dev/null
+++ b/packages/cli/src/ui/themes/googlecode.ts
@@ -0,0 +1,121 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Theme } from './theme.js';
+
+export const GoogleCode: Theme = new Theme('Google Code', {
+ hljs: {
+ display: 'block',
+ overflowX: 'auto',
+ padding: '0.5em',
+ background: 'white',
+ color: 'black',
+ },
+ 'hljs-comment': {
+ color: '#800',
+ },
+ 'hljs-quote': {
+ color: '#800',
+ },
+ 'hljs-keyword': {
+ color: '#008',
+ },
+ 'hljs-selector-tag': {
+ color: '#008',
+ },
+ 'hljs-section': {
+ color: '#008',
+ },
+ 'hljs-title': {
+ color: '#606',
+ },
+ 'hljs-name': {
+ color: '#008',
+ },
+ 'hljs-variable': {
+ color: '#660',
+ },
+ 'hljs-template-variable': {
+ color: '#660',
+ },
+ 'hljs-string': {
+ color: '#080',
+ },
+ 'hljs-selector-attr': {
+ color: '#080',
+ },
+ 'hljs-selector-pseudo': {
+ color: '#080',
+ },
+ 'hljs-regexp': {
+ color: '#080',
+ },
+ 'hljs-literal': {
+ color: '#066',
+ },
+ 'hljs-symbol': {
+ color: '#066',
+ },
+ 'hljs-bullet': {
+ color: '#066',
+ },
+ 'hljs-meta': {
+ color: '#066',
+ },
+ 'hljs-number': {
+ color: '#066',
+ },
+ 'hljs-link': {
+ color: '#066',
+ },
+ 'hljs-doctag': {
+ color: '#606',
+ fontWeight: 'bold',
+ },
+ 'hljs-type': {
+ color: '#606',
+ },
+ 'hljs-attr': {
+ color: '#606',
+ },
+ 'hljs-built_in': {
+ color: '#606',
+ },
+ 'hljs-builtin-name': {
+ color: '#606',
+ },
+ 'hljs-params': {
+ color: '#606',
+ },
+ 'hljs-attribute': {
+ color: '#000',
+ },
+ 'hljs-subst': {
+ color: '#000',
+ },
+ 'hljs-formula': {
+ backgroundColor: '#eee',
+ fontStyle: 'italic',
+ },
+ 'hljs-selector-id': {
+ color: '#9B703F',
+ },
+ 'hljs-selector-class': {
+ color: '#9B703F',
+ },
+ 'hljs-addition': {
+ backgroundColor: '#baeeba',
+ },
+ 'hljs-deletion': {
+ backgroundColor: '#ffc8bd',
+ },
+ 'hljs-strong': {
+ fontWeight: 'bold',
+ },
+ 'hljs-emphasis': {
+ fontStyle: 'italic',
+ },
+});
diff --git a/packages/cli/src/ui/themes/theme-manager.ts b/packages/cli/src/ui/themes/theme-manager.ts
index e083575c..7e375574 100644
--- a/packages/cli/src/ui/themes/theme-manager.ts
+++ b/packages/cli/src/ui/themes/theme-manager.ts
@@ -4,20 +4,65 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import { AtomOneDark } from './atom-one-dark.js';
+import { Dracula } from './dracula.js';
+import { GitHub } from './github.js';
+import { GoogleCode } from './googlecode.js';
+import { VS } from './vs.js';
import { VS2015 } from './vs2015.js';
+import { XCode } from './xcode.js';
import { Theme } from './theme.js';
+export interface ThemeDisplay {
+ name: string;
+ active: boolean;
+}
+
class ThemeManager {
private static readonly DEFAULT_THEME: Theme = VS2015;
private readonly availableThemes: Theme[];
private activeTheme: Theme;
constructor() {
- this.availableThemes = [VS2015];
+ this.availableThemes = [
+ AtomOneDark,
+ Dracula,
+ VS,
+ VS2015,
+ GitHub,
+ GoogleCode,
+ XCode,
+ ];
this.activeTheme = ThemeManager.DEFAULT_THEME;
}
/**
+ * Returns a list of available theme names.
+ */
+ getAvailableThemes(): ThemeDisplay[] {
+ return this.availableThemes.map((theme) => ({
+ name: theme.name,
+ active: theme === this.activeTheme,
+ }));
+ }
+
+ /**
+ * Sets the active theme.
+ * @param themeName The name of the theme to activate.
+ */
+ setActiveTheme(themeName: string): void {
+ const foundTheme = this.availableThemes.find(
+ (theme) => theme.name === themeName,
+ );
+
+ if (foundTheme) {
+ this.activeTheme = foundTheme;
+ } else {
+ throw new Error(`Theme "${themeName}" not found.`);
+ }
+ }
+
+ /**
* Returns the currently active theme object.
*/
getActiveTheme(): Theme {
diff --git a/packages/cli/src/ui/themes/vs.ts b/packages/cli/src/ui/themes/vs.ts
new file mode 100644
index 00000000..a87e9e80
--- /dev/null
+++ b/packages/cli/src/ui/themes/vs.ts
@@ -0,0 +1,101 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Theme } from './theme.js';
+
+export const VS: Theme = new Theme('VS', {
+ hljs: {
+ display: 'block',
+ overflowX: 'auto',
+ padding: '0.5em',
+ background: 'white',
+ color: 'black',
+ },
+ 'hljs-comment': {
+ color: '#008000',
+ },
+ 'hljs-quote': {
+ color: '#008000',
+ },
+ 'hljs-variable': {
+ color: '#008000',
+ },
+ 'hljs-keyword': {
+ color: '#00f',
+ },
+ 'hljs-selector-tag': {
+ color: '#00f',
+ },
+ 'hljs-built_in': {
+ color: '#00f',
+ },
+ 'hljs-name': {
+ color: '#00f',
+ },
+ 'hljs-tag': {
+ color: '#00f',
+ },
+ 'hljs-string': {
+ color: '#a31515',
+ },
+ 'hljs-title': {
+ color: '#a31515',
+ },
+ 'hljs-section': {
+ color: '#a31515',
+ },
+ 'hljs-attribute': {
+ color: '#a31515',
+ },
+ 'hljs-literal': {
+ color: '#a31515',
+ },
+ 'hljs-template-tag': {
+ color: '#a31515',
+ },
+ 'hljs-template-variable': {
+ color: '#a31515',
+ },
+ 'hljs-type': {
+ color: '#a31515',
+ },
+ 'hljs-addition': {
+ color: '#a31515',
+ },
+ 'hljs-deletion': {
+ color: '#2b91af',
+ },
+ 'hljs-selector-attr': {
+ color: '#2b91af',
+ },
+ 'hljs-selector-pseudo': {
+ color: '#2b91af',
+ },
+ 'hljs-meta': {
+ color: '#2b91af',
+ },
+ 'hljs-doctag': {
+ color: '#808080',
+ },
+ 'hljs-attr': {
+ color: '#f00',
+ },
+ 'hljs-symbol': {
+ color: '#00b0e8',
+ },
+ 'hljs-bullet': {
+ color: '#00b0e8',
+ },
+ 'hljs-link': {
+ color: '#00b0e8',
+ },
+ 'hljs-emphasis': {
+ fontStyle: 'italic',
+ },
+ 'hljs-strong': {
+ fontWeight: 'bold',
+ },
+});
diff --git a/packages/cli/src/ui/themes/xcode.ts b/packages/cli/src/ui/themes/xcode.ts
new file mode 100644
index 00000000..ff2fb852
--- /dev/null
+++ b/packages/cli/src/ui/themes/xcode.ts
@@ -0,0 +1,129 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Theme } from './theme.js';
+
+export const XCode: Theme = new Theme('XCode', {
+ hljs: {
+ display: 'block',
+ overflowX: 'auto',
+ padding: '0.5em',
+ background: '#fff',
+ color: 'black',
+ },
+ 'xml .hljs-meta': {
+ color: '#c0c0c0',
+ },
+ 'hljs-comment': {
+ color: '#007400',
+ },
+ 'hljs-quote': {
+ color: '#007400',
+ },
+ 'hljs-tag': {
+ color: '#aa0d91',
+ },
+ 'hljs-attribute': {
+ color: '#aa0d91',
+ },
+ 'hljs-keyword': {
+ color: '#aa0d91',
+ },
+ 'hljs-selector-tag': {
+ color: '#aa0d91',
+ },
+ 'hljs-literal': {
+ color: '#aa0d91',
+ },
+ 'hljs-name': {
+ color: '#aa0d91',
+ },
+ 'hljs-variable': {
+ color: '#3F6E74',
+ },
+ 'hljs-template-variable': {
+ color: '#3F6E74',
+ },
+ 'hljs-code': {
+ color: '#c41a16',
+ },
+ 'hljs-string': {
+ color: '#c41a16',
+ },
+ 'hljs-meta-string': {
+ color: '#c41a16',
+ },
+ 'hljs-regexp': {
+ color: '#0E0EFF',
+ },
+ 'hljs-link': {
+ color: '#0E0EFF',
+ },
+ 'hljs-title': {
+ color: '#1c00cf',
+ },
+ 'hljs-symbol': {
+ color: '#1c00cf',
+ },
+ 'hljs-bullet': {
+ color: '#1c00cf',
+ },
+ 'hljs-number': {
+ color: '#1c00cf',
+ },
+ 'hljs-section': {
+ color: '#643820',
+ },
+ 'hljs-meta': {
+ color: '#643820',
+ },
+ 'hljs-class .hljs-title': {
+ color: '#5c2699',
+ },
+ 'hljs-type': {
+ color: '#5c2699',
+ },
+ 'hljs-built_in': {
+ color: '#5c2699',
+ },
+ 'hljs-builtin-name': {
+ color: '#5c2699',
+ },
+ 'hljs-params': {
+ color: '#5c2699',
+ },
+ 'hljs-attr': {
+ color: '#836C28',
+ },
+ 'hljs-subst': {
+ color: '#000',
+ },
+ 'hljs-formula': {
+ backgroundColor: '#eee',
+ fontStyle: 'italic',
+ },
+ 'hljs-addition': {
+ backgroundColor: '#baeeba',
+ },
+ 'hljs-deletion': {
+ backgroundColor: '#ffc8bd',
+ },
+ 'hljs-selector-id': {
+ color: '#9b703f',
+ },
+ 'hljs-selector-class': {
+ color: '#9b703f',
+ },
+ 'hljs-doctag': {
+ fontWeight: 'bold',
+ },
+ 'hljs-strong': {
+ fontWeight: 'bold',
+ },
+ 'hljs-emphasis': {
+ fontStyle: 'italic',
+ },
+});