feat(cli): respect the NO_COLOR env variable (#772)

This commit is contained in:
Jacob Richman 2025-06-06 07:55:28 -07:00 committed by GitHub
parent c80ff146d2
commit 4262f5b0de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 177 additions and 25 deletions

View File

@ -15,6 +15,7 @@ import {
AccessibilitySettings,
} from '@gemini-code/core';
import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js';
import process from 'node:process';
// Define a more complete mock server config based on actual Config
interface MockServerConfig {
@ -345,20 +346,52 @@ describe('App UI', () => {
expect(lastFrame()).toContain('Using 2 MCP servers');
});
it('should display theme dialog if no theme is set in settings', async () => {
mockSettings = createMockSettings({});
mockConfig.getDebugMode.mockReturnValue(false);
mockConfig.getShowMemoryUsage.mockReturnValue(false);
describe('when no theme is set', () => {
let originalNoColor: string | undefined;
const { lastFrame, unmount } = render(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettings}
cliVersion="1.0.0"
/>,
);
currentUnmount = unmount;
beforeEach(() => {
originalNoColor = process.env.NO_COLOR;
// Ensure no theme is set for these tests
mockSettings = createMockSettings({});
mockConfig.getDebugMode.mockReturnValue(false);
mockConfig.getShowMemoryUsage.mockReturnValue(false);
});
expect(lastFrame()).toContain('Select Theme');
afterEach(() => {
process.env.NO_COLOR = originalNoColor;
});
it('should display theme dialog if NO_COLOR is not set', async () => {
delete process.env.NO_COLOR;
const { lastFrame, unmount } = render(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettings}
cliVersion="1.0.0"
/>,
);
currentUnmount = unmount;
expect(lastFrame()).toContain('Select Theme');
});
it('should display a message if NO_COLOR is set', async () => {
process.env.NO_COLOR = 'true';
const { lastFrame, unmount } = render(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettings}
cliVersion="1.0.0"
/>,
);
currentUnmount = unmount;
expect(lastFrame()).toContain(
'Theme configuration unavailable due to NO_COLOR env variable.',
);
expect(lastFrame()).not.toContain('Select Theme');
});
});
});

View File

@ -136,7 +136,7 @@ export const App = ({
openThemeDialog,
handleThemeSelect,
handleThemeHighlight,
} = useThemeCommand(settings, setThemeError);
} = useThemeCommand(settings, setThemeError, addItem);
useEffect(() => {
if (config) {

View File

@ -7,6 +7,8 @@
import { useState, useCallback, useEffect } from 'react';
import { themeManager } from '../themes/theme-manager.js';
import { LoadedSettings, SettingScope } from '../../config/settings.js'; // Import LoadedSettings, AppSettings, MergedSetting
import { type HistoryItem, MessageType } from '../types.js';
import process from 'node:process';
interface UseThemeCommandReturn {
isThemeDialogOpen: boolean;
@ -21,34 +23,55 @@ interface UseThemeCommandReturn {
export const useThemeCommand = (
loadedSettings: LoadedSettings,
setThemeError: (error: string | null) => void,
addItem: (item: Omit<HistoryItem, 'id'>, timestamp: number) => void,
): UseThemeCommandReturn => {
// Determine the effective theme
const effectiveTheme = loadedSettings.merged.theme;
// Initial state: Open dialog if no theme is set in either user or workspace settings
const [isThemeDialogOpen, setIsThemeDialogOpen] = useState(
effectiveTheme === undefined, // Open dialog if no theme is set initially
effectiveTheme === undefined && !process.env.NO_COLOR,
);
// TODO: refactor how theme's are accessed to avoid requiring a forced render.
const [, setForceRender] = useState(0);
// Apply initial theme on component mount
useEffect(() => {
// Only try to set a theme if one is actually defined.
// If effectiveTheme was undefined, the dialog is already open due to useState above.
if (effectiveTheme !== undefined) {
if (!themeManager.setActiveTheme(effectiveTheme)) {
setIsThemeDialogOpen(true);
setThemeError(`Theme "${effectiveTheme}" not found.`);
} else {
setThemeError(null); // Clear any previous theme error on success
if (effectiveTheme === undefined) {
if (process.env.NO_COLOR) {
addItem(
{
type: MessageType.INFO,
text: 'Theme configuration unavailable due to NO_COLOR env variable.',
},
Date.now(),
);
}
// If no theme is set and NO_COLOR is not set, the dialog is already open.
return;
}
}, [effectiveTheme, setThemeError]); // Re-run if effectiveTheme or setThemeError changes
if (!themeManager.setActiveTheme(effectiveTheme)) {
setIsThemeDialogOpen(true);
setThemeError(`Theme "${effectiveTheme}" not found.`);
} else {
setThemeError(null);
}
}, [effectiveTheme, setThemeError, addItem]); // Re-run if effectiveTheme or setThemeError changes
const openThemeDialog = useCallback(() => {
if (process.env.NO_COLOR) {
addItem(
{
type: MessageType.INFO,
text: 'Theme configuration unavailable due to NO_COLOR env variable.',
},
Date.now(),
);
return;
}
setIsThemeDialogOpen(true);
}, []);
}, [addItem]);
const applyTheme = useCallback(
(themeName: string | undefined) => {

View File

@ -0,0 +1,91 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { Theme, ColorsTheme } from './theme.js';
const noColorColorsTheme: ColorsTheme = {
type: 'ansi',
Background: '',
Foreground: '',
LightBlue: '',
AccentBlue: '',
AccentPurple: '',
AccentCyan: '',
AccentGreen: '',
AccentYellow: '',
AccentRed: '',
SubtleComment: '',
Gray: '',
};
export const NoColorTheme: Theme = new Theme(
'No Color',
'dark',
{
hljs: {
display: 'block',
overflowX: 'auto',
padding: '0.5em',
},
'hljs-keyword': {},
'hljs-literal': {},
'hljs-symbol': {},
'hljs-name': {},
'hljs-link': {
textDecoration: 'underline',
},
'hljs-built_in': {},
'hljs-type': {},
'hljs-number': {},
'hljs-class': {},
'hljs-string': {},
'hljs-meta-string': {},
'hljs-regexp': {},
'hljs-template-tag': {},
'hljs-subst': {},
'hljs-function': {},
'hljs-title': {},
'hljs-params': {},
'hljs-formula': {},
'hljs-comment': {
fontStyle: 'italic',
},
'hljs-quote': {
fontStyle: 'italic',
},
'hljs-doctag': {},
'hljs-meta': {},
'hljs-meta-keyword': {},
'hljs-tag': {},
'hljs-variable': {},
'hljs-template-variable': {},
'hljs-attr': {},
'hljs-attribute': {},
'hljs-builtin-name': {},
'hljs-section': {},
'hljs-emphasis': {
fontStyle: 'italic',
},
'hljs-strong': {
fontWeight: 'bold',
},
'hljs-bullet': {},
'hljs-selector-tag': {},
'hljs-selector-id': {},
'hljs-selector-class': {},
'hljs-selector-attr': {},
'hljs-selector-pseudo': {},
'hljs-addition': {
display: 'inline-block',
width: '100%',
},
'hljs-deletion': {
display: 'inline-block',
width: '100%',
},
},
noColorColorsTheme,
);

View File

@ -17,6 +17,8 @@ import { XCode } from './xcode.js';
import { Theme, ThemeType } from './theme.js';
import { ANSI } from './ansi.js';
import { ANSILight } from './ansi-light.js';
import { NoColorTheme } from './no-color.js';
import process from 'node:process';
export interface ThemeDisplay {
name: string;
@ -110,6 +112,9 @@ class ThemeManager {
* Returns the currently active theme object.
*/
getActiveTheme(): Theme {
if (process.env.NO_COLOR) {
return NoColorTheme;
}
return this.activeTheme;
}
}