diff options
Diffstat (limited to 'packages/cli/src/ui')
| -rw-r--r-- | packages/cli/src/ui/App.tsx | 7 | ||||
| -rw-r--r-- | packages/cli/src/ui/components/ThemeDialog.tsx | 98 | ||||
| -rw-r--r-- | packages/cli/src/ui/components/shared/RadioButtonSelect.tsx | 5 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/useThemeCommand.ts | 51 | ||||
| -rw-r--r-- | packages/cli/src/ui/themes/theme-manager.ts | 18 |
5 files changed, 143 insertions, 36 deletions
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 8aaa1018..5ddf13db 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -20,6 +20,7 @@ import { useStartupWarnings } from './hooks/useAppEffects.js'; import { shortenPath, type Config } from '@gemini-code/server'; import { Colors } from './colors.js'; import { Intro } from './components/Intro.js'; +import { LoadedSettings } from '../config/settings.js'; import { Tips } from './components/Tips.js'; import { ConsoleOutput } from './components/ConsolePatcher.js'; import { HistoryItemDisplay } from './components/HistoryItemDisplay.js'; @@ -29,10 +30,11 @@ import { isAtCommand } from './utils/commandUtils.js'; interface AppProps { config: Config; + settings: LoadedSettings; cliVersion: string; } -export const App = ({ config, cliVersion }: AppProps) => { +export const App = ({ config, settings, cliVersion }: AppProps) => { const [history, setHistory] = useState<HistoryItem[]>([]); const [startupWarnings, setStartupWarnings] = useState<string[]>([]); const { @@ -40,7 +42,7 @@ export const App = ({ config, cliVersion }: AppProps) => { openThemeDialog, handleThemeSelect, handleThemeHighlight, - } = useThemeCommand(); + } = useThemeCommand(settings); const { streamingState, @@ -176,6 +178,7 @@ export const App = ({ config, cliVersion }: AppProps) => { <ThemeDialog onSelect={handleThemeSelect} onHighlight={handleThemeHighlight} + settings={settings} /> ) : ( <> diff --git a/packages/cli/src/ui/components/ThemeDialog.tsx b/packages/cli/src/ui/components/ThemeDialog.tsx index 62ede336..7e8c5afd 100644 --- a/packages/cli/src/ui/components/ThemeDialog.tsx +++ b/packages/cli/src/ui/components/ThemeDialog.tsx @@ -4,33 +4,87 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; -import { Box, Text } from 'ink'; +import React, { useState } from 'react'; +import { Box, Text, useInput } from 'ink'; 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 { DiffRenderer } from './messages/DiffRenderer.js'; import { colorizeCode } from '../utils/CodeColorizer.js'; +import { LoadedSettings, SettingScope } from '../../config/settings.js'; interface ThemeDialogProps { /** 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 */ - onHighlight: (themeName: string) => void; + onHighlight: (themeName: string | undefined) => void; + /** The settings object */ + settings: LoadedSettings; } export function ThemeDialog({ onSelect, onHighlight, + settings, }: ThemeDialogProps): React.JSX.Element { + const [selectedScope, setSelectedScope] = useState<SettingScope>( + SettingScope.User, + ); + const themeItems = themeManager.getAvailableThemes().map((theme) => ({ label: theme.active ? `${theme.name} (Active)` : theme.name, value: theme.name, })); - const initialIndex = themeItems.findIndex( - (item) => item.value === themeManager.getActiveTheme().name, + const [selectInputKey, setSelectInputKey] = useState(Date.now()); + + 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 ( <Box borderStyle="round" @@ -39,18 +93,36 @@ export function ThemeDialog({ padding={1} width="50%" > - <Box marginBottom={1}> - <Text bold>Select Theme</Text> - </Box> + <Text bold={focusedSection === 'theme'}> + {focusedSection === 'theme' ? '> ' : ' '}Select Theme{' '} + <Text color={Colors.SubtleComment}>{otherScopeModifiedMessage}</Text> + </Text> + <RadioButtonSelect + key={selectInputKey} items={themeItems} - initialIndex={initialIndex} - onSelect={onSelect} + initialIndex={initialThemeIndex} + onSelect={handleThemeSelect} // Use the wrapper handler 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}> <Text color={Colors.SubtleComment}> - (Use ↑/↓ arrows and Enter to select) + (Use ↑/↓ arrows and Enter to select, Tab to change focus) </Text> </Box> diff --git a/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx b/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx index bda56014..3db8b678 100644 --- a/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx +++ b/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx @@ -37,6 +37,9 @@ export interface RadioButtonSelectProps<T> { /** Function called when an item is highlighted. Receives the `value` of the selected item. */ 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, onSelect, onHighlight, + isFocused, }: RadioButtonSelectProps<T>): React.JSX.Element { const handleSelect = (item: RadioSelectItem<T>) => { onSelect(item.value); @@ -95,6 +99,7 @@ export function RadioButtonSelect<T>({ initialIndex={initialIndex} onSelect={handleSelect} onHighlight={handleHighlight} + isFocused={isFocused} /> ); } diff --git a/packages/cli/src/ui/hooks/useThemeCommand.ts b/packages/cli/src/ui/hooks/useThemeCommand.ts index 66ec9eda..3ca48cbf 100644 --- a/packages/cli/src/ui/hooks/useThemeCommand.ts +++ b/packages/cli/src/ui/hooks/useThemeCommand.ts @@ -6,23 +6,36 @@ import { useState, useCallback } from 'react'; import { themeManager } from '../themes/theme-manager.js'; +import { LoadedSettings, SettingScope } from '../../config/settings.js'; // Import LoadedSettings, AppSettings, MergedSetting interface UseThemeCommandReturn { isThemeDialogOpen: boolean; openThemeDialog: () => void; - handleThemeSelect: (themeName: string) => void; - handleThemeHighlight: (themeName: string) => void; + handleThemeSelect: ( + themeName: string | undefined, + scope: SettingScope, + ) => void; // Added scope + handleThemeHighlight: (themeName: string | undefined) => void; } -export const useThemeCommand = (): UseThemeCommandReturn => { - const [isThemeDialogOpen, setIsThemeDialogOpen] = useState(false); +export const useThemeCommand = ( + 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 openThemeDialog = useCallback(() => { setIsThemeDialogOpen(true); }, []); - function applyTheme(themeName: string) { + function applyTheme(themeName: string | undefined) { try { themeManager.setActiveTheme(themeName); setForceRender((v) => v + 1); // Trigger potential re-render @@ -31,17 +44,25 @@ export const useThemeCommand = (): UseThemeCommandReturn => { } } - const handleThemeHighlight = useCallback((themeName: string) => { - applyTheme(themeName); - }, []); - - const handleThemeSelect = useCallback((themeName: string) => { - try { + const handleThemeHighlight = useCallback( + (themeName: string | undefined) => { applyTheme(themeName); - } finally { - setIsThemeDialogOpen(false); // Close the dialog - } - }, []); + }, + [applyTheme], + ); // Added applyTheme to dependencies + + const handleThemeSelect = useCallback( + (themeName: string | undefined, scope: SettingScope) => { + // Added scope parameter + try { + loadedSettings.setValue(scope, 'theme', themeName); // Update the merged settings + applyTheme(loadedSettings.getMerged().theme); // Apply the current theme + } finally { + setIsThemeDialogOpen(false); // Close the dialog + } + }, + [applyTheme], // Added applyTheme to dependencies + ); return { isThemeDialogOpen, diff --git a/packages/cli/src/ui/themes/theme-manager.ts b/packages/cli/src/ui/themes/theme-manager.ts index 5a880705..4a8cc32c 100644 --- a/packages/cli/src/ui/themes/theme-manager.ts +++ b/packages/cli/src/ui/themes/theme-manager.ts @@ -19,8 +19,9 @@ export interface ThemeDisplay { active: boolean; } +export const DEFAULT_THEME: Theme = VS2015; + class ThemeManager { - private static readonly DEFAULT_THEME: Theme = VS2015; private readonly availableThemes: Theme[]; private activeTheme: Theme; @@ -35,7 +36,7 @@ class ThemeManager { XCode, ANSI, ]; - this.activeTheme = ThemeManager.DEFAULT_THEME; + this.activeTheme = DEFAULT_THEME; } /** @@ -52,10 +53,8 @@ class ThemeManager { * Sets the active theme. * @param themeName The name of the theme to activate. */ - setActiveTheme(themeName: string): void { - const foundTheme = this.availableThemes.find( - (theme) => theme.name === themeName, - ); + setActiveTheme(themeName: string | undefined): void { + const foundTheme = this.findThemeByName(themeName); if (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. */ |
