| | |
| | |
| | |
| | |
| | |
| |
|
| | import React, { useState } from 'react'; |
| | import { Box, Text, useInput } from 'ink'; |
| | import { Colors } from '../colors.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 { |
| | |
| | onSelect: (themeName: string | undefined, scope: SettingScope) => void; |
| |
|
| | |
| | onHighlight: (themeName: string | undefined) => void; |
| | |
| | settings: LoadedSettings; |
| | availableTerminalHeight?: number; |
| | terminalWidth: number; |
| | } |
| |
|
| | export function ThemeDialog({ |
| | onSelect, |
| | onHighlight, |
| | settings, |
| | availableTerminalHeight, |
| | terminalWidth, |
| | }: ThemeDialogProps): React.JSX.Element { |
| | const [selectedScope, setSelectedScope] = useState<SettingScope>( |
| | SettingScope.User, |
| | ); |
| |
|
| | |
| | const themeItems = themeManager.getAvailableThemes().map((theme) => { |
| | const typeString = theme.type.charAt(0).toUpperCase() + theme.type.slice(1); |
| | return { |
| | label: theme.name, |
| | value: theme.name, |
| | themeNameDisplay: theme.name, |
| | themeTypeDisplay: typeString, |
| | }; |
| | }); |
| | const [selectInputKey, setSelectInputKey] = useState(Date.now()); |
| |
|
| | |
| | |
| | const initialThemeIndex = themeItems.findIndex( |
| | (item) => item.value === (settings.merged.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'); |
| | }; |
| |
|
| | const [focusedSection, setFocusedSection] = useState<'theme' | 'scope'>( |
| | 'theme', |
| | ); |
| |
|
| | useInput((input, key) => { |
| | if (key.tab) { |
| | setFocusedSection((prev) => (prev === 'theme' ? 'scope' : 'theme')); |
| | } |
| | if (key.escape) { |
| | onSelect(undefined, selectedScope); |
| | } |
| | }); |
| |
|
| | 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})`; |
| | } |
| |
|
| | |
| | |
| | const PREVIEW_PANE_WIDTH_PERCENTAGE = 0.55; |
| | |
| | |
| | const PREVIEW_PANE_WIDTH_SAFETY_MARGIN = 0.9; |
| | |
| | const TOTAL_HORIZONTAL_PADDING = 4; |
| | const colorizeCodeWidth = Math.max( |
| | Math.floor( |
| | (terminalWidth - TOTAL_HORIZONTAL_PADDING) * |
| | PREVIEW_PANE_WIDTH_PERCENTAGE * |
| | PREVIEW_PANE_WIDTH_SAFETY_MARGIN, |
| | ), |
| | 1, |
| | ); |
| |
|
| | const DAILOG_PADDING = 2; |
| | const selectThemeHeight = themeItems.length + 1; |
| | const SCOPE_SELECTION_HEIGHT = 4; |
| | const SPACE_BETWEEN_THEME_SELECTION_AND_APPLY_TO = 1; |
| | const TAB_TO_SELECT_HEIGHT = 2; |
| | availableTerminalHeight = availableTerminalHeight ?? Number.MAX_SAFE_INTEGER; |
| | availableTerminalHeight -= 2; |
| | availableTerminalHeight -= TAB_TO_SELECT_HEIGHT; |
| |
|
| | let totalLeftHandSideHeight = |
| | DAILOG_PADDING + |
| | selectThemeHeight + |
| | SCOPE_SELECTION_HEIGHT + |
| | SPACE_BETWEEN_THEME_SELECTION_AND_APPLY_TO; |
| |
|
| | let showScopeSelection = true; |
| | let includePadding = true; |
| |
|
| | |
| | if (totalLeftHandSideHeight > availableTerminalHeight) { |
| | includePadding = false; |
| | totalLeftHandSideHeight -= DAILOG_PADDING; |
| | } |
| |
|
| | if (totalLeftHandSideHeight > availableTerminalHeight) { |
| | |
| | totalLeftHandSideHeight -= SCOPE_SELECTION_HEIGHT; |
| | showScopeSelection = false; |
| | } |
| |
|
| | |
| | const currenFocusedSection = !showScopeSelection ? 'theme' : focusedSection; |
| |
|
| | |
| | |
| | const PREVIEW_PANE_FIXED_VERTICAL_SPACE = 8; |
| |
|
| | |
| | availableTerminalHeight = Math.max( |
| | availableTerminalHeight, |
| | totalLeftHandSideHeight, |
| | ); |
| | const availableTerminalHeightCodeBlock = |
| | availableTerminalHeight - |
| | PREVIEW_PANE_FIXED_VERTICAL_SPACE - |
| | (includePadding ? 2 : 0) * 2; |
| | |
| | const diffHeight = Math.floor(availableTerminalHeightCodeBlock / 2) - 1; |
| | const codeBlockHeight = Math.ceil(availableTerminalHeightCodeBlock / 2) + 1; |
| |
|
| | return ( |
| | <Box |
| | borderStyle="round" |
| | borderColor={Colors.Gray} |
| | flexDirection="column" |
| | paddingTop={includePadding ? 1 : 0} |
| | paddingBottom={includePadding ? 1 : 0} |
| | paddingLeft={1} |
| | paddingRight={1} |
| | width="100%" |
| | > |
| | <Box flexDirection="row"> |
| | {/* Left Column: Selection */} |
| | <Box flexDirection="column" width="45%" paddingRight={2}> |
| | <Text bold={currenFocusedSection === 'theme'} wrap="truncate"> |
| | {currenFocusedSection === 'theme' ? '> ' : ' '}Select Theme{' '} |
| | <Text color={Colors.Gray}>{otherScopeModifiedMessage}</Text> |
| | </Text> |
| | <RadioButtonSelect |
| | key={selectInputKey} |
| | items={themeItems} |
| | initialIndex={initialThemeIndex} |
| | onSelect={handleThemeSelect} |
| | onHighlight={onHighlight} |
| | isFocused={currenFocusedSection === 'theme'} |
| | /> |
| | |
| | {/* Scope Selection */} |
| | {showScopeSelection && ( |
| | <Box marginTop={1} flexDirection="column"> |
| | <Text bold={currenFocusedSection === 'scope'} wrap="truncate"> |
| | {currenFocusedSection === 'scope' ? '> ' : ' '}Apply To |
| | </Text> |
| | <RadioButtonSelect |
| | items={scopeItems} |
| | initialIndex={0} // Default to User Settings |
| | onSelect={handleScopeSelect} |
| | onHighlight={handleScopeHighlight} |
| | isFocused={currenFocusedSection === 'scope'} |
| | /> |
| | </Box> |
| | )} |
| | </Box> |
| | |
| | {/* Right Column: Preview */} |
| | <Box flexDirection="column" width="55%" paddingLeft={2}> |
| | <Text bold>Preview</Text> |
| | <Box |
| | borderStyle="single" |
| | borderColor={Colors.Gray} |
| | paddingTop={includePadding ? 1 : 0} |
| | paddingBottom={includePadding ? 1 : 0} |
| | paddingLeft={1} |
| | paddingRight={1} |
| | flexDirection="column" |
| | > |
| | {colorizeCode( |
| | `# function |
| | -def fibonacci(n): |
| | - a, b = 0, 1 |
| | - for _ in range(n): |
| | - a, b = b, a + b |
| | - return a`, |
| | 'python', |
| | codeBlockHeight, |
| | colorizeCodeWidth, |
| | )} |
| | <Box marginTop={1} /> |
| | <DiffRenderer |
| | diffContent={`--- a/old_file.txt |
| | -+++ b/new_file.txt |
| | -@@ -1,4 +1,5 @@ |
| | - This is a context line. |
| | --This line was deleted. |
| | -+This line was added. |
| | -`} |
| | availableTerminalHeight={diffHeight} |
| | terminalWidth={colorizeCodeWidth} |
| | /> |
| | </Box> |
| | </Box> |
| | </Box> |
| | <Box marginTop={1}> |
| | <Text color={Colors.Gray} wrap="truncate"> |
| | (Use Enter to select |
| | {showScopeSelection ? ', Tab to change focus' : ''}) |
| | </Text> |
| | </Box> |
| | </Box> |
| | ); |
| | } |
| |
|