UNPKG

buroventures-harald-code

Version:

Harald Code - AI-powered coding assistant CLI

153 lines 9.48 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { useCallback, 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 { SettingScope } from '../../config/settings.js'; export function ThemeDialog({ onSelect, onHighlight, settings, availableTerminalHeight, terminalWidth, }) { const [selectedScope, setSelectedScope] = useState(SettingScope.User); // Track the currently highlighted theme name const [highlightedThemeName, setHighlightedThemeName] = useState(settings.merged.theme || DEFAULT_THEME.name); // Generate theme items filtered by selected scope const customThemes = selectedScope === SettingScope.User ? settings.user.settings.customThemes || {} : settings.merged.customThemes || {}; const builtInThemes = themeManager .getAvailableThemes() .filter((theme) => theme.type !== 'custom'); const customThemeNames = Object.keys(customThemes); const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1); // Generate theme items const themeItems = [ ...builtInThemes.map((theme) => ({ label: theme.name, value: theme.name, themeNameDisplay: theme.name, themeTypeDisplay: capitalize(theme.type), })), ...customThemeNames.map((name) => ({ label: name, value: name, themeNameDisplay: name, themeTypeDisplay: 'Custom', })), ]; const [selectInputKey, setSelectInputKey] = useState(Date.now()); // Find the index of the selected theme, but only if it exists in the list const selectedThemeName = settings.merged.theme || DEFAULT_THEME.name; const initialThemeIndex = themeItems.findIndex((item) => item.value === selectedThemeName); // If not found, fall back to the first theme const safeInitialThemeIndex = initialThemeIndex >= 0 ? initialThemeIndex : 0; const scopeItems = [ { label: 'User Settings', value: SettingScope.User }, { label: 'Workspace Settings', value: SettingScope.Workspace }, { label: 'System Settings', value: SettingScope.System }, ]; const handleThemeSelect = useCallback((themeName) => { onSelect(themeName, selectedScope); }, [onSelect, selectedScope]); const handleThemeHighlight = (themeName) => { setHighlightedThemeName(themeName); onHighlight(themeName); }; const handleScopeHighlight = useCallback((scope) => { setSelectedScope(scope); setSelectInputKey(Date.now()); }, []); const handleScopeSelect = useCallback((scope) => { handleScopeHighlight(scope); setFocusedSection('theme'); // Reset focus to theme section }, [handleScopeHighlight]); const [focusedSection, setFocusedSection] = useState('theme'); useInput((input, key) => { if (key.tab) { setFocusedSection((prev) => (prev === 'theme' ? 'scope' : 'theme')); } if (key.escape) { onSelect(undefined, selectedScope); } }); const otherScopes = Object.values(SettingScope).filter((scope) => scope !== selectedScope); const modifiedInOtherScopes = otherScopes.filter((scope) => settings.forScope(scope).settings.theme !== undefined); let otherScopeModifiedMessage = ''; if (modifiedInOtherScopes.length > 0) { const modifiedScopesStr = modifiedInOtherScopes.join(', '); otherScopeModifiedMessage = settings.forScope(selectedScope).settings.theme !== undefined ? `(Also modified in ${modifiedScopesStr})` : `(Modified in ${modifiedScopesStr})`; } // Constants for calculating preview pane layout. // These values are based on the JSX structure below. const PREVIEW_PANE_WIDTH_PERCENTAGE = 0.55; // A safety margin to prevent text from touching the border. // This is a complete hack unrelated to the 0.9 used in App.tsx const PREVIEW_PANE_WIDTH_SAFETY_MARGIN = 0.9; // Combined horizontal padding from the dialog and preview pane. 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 DIALOG_PADDING = 2; const selectThemeHeight = themeItems.length + 1; const SCOPE_SELECTION_HEIGHT = 4; // Height for the scope selection section + margin. const SPACE_BETWEEN_THEME_SELECTION_AND_APPLY_TO = 1; const TAB_TO_SELECT_HEIGHT = 2; availableTerminalHeight = availableTerminalHeight ?? Number.MAX_SAFE_INTEGER; availableTerminalHeight -= 2; // Top and bottom borders. availableTerminalHeight -= TAB_TO_SELECT_HEIGHT; let totalLeftHandSideHeight = DIALOG_PADDING + selectThemeHeight + SCOPE_SELECTION_HEIGHT + SPACE_BETWEEN_THEME_SELECTION_AND_APPLY_TO; let showScopeSelection = true; let includePadding = true; // Remove content from the LHS that can be omitted if it exceeds the available height. if (totalLeftHandSideHeight > availableTerminalHeight) { includePadding = false; totalLeftHandSideHeight -= DIALOG_PADDING; } if (totalLeftHandSideHeight > availableTerminalHeight) { // First, try hiding the scope selection totalLeftHandSideHeight -= SCOPE_SELECTION_HEIGHT; showScopeSelection = false; } // Don't focus the scope selection if it is hidden due to height constraints. const currentFocusedSection = !showScopeSelection ? 'theme' : focusedSection; // Vertical space taken by elements other than the two code blocks in the preview pane. // Includes "Preview" title, borders, and margin between blocks. const PREVIEW_PANE_FIXED_VERTICAL_SPACE = 8; // The right column doesn't need to ever be shorter than the left column. availableTerminalHeight = Math.max(availableTerminalHeight, totalLeftHandSideHeight); const availableTerminalHeightCodeBlock = availableTerminalHeight - PREVIEW_PANE_FIXED_VERTICAL_SPACE - (includePadding ? 2 : 0) * 2; // Subtract margin between code blocks from available height. const availableHeightForPanes = Math.max(0, availableTerminalHeightCodeBlock - 1); // The code block is slightly longer than the diff, so give it more space. const codeBlockHeight = Math.ceil(availableHeightForPanes * 0.6); const diffHeight = Math.floor(availableHeightForPanes * 0.4); return (_jsxs(Box, { borderStyle: "round", borderColor: Colors.Gray, flexDirection: "column", paddingTop: includePadding ? 1 : 0, paddingBottom: includePadding ? 1 : 0, paddingLeft: 1, paddingRight: 1, width: "100%", children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Box, { flexDirection: "column", width: "45%", paddingRight: 2, children: [_jsxs(Text, { bold: currentFocusedSection === 'theme', wrap: "truncate", children: [currentFocusedSection === 'theme' ? '> ' : ' ', "Select Theme", ' ', _jsx(Text, { color: Colors.Gray, children: otherScopeModifiedMessage })] }), _jsx(RadioButtonSelect, { items: themeItems, initialIndex: safeInitialThemeIndex, onSelect: handleThemeSelect, onHighlight: handleThemeHighlight, isFocused: currentFocusedSection === 'theme', maxItemsToShow: 8, showScrollArrows: true, showNumbers: currentFocusedSection === 'theme' }, selectInputKey), showScopeSelection && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { bold: currentFocusedSection === 'scope', wrap: "truncate", children: [currentFocusedSection === 'scope' ? '> ' : ' ', "Apply To"] }), _jsx(RadioButtonSelect, { items: scopeItems, initialIndex: 0, onSelect: handleScopeSelect, onHighlight: handleScopeHighlight, isFocused: currentFocusedSection === 'scope', showNumbers: currentFocusedSection === 'scope' })] }))] }), _jsxs(Box, { flexDirection: "column", width: "55%", paddingLeft: 2, children: [_jsx(Text, { bold: true, children: "Preview" }), (() => { const previewTheme = themeManager.getTheme(highlightedThemeName || DEFAULT_THEME.name) || DEFAULT_THEME; return (_jsxs(Box, { borderStyle: "single", borderColor: Colors.Gray, paddingTop: includePadding ? 1 : 0, paddingBottom: includePadding ? 1 : 0, paddingLeft: 1, paddingRight: 1, flexDirection: "column", children: [colorizeCode(`# function def fibonacci(n): a, b = 0, 1 for _ in range(n): a, b = b, a + b return a`, 'python', codeBlockHeight, colorizeCodeWidth), _jsx(Box, { marginTop: 1 }), _jsx(DiffRenderer, { diffContent: `--- a/util.py +++ b/util.py @@ -1,2 +1,2 @@ - print("Hello, " + name) + print(f"Hello, {name}!") `, availableTerminalHeight: diffHeight, terminalWidth: colorizeCodeWidth, theme: previewTheme })] })); })()] })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: Colors.Gray, wrap: "truncate", children: ["(Use Enter to select", showScopeSelection ? ', Tab to change focus' : '', ")"] }) })] })); } //# sourceMappingURL=ThemeDialog.js.map