Save settings to ~/.gemini/settings.json and optionally /your/workspace/.gemini/settings.json (#237)
This commit is contained in:
parent
a18eea8c23
commit
7e8f379dfb
|
@ -0,0 +1,132 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { homedir } from 'os';
|
||||||
|
import { Config } from '@gemini-code/server';
|
||||||
|
|
||||||
|
const SETTINGS_DIRECTORY_NAME = '.gemini';
|
||||||
|
const USER_SETTINGS_DIR = path.join(homedir(), SETTINGS_DIRECTORY_NAME);
|
||||||
|
const USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, 'settings.json');
|
||||||
|
|
||||||
|
export enum SettingScope {
|
||||||
|
User = 'User',
|
||||||
|
Workspace = 'Workspace',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Settings {
|
||||||
|
theme?: string;
|
||||||
|
// Add other settings here.
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettingsFile {
|
||||||
|
settings: Settings;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
export class LoadedSettings {
|
||||||
|
constructor(user: SettingsFile, workspace: SettingsFile) {
|
||||||
|
this.user = user;
|
||||||
|
this.workspace = workspace;
|
||||||
|
this.merged = this.computeMergedSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly user: SettingsFile;
|
||||||
|
readonly workspace: SettingsFile;
|
||||||
|
|
||||||
|
private merged: Settings;
|
||||||
|
|
||||||
|
getMerged(): Settings {
|
||||||
|
return this.merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeMergedSettings(): Settings {
|
||||||
|
return {
|
||||||
|
...this.user.settings,
|
||||||
|
...this.workspace.settings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
forScope(scope: SettingScope): SettingsFile {
|
||||||
|
switch (scope) {
|
||||||
|
case SettingScope.User:
|
||||||
|
return this.user;
|
||||||
|
case SettingScope.Workspace:
|
||||||
|
return this.workspace;
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid scope: ${scope}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(
|
||||||
|
scope: SettingScope,
|
||||||
|
key: keyof Settings,
|
||||||
|
value: string | undefined,
|
||||||
|
): void {
|
||||||
|
const settingsFile = this.forScope(scope);
|
||||||
|
settingsFile.settings[key] = value;
|
||||||
|
this.merged = this.computeMergedSettings();
|
||||||
|
saveSettings(settingsFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads settings from user and project configuration files.
|
||||||
|
* Project settings override user settings.
|
||||||
|
*/
|
||||||
|
export function loadSettings(config: Config): LoadedSettings {
|
||||||
|
let userSettings: Settings = {};
|
||||||
|
let workspaceSettings = {};
|
||||||
|
|
||||||
|
// Load user settings
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(USER_SETTINGS_PATH)) {
|
||||||
|
const userContent = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8');
|
||||||
|
userSettings = JSON.parse(userContent);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading user settings file:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceSettingsPath = path.join(
|
||||||
|
config.getTargetDir(),
|
||||||
|
SETTINGS_DIRECTORY_NAME,
|
||||||
|
'settings.json',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load workspace settings
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(workspaceSettingsPath)) {
|
||||||
|
const projectContent = fs.readFileSync(workspaceSettingsPath, 'utf-8');
|
||||||
|
workspaceSettings = JSON.parse(projectContent);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading workspace settings file:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LoadedSettings(
|
||||||
|
{ path: USER_SETTINGS_PATH, settings: userSettings },
|
||||||
|
{ path: workspaceSettingsPath, settings: workspaceSettings },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveSettings(settingsFile: SettingsFile): void {
|
||||||
|
try {
|
||||||
|
// Ensure the directory exists
|
||||||
|
const dirPath = path.dirname(settingsFile.path);
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
settingsFile.path,
|
||||||
|
JSON.stringify(settingsFile.settings, null, 2),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving user settings file:', error);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,12 +14,20 @@ import { readPackageUp } from 'read-package-up';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import { sandbox_command, start_sandbox } from './utils/sandbox.js';
|
import { sandbox_command, start_sandbox } from './utils/sandbox.js';
|
||||||
|
import { loadSettings } from './config/settings.js';
|
||||||
|
import { themeManager } from './ui/themes/theme-manager.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const config = await loadCliConfig();
|
const config = await loadCliConfig();
|
||||||
|
const settings = loadSettings(config);
|
||||||
|
const theme = settings.getMerged().theme;
|
||||||
|
if (theme) {
|
||||||
|
themeManager.setActiveTheme(theme);
|
||||||
|
}
|
||||||
|
|
||||||
let input = config.getQuestion();
|
let input = config.getQuestion();
|
||||||
|
|
||||||
// hop into sandbox if we are outside and sandboxing is enabled
|
// hop into sandbox if we are outside and sandboxing is enabled
|
||||||
|
@ -41,6 +49,7 @@ async function main() {
|
||||||
render(
|
render(
|
||||||
React.createElement(App, {
|
React.createElement(App, {
|
||||||
config,
|
config,
|
||||||
|
settings,
|
||||||
cliVersion,
|
cliVersion,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { useStartupWarnings } from './hooks/useAppEffects.js';
|
||||||
import { shortenPath, type Config } from '@gemini-code/server';
|
import { shortenPath, type Config } from '@gemini-code/server';
|
||||||
import { Colors } from './colors.js';
|
import { Colors } from './colors.js';
|
||||||
import { Intro } from './components/Intro.js';
|
import { Intro } from './components/Intro.js';
|
||||||
|
import { LoadedSettings } from '../config/settings.js';
|
||||||
import { Tips } from './components/Tips.js';
|
import { Tips } from './components/Tips.js';
|
||||||
import { ConsoleOutput } from './components/ConsolePatcher.js';
|
import { ConsoleOutput } from './components/ConsolePatcher.js';
|
||||||
import { HistoryItemDisplay } from './components/HistoryItemDisplay.js';
|
import { HistoryItemDisplay } from './components/HistoryItemDisplay.js';
|
||||||
|
@ -29,10 +30,11 @@ import { isAtCommand } from './utils/commandUtils.js';
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
config: Config;
|
config: Config;
|
||||||
|
settings: LoadedSettings;
|
||||||
cliVersion: string;
|
cliVersion: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const App = ({ config, cliVersion }: AppProps) => {
|
export const App = ({ config, settings, cliVersion }: AppProps) => {
|
||||||
const [history, setHistory] = useState<HistoryItem[]>([]);
|
const [history, setHistory] = useState<HistoryItem[]>([]);
|
||||||
const [startupWarnings, setStartupWarnings] = useState<string[]>([]);
|
const [startupWarnings, setStartupWarnings] = useState<string[]>([]);
|
||||||
const {
|
const {
|
||||||
|
@ -40,7 +42,7 @@ export const App = ({ config, cliVersion }: AppProps) => {
|
||||||
openThemeDialog,
|
openThemeDialog,
|
||||||
handleThemeSelect,
|
handleThemeSelect,
|
||||||
handleThemeHighlight,
|
handleThemeHighlight,
|
||||||
} = useThemeCommand();
|
} = useThemeCommand(settings);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
streamingState,
|
streamingState,
|
||||||
|
@ -176,6 +178,7 @@ export const App = ({ config, cliVersion }: AppProps) => {
|
||||||
<ThemeDialog
|
<ThemeDialog
|
||||||
onSelect={handleThemeSelect}
|
onSelect={handleThemeSelect}
|
||||||
onHighlight={handleThemeHighlight}
|
onHighlight={handleThemeHighlight}
|
||||||
|
settings={settings}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -4,33 +4,87 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text, useInput } from 'ink';
|
||||||
import { Colors } from '../colors.js';
|
import { Colors } from '../colors.js';
|
||||||
import { themeManager } from '../themes/theme-manager.js';
|
import { themeManager, DEFAULT_THEME } from '../themes/theme-manager.js';
|
||||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||||
import { DiffRenderer } from './messages/DiffRenderer.js';
|
import { DiffRenderer } from './messages/DiffRenderer.js';
|
||||||
import { colorizeCode } from '../utils/CodeColorizer.js';
|
import { colorizeCode } from '../utils/CodeColorizer.js';
|
||||||
|
import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
||||||
|
|
||||||
interface ThemeDialogProps {
|
interface ThemeDialogProps {
|
||||||
/** Callback function when a theme is selected */
|
/** Callback function when a theme is selected */
|
||||||
onSelect: (themeName: string) => void;
|
onSelect: (themeName: string | undefined, scope: SettingScope) => void;
|
||||||
|
|
||||||
/** Callback function when a theme is highlighted */
|
/** Callback function when a theme is highlighted */
|
||||||
onHighlight: (themeName: string) => void;
|
onHighlight: (themeName: string | undefined) => void;
|
||||||
|
/** The settings object */
|
||||||
|
settings: LoadedSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ThemeDialog({
|
export function ThemeDialog({
|
||||||
onSelect,
|
onSelect,
|
||||||
onHighlight,
|
onHighlight,
|
||||||
|
settings,
|
||||||
}: ThemeDialogProps): React.JSX.Element {
|
}: ThemeDialogProps): React.JSX.Element {
|
||||||
|
const [selectedScope, setSelectedScope] = useState<SettingScope>(
|
||||||
|
SettingScope.User,
|
||||||
|
);
|
||||||
|
|
||||||
const themeItems = themeManager.getAvailableThemes().map((theme) => ({
|
const themeItems = themeManager.getAvailableThemes().map((theme) => ({
|
||||||
label: theme.active ? `${theme.name} (Active)` : theme.name,
|
label: theme.active ? `${theme.name} (Active)` : theme.name,
|
||||||
value: theme.name,
|
value: theme.name,
|
||||||
}));
|
}));
|
||||||
const initialIndex = themeItems.findIndex(
|
const [selectInputKey, setSelectInputKey] = useState(Date.now());
|
||||||
(item) => item.value === themeManager.getActiveTheme().name,
|
|
||||||
|
const initialThemeIndex = themeItems.findIndex(
|
||||||
|
(item) =>
|
||||||
|
item.value ===
|
||||||
|
(settings.forScope(selectedScope).settings.theme || DEFAULT_THEME.name),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const scopeItems = [
|
||||||
|
{ label: 'User Settings', value: SettingScope.User },
|
||||||
|
{ label: 'Workspace Settings', value: SettingScope.Workspace },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleThemeSelect = (themeName: string) => {
|
||||||
|
onSelect(themeName, selectedScope);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScopeHighlight = (scope: SettingScope) => {
|
||||||
|
setSelectedScope(scope);
|
||||||
|
setSelectInputKey(Date.now());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScopeSelect = (scope: SettingScope) => {
|
||||||
|
handleScopeHighlight(scope);
|
||||||
|
setFocusedSection('theme'); // Reset focus to theme section
|
||||||
|
};
|
||||||
|
|
||||||
|
const [focusedSection, setFocusedSection] = useState<'theme' | 'scope'>(
|
||||||
|
'theme',
|
||||||
|
);
|
||||||
|
|
||||||
|
useInput((input, key) => {
|
||||||
|
if (key.tab) {
|
||||||
|
setFocusedSection((prev) => (prev === 'theme' ? 'scope' : 'theme'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let otherScopeModifiedMessage = '';
|
||||||
|
const otherScope =
|
||||||
|
selectedScope === SettingScope.User
|
||||||
|
? SettingScope.Workspace
|
||||||
|
: SettingScope.User;
|
||||||
|
if (settings.forScope(otherScope).settings.theme !== undefined) {
|
||||||
|
otherScopeModifiedMessage =
|
||||||
|
settings.forScope(selectedScope).settings.theme !== undefined
|
||||||
|
? `(Also modified in ${otherScope})`
|
||||||
|
: `(Modified in ${otherScope})`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
borderStyle="round"
|
borderStyle="round"
|
||||||
|
@ -39,18 +93,36 @@ export function ThemeDialog({
|
||||||
padding={1}
|
padding={1}
|
||||||
width="50%"
|
width="50%"
|
||||||
>
|
>
|
||||||
<Box marginBottom={1}>
|
<Text bold={focusedSection === 'theme'}>
|
||||||
<Text bold>Select Theme</Text>
|
{focusedSection === 'theme' ? '> ' : ' '}Select Theme{' '}
|
||||||
</Box>
|
<Text color={Colors.SubtleComment}>{otherScopeModifiedMessage}</Text>
|
||||||
|
</Text>
|
||||||
|
|
||||||
<RadioButtonSelect
|
<RadioButtonSelect
|
||||||
|
key={selectInputKey}
|
||||||
items={themeItems}
|
items={themeItems}
|
||||||
initialIndex={initialIndex}
|
initialIndex={initialThemeIndex}
|
||||||
onSelect={onSelect}
|
onSelect={handleThemeSelect} // Use the wrapper handler
|
||||||
onHighlight={onHighlight}
|
onHighlight={onHighlight}
|
||||||
|
isFocused={focusedSection === 'theme'}
|
||||||
/>
|
/>
|
||||||
|
{/* Scope Selection */}
|
||||||
|
<Box marginTop={1} flexDirection="column">
|
||||||
|
<Text bold={focusedSection === 'scope'}>
|
||||||
|
{focusedSection === 'scope' ? '> ' : ' '}Apply To
|
||||||
|
</Text>
|
||||||
|
<RadioButtonSelect
|
||||||
|
items={scopeItems}
|
||||||
|
initialIndex={0} // Default to User Settings
|
||||||
|
onSelect={handleScopeSelect}
|
||||||
|
onHighlight={handleScopeHighlight}
|
||||||
|
isFocused={focusedSection === 'scope'}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
<Text color={Colors.SubtleComment}>
|
<Text color={Colors.SubtleComment}>
|
||||||
(Use ↑/↓ arrows and Enter to select)
|
(Use ↑/↓ arrows and Enter to select, Tab to change focus)
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,9 @@ export interface RadioButtonSelectProps<T> {
|
||||||
|
|
||||||
/** Function called when an item is highlighted. Receives the `value` of the selected item. */
|
/** Function called when an item is highlighted. Receives the `value` of the selected item. */
|
||||||
onHighlight?: (value: T) => void;
|
onHighlight?: (value: T) => void;
|
||||||
|
|
||||||
|
/** Whether this select input is currently focused and should respond to input. */
|
||||||
|
isFocused?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,6 +80,7 @@ export function RadioButtonSelect<T>({
|
||||||
initialIndex,
|
initialIndex,
|
||||||
onSelect,
|
onSelect,
|
||||||
onHighlight,
|
onHighlight,
|
||||||
|
isFocused,
|
||||||
}: RadioButtonSelectProps<T>): React.JSX.Element {
|
}: RadioButtonSelectProps<T>): React.JSX.Element {
|
||||||
const handleSelect = (item: RadioSelectItem<T>) => {
|
const handleSelect = (item: RadioSelectItem<T>) => {
|
||||||
onSelect(item.value);
|
onSelect(item.value);
|
||||||
|
@ -95,6 +99,7 @@ export function RadioButtonSelect<T>({
|
||||||
initialIndex={initialIndex}
|
initialIndex={initialIndex}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
onHighlight={handleHighlight}
|
onHighlight={handleHighlight}
|
||||||
|
isFocused={isFocused}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,23 +6,36 @@
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { themeManager } from '../themes/theme-manager.js';
|
import { themeManager } from '../themes/theme-manager.js';
|
||||||
|
import { LoadedSettings, SettingScope } from '../../config/settings.js'; // Import LoadedSettings, AppSettings, MergedSetting
|
||||||
|
|
||||||
interface UseThemeCommandReturn {
|
interface UseThemeCommandReturn {
|
||||||
isThemeDialogOpen: boolean;
|
isThemeDialogOpen: boolean;
|
||||||
openThemeDialog: () => void;
|
openThemeDialog: () => void;
|
||||||
handleThemeSelect: (themeName: string) => void;
|
handleThemeSelect: (
|
||||||
handleThemeHighlight: (themeName: string) => void;
|
themeName: string | undefined,
|
||||||
|
scope: SettingScope,
|
||||||
|
) => void; // Added scope
|
||||||
|
handleThemeHighlight: (themeName: string | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useThemeCommand = (): UseThemeCommandReturn => {
|
export const useThemeCommand = (
|
||||||
const [isThemeDialogOpen, setIsThemeDialogOpen] = useState(false);
|
loadedSettings: LoadedSettings, // Changed parameter
|
||||||
|
): UseThemeCommandReturn => {
|
||||||
|
// Determine the effective theme
|
||||||
|
const effectiveTheme = loadedSettings.getMerged().theme;
|
||||||
|
|
||||||
|
// Initial state: Open dialog if no theme is set in either user or workspace settings
|
||||||
|
const [isThemeDialogOpen, setIsThemeDialogOpen] = useState(
|
||||||
|
effectiveTheme === undefined,
|
||||||
|
);
|
||||||
|
// TODO: refactor how theme's are accessed to avoid requiring a forced render.
|
||||||
const [, setForceRender] = useState(0);
|
const [, setForceRender] = useState(0);
|
||||||
|
|
||||||
const openThemeDialog = useCallback(() => {
|
const openThemeDialog = useCallback(() => {
|
||||||
setIsThemeDialogOpen(true);
|
setIsThemeDialogOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function applyTheme(themeName: string) {
|
function applyTheme(themeName: string | undefined) {
|
||||||
try {
|
try {
|
||||||
themeManager.setActiveTheme(themeName);
|
themeManager.setActiveTheme(themeName);
|
||||||
setForceRender((v) => v + 1); // Trigger potential re-render
|
setForceRender((v) => v + 1); // Trigger potential re-render
|
||||||
|
@ -31,17 +44,25 @@ export const useThemeCommand = (): UseThemeCommandReturn => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleThemeHighlight = useCallback((themeName: string) => {
|
const handleThemeHighlight = useCallback(
|
||||||
|
(themeName: string | undefined) => {
|
||||||
applyTheme(themeName);
|
applyTheme(themeName);
|
||||||
}, []);
|
},
|
||||||
|
[applyTheme],
|
||||||
|
); // Added applyTheme to dependencies
|
||||||
|
|
||||||
const handleThemeSelect = useCallback((themeName: string) => {
|
const handleThemeSelect = useCallback(
|
||||||
|
(themeName: string | undefined, scope: SettingScope) => {
|
||||||
|
// Added scope parameter
|
||||||
try {
|
try {
|
||||||
applyTheme(themeName);
|
loadedSettings.setValue(scope, 'theme', themeName); // Update the merged settings
|
||||||
|
applyTheme(loadedSettings.getMerged().theme); // Apply the current theme
|
||||||
} finally {
|
} finally {
|
||||||
setIsThemeDialogOpen(false); // Close the dialog
|
setIsThemeDialogOpen(false); // Close the dialog
|
||||||
}
|
}
|
||||||
}, []);
|
},
|
||||||
|
[applyTheme], // Added applyTheme to dependencies
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isThemeDialogOpen,
|
isThemeDialogOpen,
|
||||||
|
|
|
@ -19,8 +19,9 @@ export interface ThemeDisplay {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_THEME: Theme = VS2015;
|
||||||
|
|
||||||
class ThemeManager {
|
class ThemeManager {
|
||||||
private static readonly DEFAULT_THEME: Theme = VS2015;
|
|
||||||
private readonly availableThemes: Theme[];
|
private readonly availableThemes: Theme[];
|
||||||
private activeTheme: Theme;
|
private activeTheme: Theme;
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@ class ThemeManager {
|
||||||
XCode,
|
XCode,
|
||||||
ANSI,
|
ANSI,
|
||||||
];
|
];
|
||||||
this.activeTheme = ThemeManager.DEFAULT_THEME;
|
this.activeTheme = DEFAULT_THEME;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -52,10 +53,8 @@ class ThemeManager {
|
||||||
* Sets the active theme.
|
* Sets the active theme.
|
||||||
* @param themeName The name of the theme to activate.
|
* @param themeName The name of the theme to activate.
|
||||||
*/
|
*/
|
||||||
setActiveTheme(themeName: string): void {
|
setActiveTheme(themeName: string | undefined): void {
|
||||||
const foundTheme = this.availableThemes.find(
|
const foundTheme = this.findThemeByName(themeName);
|
||||||
(theme) => theme.name === themeName,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (foundTheme) {
|
if (foundTheme) {
|
||||||
this.activeTheme = foundTheme;
|
this.activeTheme = foundTheme;
|
||||||
|
@ -64,6 +63,13 @@ class ThemeManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
* Returns the currently active theme object.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue