ui(polish) blend background color with theme (#18802)

This commit is contained in:
Jacob Richman
2026-02-12 11:56:07 -08:00
committed by GitHub
parent db00c5abf3
commit 207ac6f2dc
20 changed files with 432 additions and 240 deletions

View File

@@ -16,7 +16,11 @@ import type { UIState } from '../contexts/UIStateContext.js';
vi.mock('../themes/theme-manager.js', () => ({
themeManager: {
getActiveTheme: vi.fn(),
setTerminalBackground: vi.fn(),
getAllThemes: vi.fn(() => []),
setActiveTheme: vi.fn(),
},
DEFAULT_THEME: { name: 'Default' },
}));
vi.mock('../themes/holiday.js', () => ({

View File

@@ -15,6 +15,7 @@ const mockWrite = vi.fn();
const mockSubscribe = vi.fn();
const mockUnsubscribe = vi.fn();
const mockHandleThemeSelect = vi.fn();
const mockQueryTerminalBackground = vi.fn();
vi.mock('ink', async () => ({
useStdout: () => ({
@@ -28,6 +29,7 @@ vi.mock('../contexts/TerminalContext.js', () => ({
useTerminalContext: () => ({
subscribe: mockSubscribe,
unsubscribe: mockUnsubscribe,
queryTerminalBackground: mockQueryTerminalBackground,
}),
}));
@@ -52,6 +54,7 @@ vi.mock('../themes/theme-manager.js', async () => {
themeManager: {
isDefaultTheme: (name: string) =>
name === 'default' || name === 'default-light',
setTerminalBackground: vi.fn(),
},
DEFAULT_THEME: { name: 'default' },
};
@@ -78,6 +81,7 @@ describe('useTerminalTheme', () => {
mockSubscribe.mockClear();
mockUnsubscribe.mockClear();
mockHandleThemeSelect.mockClear();
mockQueryTerminalBackground.mockClear();
// Reset any settings modifications
mockSettings.merged.ui.autoThemeSwitching = true;
mockSettings.merged.ui.theme = 'default';
@@ -89,37 +93,37 @@ describe('useTerminalTheme', () => {
});
it('should subscribe to terminal background events on mount', () => {
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config));
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config, vi.fn()));
expect(mockSubscribe).toHaveBeenCalled();
});
it('should unsubscribe on unmount', () => {
const { unmount } = renderHook(() =>
useTerminalTheme(mockHandleThemeSelect, config),
useTerminalTheme(mockHandleThemeSelect, config, vi.fn()),
);
unmount();
expect(mockUnsubscribe).toHaveBeenCalled();
});
it('should poll for terminal background', () => {
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config));
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config, vi.fn()));
// Fast-forward time (1 minute)
vi.advanceTimersByTime(60000);
expect(mockWrite).toHaveBeenCalledWith('\x1b]11;?\x1b\\');
expect(mockQueryTerminalBackground).toHaveBeenCalled();
});
it('should not poll if terminal background is undefined at startup', () => {
config.getTerminalBackground = vi.fn().mockReturnValue(undefined);
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config));
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config, vi.fn()));
// Poll should not happen
vi.advanceTimersByTime(60000);
expect(mockWrite).not.toHaveBeenCalled();
expect(mockQueryTerminalBackground).not.toHaveBeenCalled();
});
it('should switch to light theme when background is light', () => {
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config));
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config, vi.fn()));
const handler = mockSubscribe.mock.calls[0][0];
@@ -137,7 +141,7 @@ describe('useTerminalTheme', () => {
// Start with light theme
mockSettings.merged.ui.theme = 'default-light';
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config));
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config, vi.fn()));
const handler = mockSubscribe.mock.calls[0][0];
@@ -156,11 +160,11 @@ describe('useTerminalTheme', () => {
it('should not switch theme if autoThemeSwitching is disabled', () => {
mockSettings.merged.ui.autoThemeSwitching = false;
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config));
renderHook(() => useTerminalTheme(mockHandleThemeSelect, config, vi.fn()));
// Poll should not happen
vi.advanceTimersByTime(60000);
expect(mockWrite).not.toHaveBeenCalled();
expect(mockQueryTerminalBackground).not.toHaveBeenCalled();
mockSettings.merged.ui.autoThemeSwitching = true;
});

View File

@@ -5,7 +5,6 @@
*/
import { useEffect } from 'react';
import { useStdout } from 'ink';
import {
getLuminance,
parseColor,
@@ -22,10 +21,11 @@ import type { UIActions } from '../contexts/UIActionsContext.js';
export function useTerminalTheme(
handleThemeSelect: UIActions['handleThemeSelect'],
config: Config,
refreshStatic: () => void,
) {
const { stdout } = useStdout();
const settings = useSettings();
const { subscribe, unsubscribe } = useTerminalContext();
const { subscribe, unsubscribe, queryTerminalBackground } =
useTerminalContext();
useEffect(() => {
if (settings.merged.ui.autoThemeSwitching === false) {
@@ -44,7 +44,7 @@ export function useTerminalTheme(
return;
}
stdout.write('\x1b]11;?\x1b\\');
void queryTerminalBackground();
}, settings.merged.ui.terminalBackgroundPollingInterval * 1000);
const handleTerminalBackground = (colorStr: string) => {
@@ -58,6 +58,8 @@ export function useTerminalTheme(
const hexColor = parseColor(match[1], match[2], match[3]);
const luminance = getLuminance(hexColor);
config.setTerminalBackground(hexColor);
themeManager.setTerminalBackground(hexColor);
refreshStatic();
const currentThemeName = settings.merged.ui.theme;
@@ -69,7 +71,7 @@ export function useTerminalTheme(
);
if (newTheme) {
handleThemeSelect(newTheme, SettingScope.User);
void handleThemeSelect(newTheme, SettingScope.User);
}
};
@@ -83,10 +85,11 @@ export function useTerminalTheme(
settings.merged.ui.theme,
settings.merged.ui.autoThemeSwitching,
settings.merged.ui.terminalBackgroundPollingInterval,
stdout,
config,
handleThemeSelect,
subscribe,
unsubscribe,
queryTerminalBackground,
refreshStatic,
]);
}

View File

@@ -13,12 +13,16 @@ import type {
import { MessageType } from '../types.js';
import process from 'node:process';
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
import { useTerminalContext } from '../contexts/TerminalContext.js';
interface UseThemeCommandReturn {
isThemeDialogOpen: boolean;
openThemeDialog: () => void;
closeThemeDialog: () => void;
handleThemeSelect: (themeName: string, scope: LoadableSettingScope) => void;
handleThemeSelect: (
themeName: string,
scope: LoadableSettingScope,
) => Promise<void>;
handleThemeHighlight: (themeName: string | undefined) => void;
}
@@ -30,8 +34,9 @@ export const useThemeCommand = (
): UseThemeCommandReturn => {
const [isThemeDialogOpen, setIsThemeDialogOpen] =
useState(!!initialThemeError);
const { queryTerminalBackground } = useTerminalContext();
const openThemeDialog = useCallback(() => {
const openThemeDialog = useCallback(async () => {
if (process.env['NO_COLOR']) {
addItem(
{
@@ -42,8 +47,14 @@ export const useThemeCommand = (
);
return;
}
// Ensure we have an up to date terminal background color when opening the
// theme dialog as the user may have just changed it before opening the
// dialog.
await queryTerminalBackground();
setIsThemeDialogOpen(true);
}, [addItem]);
}, [addItem, queryTerminalBackground]);
const applyTheme = useCallback(
(themeName: string | undefined) => {
@@ -72,7 +83,7 @@ export const useThemeCommand = (
}, [applyTheme, loadedSettings]);
const handleThemeSelect = useCallback(
(themeName: string, scope: LoadableSettingScope) => {
async (themeName: string, scope: LoadableSettingScope) => {
try {
const mergedCustomThemes = {
...(loadedSettings.user.settings.ui?.customThemes || {}),