Feature custom themes logic (#2639)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
Ali Al Jufairi
2025-07-20 16:51:18 +09:00
committed by GitHub
parent c0bfa388c5
commit 76b935d598
19 changed files with 1313 additions and 341 deletions

View File

@@ -15,7 +15,13 @@ import { DefaultLight } from './default-light.js';
import { DefaultDark } from './default.js';
import { ShadesOfPurple } from './shades-of-purple.js';
import { XCode } from './xcode.js';
import { Theme, ThemeType } from './theme.js';
import {
Theme,
ThemeType,
CustomTheme,
createCustomTheme,
validateCustomTheme,
} from './theme.js';
import { ANSI } from './ansi.js';
import { ANSILight } from './ansi-light.js';
import { NoColorTheme } from './no-color.js';
@@ -24,6 +30,7 @@ import process from 'node:process';
export interface ThemeDisplay {
name: string;
type: ThemeType;
isCustom?: boolean;
}
export const DEFAULT_THEME: Theme = DefaultDark;
@@ -31,6 +38,7 @@ export const DEFAULT_THEME: Theme = DefaultDark;
class ThemeManager {
private readonly availableThemes: Theme[];
private activeTheme: Theme;
private customThemes: Map<string, Theme> = new Map();
constructor() {
this.availableThemes = [
@@ -51,19 +59,121 @@ class ThemeManager {
this.activeTheme = DEFAULT_THEME;
}
/**
* Loads custom themes from settings.
* @param customThemesSettings Custom themes from settings.
*/
loadCustomThemes(customThemesSettings?: Record<string, CustomTheme>): void {
this.customThemes.clear();
if (!customThemesSettings) {
return;
}
for (const [name, customThemeConfig] of Object.entries(
customThemesSettings,
)) {
const validation = validateCustomTheme(customThemeConfig);
if (validation.isValid) {
try {
const theme = createCustomTheme(customThemeConfig);
this.customThemes.set(name, theme);
} catch (error) {
console.warn(`Failed to load custom theme "${name}":`, error);
}
} else {
console.warn(`Invalid custom theme "${name}": ${validation.error}`);
}
}
// If the current active theme is a custom theme, keep it if still valid
if (
this.activeTheme &&
this.activeTheme.type === 'custom' &&
this.customThemes.has(this.activeTheme.name)
) {
this.activeTheme = this.customThemes.get(this.activeTheme.name)!;
}
}
/**
* Sets the active theme.
* @param themeName The name of the theme to set as active.
* @returns True if the theme was successfully set, false otherwise.
*/
setActiveTheme(themeName: string | undefined): boolean {
const theme = this.findThemeByName(themeName);
if (!theme) {
return false;
}
this.activeTheme = theme;
return true;
}
/**
* Gets the currently active theme.
* @returns The active theme.
*/
getActiveTheme(): Theme {
if (process.env.NO_COLOR) {
return NoColorTheme;
}
// Ensure the active theme is always valid (fallback to default if not)
if (!this.activeTheme || !this.findThemeByName(this.activeTheme.name)) {
this.activeTheme = DEFAULT_THEME;
}
return this.activeTheme;
}
/**
* Gets a list of custom theme names.
* @returns Array of custom theme names.
*/
getCustomThemeNames(): string[] {
return Array.from(this.customThemes.keys());
}
/**
* Checks if a theme name is a custom theme.
* @param themeName The theme name to check.
* @returns True if the theme is custom.
*/
isCustomTheme(themeName: string): boolean {
return this.customThemes.has(themeName);
}
/**
* Returns a list of available theme names.
*/
getAvailableThemes(): ThemeDisplay[] {
const sortedThemes = [...this.availableThemes].sort((a, b) => {
const builtInThemes = this.availableThemes.map((theme) => ({
name: theme.name,
type: theme.type,
isCustom: false,
}));
const customThemes = Array.from(this.customThemes.values()).map(
(theme) => ({
name: theme.name,
type: theme.type,
isCustom: true,
}),
);
const allThemes = [...builtInThemes, ...customThemes];
const sortedThemes = allThemes.sort((a, b) => {
const typeOrder = (type: ThemeType): number => {
switch (type) {
case 'dark':
return 1;
case 'light':
return 2;
default:
case 'ansi':
return 3;
case 'custom':
return 4; // Custom themes at the end
default:
return 5;
}
};
@@ -74,50 +184,33 @@ class ThemeManager {
return a.name.localeCompare(b.name);
});
return sortedThemes.map((theme) => ({
name: theme.name,
type: theme.type,
}));
return sortedThemes;
}
/**
* Sets the active theme.
* @param themeName The name of the theme to activate.
* @returns True if the theme was successfully set, false otherwise.
* Gets a theme by name.
* @param themeName The name of the theme to get.
* @returns The theme if found, undefined otherwise.
*/
setActiveTheme(themeName: string | undefined): boolean {
const foundTheme = this.findThemeByName(themeName);
if (foundTheme) {
this.activeTheme = foundTheme;
return true;
} else {
// If themeName is undefined, it means we want to set the default theme.
// If findThemeByName returns undefined (e.g. default theme is also not found for some reason)
// then this will return false.
if (themeName === undefined) {
this.activeTheme = DEFAULT_THEME;
return true;
}
return false;
}
getTheme(themeName: string): Theme | undefined {
return this.findThemeByName(themeName);
}
findThemeByName(themeName: string | undefined): Theme | undefined {
if (!themeName) {
return DEFAULT_THEME;
}
return this.availableThemes.find((theme) => theme.name === themeName);
}
/**
* Returns the currently active theme object.
*/
getActiveTheme(): Theme {
if (process.env.NO_COLOR) {
return NoColorTheme;
// First check built-in themes
const builtInTheme = this.availableThemes.find(
(theme) => theme.name === themeName,
);
if (builtInTheme) {
return builtInTheme;
}
return this.activeTheme;
// Then check custom themes
return this.customThemes.get(themeName);
}
}