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

@@ -11,6 +11,7 @@ import {
interpolateColor,
CSS_NAME_TO_HEX_MAP,
INK_SUPPORTED_NAMES,
getThemeTypeFromBackgroundColor,
} from './color-utils.js';
describe('Color Utils', () => {
@@ -255,4 +256,27 @@ describe('Color Utils', () => {
expect(interpolateColor('#ffffff', '', 1)).toBe('');
});
});
describe('getThemeTypeFromBackgroundColor', () => {
it('should return light for light backgrounds', () => {
expect(getThemeTypeFromBackgroundColor('#ffffff')).toBe('light');
expect(getThemeTypeFromBackgroundColor('#f0f0f0')).toBe('light');
expect(getThemeTypeFromBackgroundColor('#cccccc')).toBe('light');
});
it('should return dark for dark backgrounds', () => {
expect(getThemeTypeFromBackgroundColor('#000000')).toBe('dark');
expect(getThemeTypeFromBackgroundColor('#1a1a1a')).toBe('dark');
expect(getThemeTypeFromBackgroundColor('#333333')).toBe('dark');
});
it('should return undefined for undefined background', () => {
expect(getThemeTypeFromBackgroundColor(undefined)).toBeUndefined();
});
it('should handle colors without # prefix', () => {
expect(getThemeTypeFromBackgroundColor('ffffff')).toBe('light');
expect(getThemeTypeFromBackgroundColor('000000')).toBe('dark');
});
});
});

View File

@@ -251,3 +251,22 @@ export function interpolateColor(
const color = gradient.rgbAt(factor);
return color.toHexString();
}
export function getThemeTypeFromBackgroundColor(
backgroundColor: string | undefined,
): 'light' | 'dark' | undefined {
if (!backgroundColor) {
return undefined;
}
// Parse hex color
const hex = backgroundColor.replace(/^#/, '');
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
// Calculate luminance
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
return luminance > 128 ? 'light' : 'dark';
}

View File

@@ -14,7 +14,7 @@ import { interpolateColor } from './color-utils.js';
const shadesOfPurpleColors: ColorsTheme = {
type: 'dark',
// Required colors for ColorsTheme interface
Background: '#2d2b57', // Main background
Background: '#1e1e3f', // Main background in the VSCode terminal.
Foreground: '#e3dfff', // Default text color (hljs, hljs-subst)
LightBlue: '#847ace', // Light blue/purple accent
AccentBlue: '#a599e9', // Borders, secondary blue

View File

@@ -228,6 +228,14 @@ class ThemeManager {
return this.findThemeByName(themeName);
}
/**
* Gets all available themes.
* @returns A list of all available themes.
*/
getAllThemes(): Theme[] {
return [...this.availableThemes, ...Array.from(this.customThemes.values())];
}
private isPath(themeName: string): boolean {
return (
themeName.endsWith('.json') ||

View File

@@ -168,3 +168,42 @@ describe('themeManager.loadCustomThemes', () => {
expect(result.name).toBe(legacyTheme.name);
});
});
describe('pickDefaultThemeName', () => {
const { pickDefaultThemeName } = themeModule;
const mockThemes = [
{ name: 'Dark Theme', type: 'dark', colors: { Background: '#000000' } },
{ name: 'Light Theme', type: 'light', colors: { Background: '#ffffff' } },
{ name: 'Blue Theme', type: 'dark', colors: { Background: '#0000ff' } },
] as unknown as themeModule.Theme[];
it('should return exact match if found', () => {
expect(
pickDefaultThemeName('#0000ff', mockThemes, 'Dark Theme', 'Light Theme'),
).toBe('Blue Theme');
});
it('should return exact match (case insensitive)', () => {
expect(
pickDefaultThemeName('#FFFFFF', mockThemes, 'Dark Theme', 'Light Theme'),
).toBe('Light Theme');
});
it('should return default light theme for light background if no match', () => {
expect(
pickDefaultThemeName('#eeeeee', mockThemes, 'Dark Theme', 'Light Theme'),
).toBe('Light Theme');
});
it('should return default dark theme for dark background if no match', () => {
expect(
pickDefaultThemeName('#111111', mockThemes, 'Dark Theme', 'Light Theme'),
).toBe('Dark Theme');
});
it('should return default dark theme if background is undefined', () => {
expect(
pickDefaultThemeName(undefined, mockThemes, 'Dark Theme', 'Light Theme'),
).toBe('Dark Theme');
});
});

View File

@@ -6,7 +6,11 @@
import type { CSSProperties } from 'react';
import type { SemanticColors } from './semantic-tokens.js';
import { resolveColor, interpolateColor } from './color-utils.js';
import {
resolveColor,
interpolateColor,
getThemeTypeFromBackgroundColor,
} from './color-utils.js';
export type ThemeType = 'light' | 'dark' | 'ansi' | 'custom';
@@ -499,3 +503,40 @@ function isValidThemeName(name: string): boolean {
// Theme name should be non-empty and not contain invalid characters
return name.trim().length > 0 && name.trim().length <= 50;
}
/**
* Picks a default theme name based on terminal background color.
* It first tries to find a theme with an exact background color match.
* If no match is found, it falls back to a light or dark theme based on the
* luminance of the background color.
* @param terminalBackground The hex color string of the terminal background.
* @param availableThemes A list of available themes to search through.
* @param defaultDarkThemeName The name of the fallback dark theme.
* @param defaultLightThemeName The name of the fallback light theme.
* @returns The name of the chosen theme.
*/
export function pickDefaultThemeName(
terminalBackground: string | undefined,
availableThemes: readonly Theme[],
defaultDarkThemeName: string,
defaultLightThemeName: string,
): string {
if (terminalBackground) {
const lowerTerminalBackground = terminalBackground.toLowerCase();
for (const theme of availableThemes) {
if (!theme.colors.Background) continue;
// resolveColor can return undefined
const themeBg = resolveColor(theme.colors.Background)?.toLowerCase();
if (themeBg === lowerTerminalBackground) {
return theme.name;
}
}
}
const themeType = getThemeTypeFromBackgroundColor(terminalBackground);
if (themeType === 'light') {
return defaultLightThemeName;
}
return defaultDarkThemeName;
}