feat: Detect background color (#15132)

This commit is contained in:
Jacob Richman
2025-12-18 10:36:48 -08:00
committed by GitHub
parent 54466a3ea8
commit 322232e514
28 changed files with 1031 additions and 359 deletions

View File

@@ -9,6 +9,7 @@ import { useCallback, useState } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { themeManager, DEFAULT_THEME } from '../themes/theme-manager.js';
import { pickDefaultThemeName } from '../themes/theme.js';
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
import { DiffRenderer } from './messages/DiffRenderer.js';
import { colorizeCode } from '../utils/CodeColorizer.js';
@@ -22,6 +23,7 @@ import { useKeypress } from '../hooks/useKeypress.js';
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
import { ScopeSelector } from './shared/ScopeSelector.js';
import { useUIActions } from '../contexts/UIActionsContext.js';
import { useUIState } from '../contexts/UIStateContext.js';
interface ThemeDialogProps {
/** Callback function when a theme is selected */
@@ -38,6 +40,42 @@ interface ThemeDialogProps {
terminalWidth: number;
}
import {
getThemeTypeFromBackgroundColor,
resolveColor,
} from '../themes/color-utils.js';
function generateThemeItem(
name: string,
typeDisplay: string,
themeType: string,
themeBackground: string | undefined,
terminalBackgroundColor: string | undefined,
terminalThemeType: 'light' | 'dark' | undefined,
) {
const isCompatible =
themeType === 'custom' ||
terminalThemeType === undefined ||
themeType === 'ansi' ||
themeType === terminalThemeType;
const isBackgroundMatch =
terminalBackgroundColor &&
themeBackground &&
terminalBackgroundColor.toLowerCase() === themeBackground.toLowerCase();
return {
label: name,
value: name,
themeNameDisplay: name,
themeTypeDisplay: typeDisplay,
themeWarning: isCompatible ? '' : ' (Incompatible)',
themeMatch: isBackgroundMatch ? ' (Matches terminal)' : '',
key: name,
isCompatible,
};
}
export function ThemeDialog({
onSelect,
onCancel,
@@ -48,13 +86,27 @@ export function ThemeDialog({
}: ThemeDialogProps): React.JSX.Element {
const isAlternateBuffer = useAlternateBuffer();
const { refreshStatic } = useUIActions();
const { terminalBackgroundColor } = useUIState();
const [selectedScope, setSelectedScope] = useState<LoadableSettingScope>(
SettingScope.User,
);
// Track the currently highlighted theme name
const [highlightedThemeName, setHighlightedThemeName] = useState<string>(
settings.merged.ui?.theme || DEFAULT_THEME.name,
() => {
// If a theme is already set, use it.
if (settings.merged.ui?.theme) {
return settings.merged.ui.theme;
}
// Otherwise, try to pick a theme that matches the terminal background.
return pickDefaultThemeName(
terminalBackgroundColor,
themeManager.getAllThemes(),
DEFAULT_THEME.name,
'Default Light',
);
},
);
// Generate theme items filtered by selected scope
@@ -67,23 +119,49 @@ export function ThemeDialog({
.filter((theme) => theme.type !== 'custom');
const customThemeNames = Object.keys(customThemes);
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
const terminalThemeType = getThemeTypeFromBackgroundColor(
terminalBackgroundColor,
);
// Generate theme items
const themeItems = [
...builtInThemes.map((theme) => ({
label: theme.name,
value: theme.name,
themeNameDisplay: theme.name,
themeTypeDisplay: capitalize(theme.type),
key: theme.name,
})),
...customThemeNames.map((name) => ({
label: name,
value: name,
themeNameDisplay: name,
themeTypeDisplay: 'Custom',
key: name,
})),
];
...builtInThemes.map((theme) => {
const fullTheme = themeManager.getTheme(theme.name);
const themeBackground = fullTheme
? resolveColor(fullTheme.colors.Background)
: undefined;
return generateThemeItem(
theme.name,
capitalize(theme.type),
theme.type,
themeBackground,
terminalBackgroundColor,
terminalThemeType,
);
}),
...customThemeNames.map((name) => {
const themeConfig = customThemes[name];
const bg = themeConfig.background?.primary ?? themeConfig.Background;
const themeBackground = bg ? resolveColor(bg) : undefined;
return generateThemeItem(
name,
'Custom',
'custom',
themeBackground,
terminalBackgroundColor,
terminalThemeType,
);
}),
].sort((a, b) => {
// Show compatible themes first
if (a.isCompatible && !b.isCompatible) return -1;
if (!a.isCompatible && b.isCompatible) return 1;
// Then sort by name
return a.label.localeCompare(b.label);
});
// Find the index of the selected theme, but only if it exists in the list
const initialThemeIndex = themeItems.findIndex(
@@ -225,6 +303,40 @@ export function ThemeDialog({
maxItemsToShow={12}
showScrollArrows={true}
showNumbers={mode === 'theme'}
renderItem={(item, { titleColor }) => {
// We know item has themeWarning because we put it there, but we need to cast or access safely
const itemWithExtras = item as typeof item & {
themeWarning?: string;
themeMatch?: string;
};
if (item.themeNameDisplay && item.themeTypeDisplay) {
return (
<Text color={titleColor} wrap="truncate" key={item.key}>
{item.themeNameDisplay}{' '}
<Text color={theme.text.secondary}>
{item.themeTypeDisplay}
</Text>
{itemWithExtras.themeMatch && (
<Text color={theme.status.success}>
{itemWithExtras.themeMatch}
</Text>
)}
{itemWithExtras.themeWarning && (
<Text color={theme.status.warning}>
{itemWithExtras.themeWarning}
</Text>
)}
</Text>
);
}
// Regular label display
return (
<Text color={titleColor} wrap="truncate">
{item.label}
</Text>
);
}}
/>
</Box>
@@ -239,6 +351,7 @@ export function ThemeDialog({
themeManager.getTheme(
highlightedThemeName || DEFAULT_THEME.name,
) || DEFAULT_THEME;
return (
<Box
borderStyle="single"