import { feature } from 'bun:bundle'; import type { StructuredPatchHunk } from 'diff'; import * as React from 'react'; import { useExitOnCtrlCDWithKeybindings } from '../hooks/useExitOnCtrlCDWithKeybindings.js' import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { Box, Text, usePreviewTheme, useTheme, useThemeSetting } from '../ink.js'; import { useRegisterKeybindingContext } from '../keybindings/KeybindingContext.js'; import { useKeybinding } from '../keybindings/useKeybinding.js'; import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js'; import { useAppState, useSetAppState } from '../state/AppState.js'; import type { AppState } from '../state/AppStateStore.js'; import { gracefulShutdown } from '../utils/gracefulShutdown.js'; import { updateSettingsForSource } from '../utils/settings/settings.js'; import type { ThemeSetting } from '../utils/theme.js'; import { Select } from './CustomSelect/index.js'; import { Byline } from './design-system/Byline.js'; import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'; import { getColorModuleUnavailableReason, getSyntaxTheme } from './StructuredDiff/colorDiff.js'; import { StructuredDiff } from './StructuredDiff.js'; type StructuredDiffComponent = React.ComponentType<{ patch: StructuredPatchHunk dim: boolean filePath: string firstLine: string | null width: number skipHighlighting?: boolean }> const StructuredDiffView = StructuredDiff as StructuredDiffComponent export type ThemePickerProps = { onThemeSelect: (setting: ThemeSetting) => void; showIntroText?: boolean; helpText?: string; showHelpTextBelow?: boolean; hideEscToCancel?: boolean; /** Skip exit handling when running in a context that already has it (e.g., onboarding) */ skipExitHandling?: boolean; /** Called when the user cancels (presses Escape). If skipExitHandling is true and this is provided, it will be called instead of just saving the preview. */ onCancel?: () => void; } const DEMO_PATCH: StructuredPatchHunk = { oldStart: 1, newStart: 1, oldLines: 3, newLines: 3, lines: [ ' function greet() {', '- console.log("Hello, World!");', '+ console.log("Hello, Claude!");', ' }', ], } /** * Theme chooser with live preview. Implemented without react-compiler `_c` memo * caches so preview/subtree reconciliation cannot stick on stale element refs when * `setPreviewTheme` updates the resolved palette. */ export function ThemePicker({ onThemeSelect, showIntroText = false, helpText = '', showHelpTextBelow = false, hideEscToCancel = false, skipExitHandling = false, onCancel: onCancelProp, }: ThemePickerProps) { const [theme] = useTheme(); const themeSetting = useThemeSetting(); const { columns } = useTerminalSize(); const colorModuleUnavailableReason = React.useMemo( () => getColorModuleUnavailableReason(), [], ) const syntaxTheme = colorModuleUnavailableReason === null ? getSyntaxTheme(theme) : null const { setPreviewTheme, savePreview, cancelPreview } = usePreviewTheme() const syntaxHighlightingDisabled = useAppState( (s: AppState) => s.settings.syntaxHighlightingDisabled ?? false ); const setAppState = useSetAppState(); useRegisterKeybindingContext("ThemePicker", true); const syntaxToggleShortcut = useShortcutDisplay("theme:toggleSyntaxHighlighting", "ThemePicker", "ctrl+t"); const toggleSyntax = React.useCallback(() => { if (colorModuleUnavailableReason === null) { const newValue = !syntaxHighlightingDisabled updateSettingsForSource("userSettings", { syntaxHighlightingDisabled: newValue }); setAppState(prev => ({ ...prev, settings: { ...prev.settings, syntaxHighlightingDisabled: newValue } })); } }, [ colorModuleUnavailableReason, syntaxHighlightingDisabled, setAppState, ]) useKeybinding("theme:toggleSyntaxHighlighting", toggleSyntax, { context: "ThemePicker", }) const exitState = useExitOnCtrlCDWithKeybindings( skipExitHandling ? () => {} : undefined, ) const themeOptions = React.useMemo( () => [ ...(feature("AUTO_THEME") ? [{ label: "Auto (match terminal)", value: "auto" as const }] : []), { label: "Dark mode", value: "dark" as const }, { label: "Light mode", value: "light" as const }, { label: "Dark mode (colorblind-friendly)", value: "dark-daltonized" as const, }, { label: "Light mode (colorblind-friendly)", value: "light-daltonized" as const, }, { label: "Dark mode (ANSI colors only)", value: "dark-ansi" as const }, { label: "Light mode (ANSI colors only)", value: "light-ansi" as const },], [], ) const handleRowFocus = React.useCallback( (setting: ThemeSetting) => { setPreviewTheme(setting) }, [setPreviewTheme], ) const handleSelect = React.useCallback( (setting: ThemeSetting) => { savePreview() onThemeSelect(setting) }, [savePreview, onThemeSelect], ) const handleCancel = React.useCallback(() => { cancelPreview() if (skipExitHandling) { onCancelProp?.() } else { void gracefulShutdown(0) } }, [cancelPreview, onCancelProp, skipExitHandling]) const syntaxHint = colorModuleUnavailableReason === 'env' ? `Syntax highlighting disabled (via CLAUDE_CODE_SYNTAX_HIGHLIGHT=${process.env.CLAUDE_CODE_SYNTAX_HIGHLIGHT})` : syntaxHighlightingDisabled ? `Syntax highlighting disabled (${syntaxToggleShortcut} to enable)` : syntaxTheme ? `Syntax theme: ${syntaxTheme.theme}${syntaxTheme.source ? ` (from ${syntaxTheme.source})` : ''} (${syntaxToggleShortcut} to disable)` : `Syntax highlighting enabled (${syntaxToggleShortcut} to disable)` const header = showIntroText ? ( {"Let's get started."} ) : ( Theme ) const introBlock = ( Choose the text style that looks best with your terminal {helpText && !showHelpTextBelow ? ( {helpText} ) : null} ) const content = ( {header} {introBlock}