From ee44140fb9b9bb184e25bfaa4adc97b3c0c7e2ed Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 31 Oct 2026 13:12:56 -0700 Subject: [PATCH] fix(terminal): scope terminal color overrides to their light/dark theme slot (#36581) --- .../normalize-loaded-global-settings.test.ts | 34 ++++++ .../normalize-loaded-global-settings.ts | 2 + .../prepare-loaded-terminal-settings.ts | 7 ++ .../dashboard-popout/AgentTerminalPreview.tsx | 7 +- .../native-chat-appearance-palette.test.ts | 3 +- .../native-chat-appearance-style.ts | 4 +- .../src/components/onboarding/ThemeStep.tsx | 16 +-- .../PlainSshTerminalView.tsx | 7 +- .../plugins/page/PluginThemeUseNow.test.tsx | 3 +- .../plugins/state/plugin-theme-apply.ts | 12 ++- .../settings/AgentDefaultSurfacePreviews.tsx | 5 +- .../settings/TerminalAppearanceSection.tsx | 102 ++++++++++-------- .../settings/TerminalSettingsPreview.tsx | 9 +- .../TerminalThemeSections.lifecycle.test.ts | 7 +- .../settings/TerminalThemeSections.tsx | 35 +++--- .../settings/TerminalWindowSection.tsx | 48 +++++++-- .../components/settings/useGhosttyImport.ts | 18 ++-- .../compose-active-terminal-theme.test.ts | 11 -- .../terminal-pane/terminal-appearance.ts | 23 ++-- .../use-terminal-pane-projection.ts | 3 +- src/renderer/src/i18n/locales/de.json | 7 +- src/renderer/src/i18n/locales/en.json | 7 +- src/renderer/src/i18n/locales/es.json | 7 +- src/renderer/src/i18n/locales/fr.json | 7 +- src/renderer/src/i18n/locales/ja.json | 7 +- src/renderer/src/i18n/locales/ko.json | 7 +- src/renderer/src/i18n/locales/pt.json | 7 +- src/renderer/src/i18n/locales/ru.json | 7 +- src/renderer/src/i18n/locales/tr.json | 7 +- src/renderer/src/i18n/locales/zh-Hant.json | 7 +- src/renderer/src/i18n/locales/zh.json | 7 +- .../src/lib/left-sidebar-appearance.ts | 6 +- src/renderer/src/lib/terminal-theme.ts | 34 ++---- ...-graph-terminal-surface-projection.test.ts | 2 +- .../mobile-terminal-theme.ts | 5 +- .../web-preference-normalization.ts | 2 + .../web/preload-api/web-preferences-store.ts | 1 + src/shared/default-global-settings.ts | 1 + src/shared/global-settings-base.ts | 4 +- src/shared/global-settings-types.ts | 2 - src/shared/terminal-color-overrides.ts | 74 +++++++++++++ src/shared/terminal-theme-selection.test.ts | 50 ++++++++- src/shared/terminal-theme-selection.ts | 62 ++++++++--- 43 files changed, 454 insertions(+), 222 deletions(-) diff --git a/src/main/persistence/loading-store/normalize-loaded-global-settings.test.ts b/src/main/persistence/loading-store/normalize-loaded-global-settings.test.ts index 41067b05f0a..9f0f784d711 100644 --- a/src/main/persistence/loading-store/normalize-loaded-global-settings.test.ts +++ b/src/main/persistence/loading-store/normalize-loaded-global-settings.test.ts @@ -278,6 +278,40 @@ it('migrates a shipped split chat/terminal profile using terminal choices while }) }) +describe('pre-#10561 shared terminal color overrides', () => { + const overrides = { background: '#101010' } + const legacy = { terminalColorOverridesSplit: undefined, terminalColorOverrides: overrides } + + it.each([ + { theme: 'system', terminalUseSeparateLightTheme: true }, + { theme: 'light', terminalUseSeparateLightTheme: true }, + { theme: 'system', terminalUseSeparateLightTheme: undefined } + ])('keep painting both modes when the light slot could show them (%o)', (appearance) => { + const normalized = normalizeLegacyProfile({ ...legacy, ...appearance }) + expect(normalized.terminalColorOverrides).toEqual(overrides) + expect(normalized.terminalColorOverridesLight).toEqual(overrides) + expect(normalized.terminalColorOverridesSplit).toBe(true) + }) + + it.each([ + { theme: 'dark', terminalUseSeparateLightTheme: true }, + { theme: 'light', terminalUseSeparateLightTheme: false } + ])('stay on the dark slot otherwise (%o)', (appearance) => { + const normalized = normalizeLegacyProfile({ ...legacy, ...appearance }) + expect(normalized.terminalColorOverrides).toEqual(overrides) + expect(normalized.terminalColorOverridesLight).toBeUndefined() + }) + + it('never re-split once migrated, so a cleared light slot stays clear', () => { + const normalized = normalizeLegacyProfile({ + terminalColorOverrides: overrides, + theme: 'light', + terminalUseSeparateLightTheme: true + }) + expect(normalized.terminalColorOverridesLight).toBeUndefined() + }) +}) + describe('terminal default shell', () => { it('trims a hand-edited value and treats whitespace-only as unset', () => { expect( diff --git a/src/main/persistence/loading-store/normalize-loaded-global-settings.ts b/src/main/persistence/loading-store/normalize-loaded-global-settings.ts index 7689fe82867..979aeff3c6f 100644 --- a/src/main/persistence/loading-store/normalize-loaded-global-settings.ts +++ b/src/main/persistence/loading-store/normalize-loaded-global-settings.ts @@ -36,6 +36,7 @@ export function normalizeLoadedGlobalSettings( migratedOptionAsAlt, migratedFloatingTerminalEnabled, migratedOsc52Clipboard, + migratedTerminalColorOverrides, migratedFloatingTerminalCwd, migratedFloatingTerminalTrustedCwds } = terminal @@ -111,6 +112,7 @@ export function normalizeLoadedGlobalSettings( localAccountRuntime: migratedLocalAccountRuntime, localAccountRuntimeDefaultedToAutoForAllUsers: true, ...migratedOsc52Clipboard, + ...migratedTerminalColorOverrides, floatingTerminalEnabled: migratedFloatingTerminalEnabled, floatingTerminalDefaultedForAllUsers: true, floatingTerminalCwd: migratedFloatingTerminalCwd, diff --git a/src/main/persistence/loading-store/prepare-loaded-terminal-settings.ts b/src/main/persistence/loading-store/prepare-loaded-terminal-settings.ts index d6e6f1a2f5d..2046d78de79 100644 --- a/src/main/persistence/loading-store/prepare-loaded-terminal-settings.ts +++ b/src/main/persistence/loading-store/prepare-loaded-terminal-settings.ts @@ -16,6 +16,7 @@ import { normalizeOsc52ClipboardDefaultOn, osc52ClipboardDefaultOnOverridesPersistedOff } from '../../../shared/osc52-clipboard-settings' +import { splitLegacyTerminalColorOverrides } from '../../../shared/terminal-color-overrides' import { migrateTerminalScrollbackRows, migrateTerminalTuiScrollSensitivityDefault @@ -47,6 +48,7 @@ export type PreparedLoadedTerminalSettings = { GlobalSettings, 'terminalAllowOsc52Clipboard' | 'terminalAllowOsc52ClipboardDefaultedOnForAllUsers' > + migratedTerminalColorOverrides: ReturnType migratedFloatingTerminalCwd: GlobalSettings['floatingTerminalCwd'] migratedFloatingTerminalTrustedCwds: GlobalSettings['floatingTerminalTrustedCwds'] osc52ClipboardNoticePending: boolean @@ -135,6 +137,10 @@ export function prepareLoadedTerminalSettings( if (parsed.settings?.terminalAllowOsc52ClipboardDefaultedOnForAllUsers !== true) { markNeedsSave() } + const migratedTerminalColorOverrides = splitLegacyTerminalColorOverrides(parsed.settings) + if (parsed.settings?.terminalColorOverridesSplit !== true) { + markNeedsSave() + } const floatingTerminalCwdMigrated = parsed.settings?.floatingTerminalCwdMigratedToAppWorkspace === true // Repair the old empty notes-directory migration; terminal cwd and notes now resolve separately. @@ -182,6 +188,7 @@ export function prepareLoadedTerminalSettings( migratedOptionAsAlt, migratedFloatingTerminalEnabled, migratedOsc52Clipboard, + migratedTerminalColorOverrides, migratedFloatingTerminalCwd, migratedFloatingTerminalTrustedCwds, osc52ClipboardNoticePending diff --git a/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.tsx b/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.tsx index 3d8b2a0ce3b..428e022a8c9 100644 --- a/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.tsx @@ -14,7 +14,7 @@ import { import { syncPreviewTerminalLigatures } from './preview-terminal-ligatures' import type { DashboardCardTerminalInput } from '../../../../shared/dashboard-snapshot' import { terminalPreviewUnavailableMessage } from './terminal-preview-unavailable-message' -import { getBuiltinTheme, resolveEffectiveTerminalAppearance } from '@/lib/terminal-theme' +import { resolveEffectiveTerminalAppearance } from '@/lib/terminal-theme' import { cn } from '@/lib/utils' import { useAppStore } from '@/store' import { createPreviewGridClaim } from './preview-grid-claim' @@ -65,10 +65,7 @@ export function AgentTerminalPreview({ return { terminalTheme: null, terminalMode: 'dark' as const } } const appearance = resolveEffectiveTerminalAppearance(settings, systemPrefersDark) - const theme = composeActiveTerminalTheme( - appearance.theme ?? getBuiltinTheme(appearance.themeName), - settings - ) + const theme = composeActiveTerminalTheme(appearance.theme, settings) return { terminalTheme: theme, terminalMode: appearance.mode } }, [settings, systemPrefersDark]) // Settings arrive as cloned snapshots; compare theme values before reconnecting. diff --git a/src/renderer/src/components/native-chat/native-chat-appearance-palette.test.ts b/src/renderer/src/components/native-chat/native-chat-appearance-palette.test.ts index 3c01c56c756..5314972bf94 100644 --- a/src/renderer/src/components/native-chat/native-chat-appearance-palette.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-appearance-palette.test.ts @@ -4,6 +4,7 @@ import { resolve } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { createGlobalSettingsFixture } from '../../../../shared/global-settings-test-fixture' import * as terminalThemeSelection from '../../../../shared/terminal-theme-selection' +import { TERMINAL_COLOR_OVERRIDES_SETTING } from '../../../../shared/terminal-color-overrides' import { nativeChatAppearanceStyle } from './native-chat-appearance-style' const css = readFileSync(resolve('src/renderer/src/assets/main.css'), 'utf8') @@ -31,7 +32,7 @@ function createChat(appScheme: 'light' | 'dark', terminalScheme: 'light' | 'dark const style = nativeChatAppearanceStyle( createGlobalSettingsFixture({ theme: appScheme, - terminalColorOverrides: + [TERMINAL_COLOR_OVERRIDES_SETTING[appScheme]]: terminalScheme === 'light' ? { background: '#ffffff', foreground: '#000000' } : { background: '#000000', foreground: '#ffffff' }, diff --git a/src/renderer/src/components/native-chat/native-chat-appearance-style.ts b/src/renderer/src/components/native-chat/native-chat-appearance-style.ts index e2b594158c0..07d8da15616 100644 --- a/src/renderer/src/components/native-chat/native-chat-appearance-style.ts +++ b/src/renderer/src/components/native-chat/native-chat-appearance-style.ts @@ -46,6 +46,7 @@ export function selectNativeChatAppearanceSettings( terminalUseSeparateLightTheme: settings?.terminalUseSeparateLightTheme, terminalCustomThemes: settings?.terminalCustomThemes, terminalColorOverrides: settings?.terminalColorOverrides, + terminalColorOverridesLight: settings?.terminalColorOverridesLight, terminalFontFamily: settings?.terminalFontFamily, terminalFontSize: settings?.nativeChatAppearance?.matchTerminalInterface ? settings.terminalFontSize @@ -107,7 +108,8 @@ export function nativeChatAppearanceStyle( terminalThemeLight: settings?.terminalThemeLight ?? '', terminalUseSeparateLightTheme: settings?.terminalUseSeparateLightTheme ?? false, terminalCustomThemes: settings?.terminalCustomThemes, - terminalColorOverrides: settings?.terminalColorOverrides + terminalColorOverrides: settings?.terminalColorOverrides, + terminalColorOverridesLight: settings?.terminalColorOverridesLight }, systemPrefersDark ) diff --git a/src/renderer/src/components/onboarding/ThemeStep.tsx b/src/renderer/src/components/onboarding/ThemeStep.tsx index 89390e95bde..29e4d2dce1f 100644 --- a/src/renderer/src/components/onboarding/ThemeStep.tsx +++ b/src/renderer/src/components/onboarding/ThemeStep.tsx @@ -10,6 +10,8 @@ import type { GhosttyImportPreview, GlobalSettings } from '../../../../shared/gl import type { DiscoveryStatusEmitted } from '../../../../shared/onboarding-state-types' import { i18n, translate } from '@/i18n/i18n' import { ChromePreview } from './theme-chrome-preview' +import { withImportedTerminalColorOverrides } from '../../../../shared/terminal-theme-selection' +import { getSystemPrefersDark } from '@/lib/terminal-theme' type ThemeStepProps = { theme: GlobalSettings['theme'] @@ -128,17 +130,9 @@ export function ThemeStep({ theme, onThemeChange, settings, updateSettings }: Th track('onboarding_ghostty_import_failed', { reason: 'empty_diff' }) return } - await updateSettings({ - ...resolved.diff, - ...(resolved.diff.terminalColorOverrides - ? { - terminalColorOverrides: { - ...settings.terminalColorOverrides, - ...resolved.diff.terminalColorOverrides - } - } - : {}) - }) + await updateSettings( + withImportedTerminalColorOverrides(settings, resolved.diff, getSystemPrefersDark()) + ) // Sync the controller’s local theme so Continue cannot overwrite the imported theme. if (resolved.diff.theme && mountedRef.current) { onThemeChange(resolved.diff.theme) diff --git a/src/renderer/src/components/plain-ssh-terminal/PlainSshTerminalView.tsx b/src/renderer/src/components/plain-ssh-terminal/PlainSshTerminalView.tsx index 60fac9e8330..41839e7c83e 100644 --- a/src/renderer/src/components/plain-ssh-terminal/PlainSshTerminalView.tsx +++ b/src/renderer/src/components/plain-ssh-terminal/PlainSshTerminalView.tsx @@ -11,7 +11,7 @@ import { translate } from '@/i18n/i18n' import { useAppStore } from '@/store' import { useEffectiveMacOptionAsAlt } from '@/lib/keyboard-layout/use-effective-mac-option-as-alt' import { composeActiveTerminalTheme } from '@/components/terminal-pane/terminal-appearance' -import { getBuiltinTheme, resolveEffectiveTerminalAppearance } from '@/lib/terminal-theme' +import { resolveEffectiveTerminalAppearance } from '@/lib/terminal-theme' import { useSystemPrefersDark } from '@/components/terminal-pane/use-system-prefers-dark' import { resolveTerminalMinimumContrastRatio } from '../../../../shared/terminal-contrast-correction' import { syncPreviewTerminalLigatures } from '@/components/dashboard-popout/preview-terminal-ligatures' @@ -71,10 +71,7 @@ function resolveThemeColors( } const appearance = resolveEffectiveTerminalAppearance(settings, systemPrefersDark) return { - theme: composeActiveTerminalTheme( - appearance.theme ?? getBuiltinTheme(appearance.themeName), - settings - ), + theme: composeActiveTerminalTheme(appearance.theme, settings), mode: appearance.mode } } diff --git a/src/renderer/src/components/plugins/page/PluginThemeUseNow.test.tsx b/src/renderer/src/components/plugins/page/PluginThemeUseNow.test.tsx index 508a7632ccb..c47033774b7 100644 --- a/src/renderer/src/components/plugins/page/PluginThemeUseNow.test.tsx +++ b/src/renderer/src/components/plugins/page/PluginThemeUseNow.test.tsx @@ -172,7 +172,8 @@ it.each([6, 60])('prepares only the clicked terminal palette among %i choices', terminalUseSeparateLightTheme: true, terminalPluginPalettes: [{ id: candidate.id, colors: candidate.terminal }] }) - expect(update).toHaveProperty('terminalColorOverrides', undefined) + // The light slot now has its own theme, so the dark slot's overrides stay for dark mode. + expect(update).not.toHaveProperty('terminalColorOverrides') expect(update).not.toHaveProperty('theme') expect(settings().terminalThemeLight).toBe(before.terminalThemeLight) expect( diff --git a/src/renderer/src/components/plugins/state/plugin-theme-apply.ts b/src/renderer/src/components/plugins/state/plugin-theme-apply.ts index 1e307fa2141..e4462d60779 100644 --- a/src/renderer/src/components/plugins/state/plugin-theme-apply.ts +++ b/src/renderer/src/components/plugins/state/plugin-theme-apply.ts @@ -5,6 +5,10 @@ import type { PluginTerminalThemeRegistration } from '../../../../../shared/plug import { translate } from '@/i18n/i18n' import { resolveDocumentTheme } from '@/lib/document-theme' import { pluginAppThemeSlotUpdate } from '../../../../../shared/plugin-theme-settings' +import { + hasTerminalColorOverrides, + TERMINAL_COLOR_OVERRIDES_SETTING +} from '../../../../../shared/terminal-color-overrides' import { withTerminalPluginPalettes } from '@/lib/terminal-plugin-palettes' import { useAppStore } from '@/store' import { PLUGIN_ACTION_TOAST_DURATION_MS } from './plugin-action-toast' @@ -62,13 +66,14 @@ export function terminalThemeUpdate( dark: boolean, terminalThemes: readonly PluginTerminalThemeRegistration[] ): Partial { + const slot = terminalSlot(theme.mode, dark) === 'terminalThemeLight' ? 'light' : 'dark' return withTerminalPluginPalettes( settings, { ...terminalThemeSlotUpdate(theme, dark), - // A selected theme must replace overrides or they mask its palette. - ...(Object.keys(settings.terminalColorOverrides ?? {}).length > 0 - ? { terminalColorOverrides: undefined } + // A selected theme must replace its slot's overrides or they mask its palette. + ...(hasTerminalColorOverrides(settings, slot) + ? { [TERMINAL_COLOR_OVERRIDES_SETTING[slot]]: undefined } : {}) }, terminalThemes @@ -125,6 +130,7 @@ export async function applyPluginThemesNow(pluginKey: string): Promise terminalThemeLight: settings.terminalThemeLight, terminalUseSeparateLightTheme: settings.terminalUseSeparateLightTheme, terminalColorOverrides: settings.terminalColorOverrides, + terminalColorOverridesLight: settings.terminalColorOverridesLight, terminalPluginPalettes: settings.terminalPluginPalettes ?? [] } let next: Partial = {} diff --git a/src/renderer/src/components/settings/AgentDefaultSurfacePreviews.tsx b/src/renderer/src/components/settings/AgentDefaultSurfacePreviews.tsx index 082da923e2e..232a8ed30c5 100644 --- a/src/renderer/src/components/settings/AgentDefaultSurfacePreviews.tsx +++ b/src/renderer/src/components/settings/AgentDefaultSurfacePreviews.tsx @@ -158,7 +158,10 @@ export function TerminalSurfacePreview({ settings }: { settings: GlobalSettings const systemPrefersDark = useSystemPrefersDark() const customThemes = JSON.stringify(settings.terminalCustomThemes) const pluginPalettes = JSON.stringify(settings.terminalPluginPalettes) - const colorOverrides = JSON.stringify(settings.terminalColorOverrides) + const colorOverrides = JSON.stringify([ + settings.terminalColorOverrides, + settings.terminalColorOverridesLight + ]) const theme = useMemo( () => composeActiveTerminalTheme( diff --git a/src/renderer/src/components/settings/TerminalAppearanceSection.tsx b/src/renderer/src/components/settings/TerminalAppearanceSection.tsx index 5032fd40eec..3cbde715000 100644 --- a/src/renderer/src/components/settings/TerminalAppearanceSection.tsx +++ b/src/renderer/src/components/settings/TerminalAppearanceSection.tsx @@ -1,5 +1,6 @@ -import { useState } from 'react' +import { useState, type ReactNode } from 'react' import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { TerminalThemeSlot } from '../../../../shared/terminal-color-overrides' import { matchesSettingsSearch, normalizeSettingsSearchQuery, @@ -49,8 +50,6 @@ type TerminalAppearanceSectionProps = { forceVisiblePrimary?: boolean } -type TerminalThemeTarget = 'dark' | 'light' - function scoreThemeTargetIntent(searchQuery: string, entries: SettingsSearchEntry[]): number { // Infer dark/light target from labels/aliases, not incidental description mentions. return scoreSettingsSearch( @@ -62,7 +61,7 @@ function scoreThemeTargetIntent(searchQuery: string, entries: SettingsSearchEntr function getPreferredThemeTarget( darkThemeSearchScore: number, lightThemeSearchScore: number -): TerminalThemeTarget | undefined { +): TerminalThemeSlot | undefined { if (darkThemeSearchScore === lightThemeSearchScore) { return undefined } @@ -127,47 +126,58 @@ export function TerminalAppearanceSection({ showDesktopThemeImports && (!isSearching || forceVisiblePrimary || ghosttyImportMatches) const showTypographyAdvancedDisclosure = !isSearching || typographyMatches - const advancedGroups = [ - cursorMatches - ? { - key: 'cursor', - node: ( - - ) - } - : null, - paneMatches - ? { - key: 'pane', - node: ( - - ) - } - : null, - windowMatches - ? { - key: 'window', - node: - } - : null - ].filter((group): group is { key: string; node: React.JSX.Element } => group !== null) - const showAdvancedDisclosure = !isSearching || advancedGroups.length > 0 - const previewAdvancedContent = showAdvancedDisclosure ? ( - - {advancedGroups.map((group, index) => ( -
0 ? 'mt-2 border-t border-border/60 pt-4' : undefined} - > - {group.node} -
- ))} -
- ) : null + const advancedGroups = (colorOverrideSlot: TerminalThemeSlot) => + [ + cursorMatches + ? { + key: 'cursor', + node: ( + + ) + } + : null, + paneMatches + ? { + key: 'pane', + node: ( + + ) + } + : null, + windowMatches + ? { + key: 'window', + node: ( + + ) + } + : null + ].filter((group): group is { key: string; node: React.JSX.Element } => group !== null) + const showAdvancedDisclosure = !isSearching || cursorMatches || paneMatches || windowMatches + const renderPreviewAdvancedContent = (colorOverrideSlot: TerminalThemeSlot): ReactNode => + showAdvancedDisclosure ? ( + + {advancedGroups(colorOverrideSlot).map((group, index) => ( +
0 ? 'mt-2 border-t border-border/60 pt-4' : undefined} + > + {group.node} +
+ ))} +
+ ) : null return (
@@ -260,7 +270,7 @@ export function TerminalAppearanceSection({ warpThemes={warpThemes} showThemeImport={showDesktopThemeImports} preferredTarget={preferredThemeTarget} - advancedContent={previewAdvancedContent} + renderAdvancedContent={renderPreviewAdvancedContent} /> ) : null} diff --git a/src/renderer/src/components/settings/TerminalSettingsPreview.tsx b/src/renderer/src/components/settings/TerminalSettingsPreview.tsx index 04ed04ea3be..50f83742417 100644 --- a/src/renderer/src/components/settings/TerminalSettingsPreview.tsx +++ b/src/renderer/src/components/settings/TerminalSettingsPreview.tsx @@ -90,6 +90,8 @@ export function TerminalSettingsPreview({ settings.terminalUseSeparateLightTheme, settings.terminalDividerColorDark, settings.terminalDividerColorLight, + settings.terminalColorOverrides, + settings.terminalColorOverridesLight, systemPrefersDark ] ) @@ -98,12 +100,7 @@ export function TerminalSettingsPreview({ const composedTheme = useMemo( () => composeActiveTerminalTheme(appearance.theme, settings), // oxlint-disable-next-line react-hooks/exhaustive-deps - [ - appearance, - settings.terminalColorOverrides, - settings.terminalBackgroundOpacity, - settings.terminalCursorOpacity - ] + [appearance, settings.terminalBackgroundOpacity, settings.terminalCursorOpacity] ) const dividerThicknessPx = clampNumber(settings.terminalDividerThicknessPx, 1, 32) diff --git a/src/renderer/src/components/settings/TerminalThemeSections.lifecycle.test.ts b/src/renderer/src/components/settings/TerminalThemeSections.lifecycle.test.ts index 462e05c9526..3cd6ccf84fb 100644 --- a/src/renderer/src/components/settings/TerminalThemeSections.lifecycle.test.ts +++ b/src/renderer/src/components/settings/TerminalThemeSections.lifecycle.test.ts @@ -234,12 +234,13 @@ describe('TerminalThemeCatalogSection', () => { }) }) - it('clears imported color overrides when selecting a light theme', () => { + it('clears only the light slot overrides when selecting a light theme', () => { const updateSettings = vi.fn() const element = renderCatalog( makeSettings({ terminalUseSeparateLightTheme: true, - terminalColorOverrides: { background: '#111111' } + terminalColorOverrides: { background: '#111111' }, + terminalColorOverridesLight: { background: '#eeeeee' } }), updateSettings, 'light' @@ -251,7 +252,7 @@ describe('TerminalThemeCatalogSection', () => { expect(updateSettings.mock.calls[0]?.[0]).toStrictEqual({ terminalThemeLight: 'GitHub Light', - terminalColorOverrides: undefined + terminalColorOverridesLight: undefined }) }) diff --git a/src/renderer/src/components/settings/TerminalThemeSections.tsx b/src/renderer/src/components/settings/TerminalThemeSections.tsx index abea84bc6f9..8309fafdc39 100644 --- a/src/renderer/src/components/settings/TerminalThemeSections.tsx +++ b/src/renderer/src/components/settings/TerminalThemeSections.tsx @@ -2,6 +2,11 @@ import { usePluginTerminalThemes } from '@/store/plugin-themes' import { withTerminalPluginPalettes } from '@/lib/terminal-plugin-palettes' import { useState, type Dispatch, type ReactNode, type SetStateAction } from 'react' import type { GlobalSettings } from '../../../../shared/global-settings-types' +import { + hasTerminalColorOverrides, + TERMINAL_COLOR_OVERRIDES_SETTING, + type TerminalThemeSlot +} from '../../../../shared/terminal-color-overrides' import { ColorField, SettingsSegmentedControl, @@ -23,11 +28,9 @@ import { import { translate } from '@/i18n/i18n' import { cn } from '@/lib/utils' -type TerminalThemeTarget = 'dark' | 'light' +let lastEditedTerminalThemeTarget: TerminalThemeSlot | null = null -let lastEditedTerminalThemeTarget: TerminalThemeTarget | null = null - -function rememberTerminalThemeTarget(target: TerminalThemeTarget): void { +function rememberTerminalThemeTarget(target: TerminalThemeSlot): void { lastEditedTerminalThemeTarget = target } @@ -43,8 +46,8 @@ function isCustomizedTheme(themeName: string, defaultThemeName: string): boolean function getInitialTerminalThemeTarget( settings: GlobalSettings, systemPrefersDark: boolean, - preferredTarget?: TerminalThemeTarget -): TerminalThemeTarget { + preferredTarget?: TerminalThemeSlot +): TerminalThemeSlot { if (preferredTarget) { return preferredTarget } @@ -73,8 +76,8 @@ type TerminalThemeCatalogSectionProps = { importedHighlightSignal: number warpThemes: UseWarpThemeImportReturn showThemeImport: boolean - preferredTarget?: TerminalThemeTarget - advancedContent?: ReactNode + preferredTarget?: TerminalThemeSlot + renderAdvancedContent?: (colorOverrideSlot: TerminalThemeSlot) => ReactNode } export function TerminalThemeCatalogSection({ @@ -88,12 +91,12 @@ export function TerminalThemeCatalogSection({ warpThemes, showThemeImport, preferredTarget, - advancedContent + renderAdvancedContent }: TerminalThemeCatalogSectionProps): React.JSX.Element { - const [target, setTargetState] = useState(() => + const [target, setTargetState] = useState(() => getInitialTerminalThemeTarget(settings, systemPrefersDark, preferredTarget) ) - const setTarget = (nextTarget: TerminalThemeTarget): void => { + const setTarget = (nextTarget: TerminalThemeSlot): void => { rememberTerminalThemeTarget(nextTarget) setTargetState(nextTarget) } @@ -103,6 +106,8 @@ export function TerminalThemeCatalogSection({ const matchDarkMode = !settings.terminalUseSeparateLightTheme const lightModeMatchesDark = isLightTarget && matchDarkMode const showCustomControls = !lightModeMatchesDark + // Light mode that matches dark paints the dark slot, so its overrides are the ones the preview shows. + const advancedContent = renderAdvancedContent?.(lightModeMatchesDark ? 'dark' : target) const selectedTheme = isLightTarget ? settings.terminalThemeLight : settings.terminalThemeDark const pickerTitle = isLightTarget ? translate('auto.components.settings.TerminalThemeSections.8273bc75d7', 'Light Theme') @@ -254,15 +259,15 @@ export function TerminalThemeCatalogSection({ onQueryChange={setThemeSearch} onSelectTheme={(theme) => { rememberTerminalThemeTarget(target) - // A selected theme must replace overrides or they mask the picker. - const hasColorOverrides = - Object.keys(settings.terminalColorOverrides ?? {}).length > 0 + // A selected theme must replace its slot's overrides or they mask the picker. updateSettings( withTerminalPluginPalettes( settings, { [isLightTarget ? 'terminalThemeLight' : 'terminalThemeDark']: theme, - ...(hasColorOverrides ? { terminalColorOverrides: undefined } : {}) + ...(hasTerminalColorOverrides(settings, target) + ? { [TERMINAL_COLOR_OVERRIDES_SETTING[target]]: undefined } + : {}) }, pluginTerminalThemes ) diff --git a/src/renderer/src/components/settings/TerminalWindowSection.tsx b/src/renderer/src/components/settings/TerminalWindowSection.tsx index 4e861cb77f0..72b04b34614 100644 --- a/src/renderer/src/components/settings/TerminalWindowSection.tsx +++ b/src/renderer/src/components/settings/TerminalWindowSection.tsx @@ -1,6 +1,11 @@ import { useRef, useState } from 'react' import { RotateCw } from 'lucide-react' import type { GlobalSettings } from '../../../../shared/global-settings-types' +import { + TERMINAL_COLOR_OVERRIDES_SETTING, + terminalColorOverridesForSlot, + type TerminalThemeSlot +} from '../../../../shared/terminal-color-overrides' import { Button } from '../ui/button' import { Label } from '../ui/label' import { Switch } from '../ui/switch' @@ -10,18 +15,23 @@ import { clampNumber } from '@/lib/terminal-theme' import { useMountedRef } from '@/hooks/useMountedRef' import { translate } from '@/i18n/i18n' +import { COLOR_OVERRIDE_GROUPS } from './terminal-window-color-groups' + type TerminalWindowSectionProps = { settings: GlobalSettings updateSettings: (updates: Partial) => void + /** Theme slot the Color Overrides editor writes, following the theme catalog's Dark/Light target. */ + colorOverrideSlot: TerminalThemeSlot } -import { COLOR_OVERRIDE_GROUPS } from './terminal-window-color-groups' - export function TerminalWindowSection({ settings, - updateSettings + updateSettings, + colorOverrideSlot }: TerminalWindowSectionProps): React.JSX.Element { const [colorOverridesExpanded, setColorOverridesExpanded] = useState(false) + const colorOverridesSetting = TERMINAL_COLOR_OVERRIDES_SETTING[colorOverrideSlot] + const colorOverrides = terminalColorOverridesForSlot(settings, colorOverrideSlot) // Blur is a startup window option on macOS/Windows; compare against mount state to request restart only after changes. const blurAtMountRef = useRef(settings.windowBackgroundBlur ?? false) const blurPendingRestart = (settings.windowBackgroundBlur ?? false) !== blurAtMountRef.current @@ -283,6 +293,17 @@ export function TerminalWindowSection({ }`} >
+

+ {colorOverrideSlot === 'light' + ? translate( + 'auto.components.settings.TerminalWindowSection.color_overrides_light_description', + 'Override individual colors of the light terminal theme.' + ) + : translate( + 'auto.components.settings.TerminalWindowSection.color_overrides_dark_description', + 'Override individual colors of the dark terminal theme.' + )} +

{COLOR_OVERRIDE_GROUPS.map((group) => (

{group.label}

@@ -292,12 +313,12 @@ export function TerminalWindowSection({ key={item.key} label={item.label} description={item.description} - value={settings.terminalColorOverrides?.[item.key] ?? ''} + value={colorOverrides?.[item.key] ?? ''} fallback="" onChange={(value) => updateSettings({ - terminalColorOverrides: { - ...settings.terminalColorOverrides, + [colorOverridesSetting]: { + ...colorOverrides, [item.key]: value || undefined } }) @@ -310,12 +331,17 @@ export function TerminalWindowSection({
diff --git a/src/renderer/src/components/settings/useGhosttyImport.ts b/src/renderer/src/components/settings/useGhosttyImport.ts index 2b08694e465..afe6327feca 100644 --- a/src/renderer/src/components/settings/useGhosttyImport.ts +++ b/src/renderer/src/components/settings/useGhosttyImport.ts @@ -1,6 +1,8 @@ import { useState } from 'react' import type { GhosttyImportPreview, GlobalSettings } from '../../../../shared/global-settings-types' import { useMountedRef } from '../../hooks/useMountedRef' +import { withImportedTerminalColorOverrides } from '../../../../shared/terminal-theme-selection' +import { getSystemPrefersDark } from '@/lib/terminal-theme' export type UseGhosttyImportReturn = { open: boolean @@ -49,17 +51,11 @@ export function useGhosttyImport( if (applied || !preview?.found || Object.keys(preview.diff).length === 0 || !settings) { return } - const merged = { - ...preview.diff, - ...(preview.diff.terminalColorOverrides - ? { - terminalColorOverrides: { - ...settings.terminalColorOverrides, - ...preview.diff.terminalColorOverrides - } - } - : {}) - } + const merged = withImportedTerminalColorOverrides( + settings, + preview.diff, + getSystemPrefersDark() + ) setApplyError(null) try { // Await persistence before marking applied so rejected IPC cannot report success. diff --git a/src/renderer/src/components/terminal-pane/compose-active-terminal-theme.test.ts b/src/renderer/src/components/terminal-pane/compose-active-terminal-theme.test.ts index ecad60de458..4e1b1064168 100644 --- a/src/renderer/src/components/terminal-pane/compose-active-terminal-theme.test.ts +++ b/src/renderer/src/components/terminal-pane/compose-active-terminal-theme.test.ts @@ -5,7 +5,6 @@ import type { GlobalSettings } from '../../../../shared/global-settings-types' describe('composeActiveTerminalTheme', () => { function settingsWith(partial: Partial): GlobalSettings { return { - terminalColorOverrides: undefined, terminalCursorOpacity: undefined, terminalBackgroundOpacity: undefined, ...partial @@ -38,16 +37,6 @@ describe('composeActiveTerminalTheme', () => { expect(result!.scrollbarSliderBackground).toBe('rgba(1, 2, 3, 0.4)') }) - it('layers terminalColorOverrides on top of the base theme', () => { - const base = { background: '#101010', foreground: '#fafafa' } - const result = composeActiveTerminalTheme( - base, - settingsWith({ terminalColorOverrides: { foreground: '#00ff00' } }) - ) - expect(result!.foreground).toBe('#00ff00') - expect(result!.background).toBe('#101010') - }) - it('applies background opacity by converting the hex background to rgba', () => { const base = { background: '#112233' } const result = composeActiveTerminalTheme( diff --git a/src/renderer/src/components/terminal-pane/terminal-appearance.ts b/src/renderer/src/components/terminal-pane/terminal-appearance.ts index ad45cf83a4b..5d98b750ca3 100644 --- a/src/renderer/src/components/terminal-pane/terminal-appearance.ts +++ b/src/renderer/src/components/terminal-pane/terminal-appearance.ts @@ -3,11 +3,7 @@ import type { PaneManager } from '@/lib/pane-manager/pane-manager' import type { GlobalSettings } from '../../../../shared/global-settings-types' import { resolveTerminalFontWeights } from '../../../../shared/terminal-fonts' import { resolveTerminalLigaturesEnabled } from '../../../../shared/terminal-ligatures' -import { - getBuiltinTheme, - resolvePaneStyleOptions, - resolveEffectiveTerminalAppearance -} from '@/lib/terminal-theme' +import { resolvePaneStyleOptions, resolveEffectiveTerminalAppearance } from '@/lib/terminal-theme' import { buildFontFamily } from '@/lib/monospace-font-family' import { safeFit, safeFitAndThen } from '@/lib/pane-manager/pane-tree-ops' import { canApplyPaneMetricOptions } from '@/lib/pane-manager/pane-fit' @@ -50,13 +46,11 @@ export function isHexColor(value: string): boolean { return HEX_COLOR_RE.test(value) } -// Keep pure so settings preview can compose the same theme without PaneManager. +// Keep pure so settings preview can compose the same theme without PaneManager. baseTheme is the resolved palette +// (resolveEffectiveTerminalAppearance), which already carries the slot's color overrides. export function composeActiveTerminalTheme( baseTheme: ITheme | null, - settings: Pick< - GlobalSettings, - 'terminalColorOverrides' | 'terminalBackgroundOpacity' | 'terminalCursorOpacity' - > + settings: Pick ): ITheme | null { if (!baseTheme) { return null @@ -70,9 +64,6 @@ export function composeActiveTerminalTheme( scrollbarSliderActiveBackground: 'rgba(180, 180, 185, 0.8)', ...baseTheme } - if (settings.terminalColorOverrides) { - theme = { ...theme, ...settings.terminalColorOverrides } - } // Why: convert the hex background to rgba so xterm honors the opacity when allowTransparency is set. if (settings.terminalBackgroundOpacity !== undefined && theme.background) { theme = { @@ -100,8 +91,7 @@ export function publishTerminalViewAttributesAtAppStart( return false } const appearance = resolveEffectiveTerminalAppearance(settings, systemPrefersDark) - const baseTheme: ITheme | null = appearance.theme ?? getBuiltinTheme(appearance.themeName) - const theme = composeActiveTerminalTheme(baseTheme, settings) + const theme = composeActiveTerminalTheme(appearance.theme, settings) return send !== undefined ? publishTerminalViewAttributes(theme, appearance.mode, settings, send) : publishTerminalViewAttributes(theme, appearance.mode, settings) @@ -144,8 +134,7 @@ export function applyTerminalAppearance( ): void { const appearance = resolveEffectiveTerminalAppearance(settings, systemPrefersDark) const paneStyles = resolvePaneStyleOptions(settings) - const baseTheme: ITheme | null = appearance.theme ?? getBuiltinTheme(appearance.themeName) - const theme = composeActiveTerminalTheme(baseTheme, settings) + const theme = composeActiveTerminalTheme(appearance.theme, settings) publishTerminalViewAttributes(theme, appearance.mode, settings) const paneBackground = theme?.background ?? '#000000' diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-projection.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-projection.ts index 454781b80e2..1128a33523a 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-projection.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-projection.ts @@ -59,8 +59,7 @@ export function useTerminalPaneProjection(controller: TerminalPaneMobileControll const effectiveAppearance = settings ? resolveEffectiveTerminalAppearance(settings, systemPrefersDark) : null - const terminalBackground = - settings?.terminalColorOverrides?.background ?? effectiveAppearance?.theme?.background + const terminalBackground = effectiveAppearance?.theme?.background const titleUsesLightSurface = isTerminalBackgroundLight(terminalBackground, { appSurface: effectiveAppearance?.mode, backgroundOpacity: settings?.terminalBackgroundOpacity diff --git a/src/renderer/src/i18n/locales/de.json b/src/renderer/src/i18n/locales/de.json index 5c1799e74aa..2de32463a75 100644 --- a/src/renderer/src/i18n/locales/de.json +++ b/src/renderer/src/i18n/locales/de.json @@ -9324,7 +9324,6 @@ }, "TerminalWindowSection": { "1705318506": "ANSI-Farbe Magenta", - "03c855d15f": "Alle Farbanpassungen zurücksetzen", "63f8d9336e": "Farbanpassungen", "e86e09b5c7": "Einzelne Terminalfarben anpassen.", "1d1920dc8a": "Den Mauszeiger beim Tippen im Terminal ausblenden.", @@ -9391,7 +9390,11 @@ "cf37ff69f6": "Basis", "8abdab9f7c": "Jetzt neu starten", "907131d741": "Wird neu gestartet…", - "605a35d600": "Wird vom Terminal-Renderer noch nicht angewendet — xterm.js hat keine Farboption für fetten Text. Ein gespeicherter Wert bleibt erhalten." + "605a35d600": "Wird vom Terminal-Renderer noch nicht angewendet — xterm.js hat keine Farboption für fetten Text. Ein gespeicherter Wert bleibt erhalten.", + "color_overrides_dark_description": "Einzelne Farben des dunklen Terminal-Themes anpassen.", + "color_overrides_light_description": "Einzelne Farben des hellen Terminal-Themes anpassen.", + "reset_dark_color_overrides": "Farbanpassungen des dunklen Themes zurücksetzen", + "reset_light_color_overrides": "Farbanpassungen des hellen Themes zurücksetzen" }, "UIZoomControl": { "c2c64b24d0": "Zurücksetzen" diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 23cbc5a1fec..26b9fecc52b 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -9404,7 +9404,6 @@ }, "TerminalWindowSection": { "1705318506": "ANSI magenta color", - "03c855d15f": "Reset all color overrides", "63f8d9336e": "Color Overrides", "e86e09b5c7": "Override individual terminal colors.", "1d1920dc8a": "Hide the mouse cursor when typing in the terminal.", @@ -9471,7 +9470,11 @@ "cf37ff69f6": "Base", "8abdab9f7c": "Restart now", "907131d741": "Restarting…", - "605a35d600": "Not applied by the terminal renderer yet — xterm.js has no bold color slot. A saved value is preserved." + "605a35d600": "Not applied by the terminal renderer yet — xterm.js has no bold color slot. A saved value is preserved.", + "color_overrides_dark_description": "Override individual colors of the dark terminal theme.", + "color_overrides_light_description": "Override individual colors of the light terminal theme.", + "reset_dark_color_overrides": "Reset dark color overrides", + "reset_light_color_overrides": "Reset light color overrides" }, "UIZoomControl": { "c2c64b24d0": "Reset" diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 4d25bb3f919..970b789f0e7 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -8930,7 +8930,6 @@ }, "TerminalWindowSection": { "1705318506": "Color magenta ANSI", - "03c855d15f": "Restablecer todas las anulaciones de color", "63f8d9336e": "Anulaciones de color", "e86e09b5c7": "Anula colores individuales de la terminal.", "1d1920dc8a": "Oculta el cursor del mouse al escribir en la terminal.", @@ -8997,7 +8996,11 @@ "cf37ff69f6": "Base", "8abdab9f7c": "Reiniciar ahora", "907131d741": "Reiniciando…", - "605a35d600": "El renderizador de la terminal aún no aplica este valor: xterm.js no tiene un ajuste de color para el texto en negrita. Se conserva el valor guardado." + "605a35d600": "El renderizador de la terminal aún no aplica este valor: xterm.js no tiene un ajuste de color para el texto en negrita. Se conserva el valor guardado.", + "color_overrides_dark_description": "Anula colores individuales del tema oscuro de la terminal.", + "color_overrides_light_description": "Anula colores individuales del tema claro de la terminal.", + "reset_dark_color_overrides": "Restablecer las anulaciones de color del tema oscuro", + "reset_light_color_overrides": "Restablecer las anulaciones de color del tema claro" }, "UIZoomControl": { "c2c64b24d0": "Restablecer" diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 583f4fd0bc7..a48d6e3233a 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -9200,7 +9200,6 @@ }, "TerminalWindowSection": { "1705318506": "Couleur ANSI magenta", - "03c855d15f": "Réinitialiser toutes les surcharges de couleurs", "63f8d9336e": "Surcharges de couleurs", "e86e09b5c7": "Remplacer individuellement les couleurs du terminal.", "1d1920dc8a": "Masquer le curseur de la souris pendant la saisie dans le terminal.", @@ -9267,7 +9266,11 @@ "cf37ff69f6": "Base", "8abdab9f7c": "Redémarrer maintenant", "907131d741": "Redémarrage…", - "605a35d600": "Non encore appliqué par le moteur de rendu du terminal — xterm.js n'a pas d'emplacement dédié aux couleurs en gras. Une valeur enregistrée est conservée." + "605a35d600": "Non encore appliqué par le moteur de rendu du terminal — xterm.js n'a pas d'emplacement dédié aux couleurs en gras. Une valeur enregistrée est conservée.", + "color_overrides_dark_description": "Remplacer individuellement les couleurs du thème de terminal sombre.", + "color_overrides_light_description": "Remplacer individuellement les couleurs du thème de terminal clair.", + "reset_dark_color_overrides": "Réinitialiser les surcharges de couleurs du thème sombre", + "reset_light_color_overrides": "Réinitialiser les surcharges de couleurs du thème clair" }, "UIZoomControl": { "c2c64b24d0": "Réinitialiser" diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 405316e30cc..88fe43b3499 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -8672,7 +8672,6 @@ }, "TerminalWindowSection": { "1705318506": "ANSIマゼンタ色", - "03c855d15f": "すべての色のオーバーライドをリセット", "63f8d9336e": "色のオーバーライド", "e86e09b5c7": "個々のターミナルの色をオーバーライドします。", "1d1920dc8a": "ターミナルに入力するときにマウスカーソルを非表示にします。", @@ -8739,7 +8738,11 @@ "cf37ff69f6": "ベース", "8abdab9f7c": "今すぐ再起動", "907131d741": "再起動中…", - "605a35d600": "ターミナルのレンダラーではまだ適用されません。xterm.js に太字用の色スロットがないためです。保存した値は保持されます。" + "605a35d600": "ターミナルのレンダラーではまだ適用されません。xterm.js に太字用の色スロットがないためです。保存した値は保持されます。", + "color_overrides_dark_description": "ダークターミナルテーマの個々の色をオーバーライドします。", + "color_overrides_light_description": "ライトターミナルテーマの個々の色をオーバーライドします。", + "reset_dark_color_overrides": "ダークテーマの色のオーバーライドをリセット", + "reset_light_color_overrides": "ライトテーマの色のオーバーライドをリセット" }, "UIZoomControl": { "c2c64b24d0": "リセット" diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 6f5b933eee7..c85631a3ae8 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -8635,7 +8635,6 @@ }, "TerminalWindowSection": { "1705318506": "ANSI 마젠타색", - "03c855d15f": "모든 색상 재정의 재설정", "63f8d9336e": "색상 재정의", "e86e09b5c7": "개별 터미널 색상을 재정의합니다.", "1d1920dc8a": "terminal에 입력할 때 마우스 커서를 숨깁니다.", @@ -8702,7 +8701,11 @@ "cf37ff69f6": "기본", "8abdab9f7c": "지금 재시작", "907131d741": "다시 시작하는 중…", - "605a35d600": "아직 터미널 렌더러에는 적용되지 않습니다. xterm.js에 굵은 글씨 색상 슬롯이 없기 때문입니다. 저장된 값은 유지됩니다." + "605a35d600": "아직 터미널 렌더러에는 적용되지 않습니다. xterm.js에 굵은 글씨 색상 슬롯이 없기 때문입니다. 저장된 값은 유지됩니다.", + "color_overrides_dark_description": "다크 터미널 테마의 개별 색상을 재정의합니다.", + "color_overrides_light_description": "라이트 터미널 테마의 개별 색상을 재정의합니다.", + "reset_dark_color_overrides": "다크 테마 색상 재정의 재설정", + "reset_light_color_overrides": "라이트 테마 색상 재정의 재설정" }, "UIZoomControl": { "c2c64b24d0": "재설정" diff --git a/src/renderer/src/i18n/locales/pt.json b/src/renderer/src/i18n/locales/pt.json index 475c5445e2c..a1a4207cb99 100644 --- a/src/renderer/src/i18n/locales/pt.json +++ b/src/renderer/src/i18n/locales/pt.json @@ -5177,7 +5177,6 @@ }, "TerminalWindowSection": { "1705318506": "Cor magenta ANSI", - "03c855d15f": "Redefinir todas as substituições de cor", "63f8d9336e": "Substituições de Cor", "e86e09b5c7": "Substitui cores individuais do terminal.", "1d1920dc8a": "Oculta o cursor do mouse ao digitar no terminal.", @@ -5244,7 +5243,11 @@ "cf37ff69f6": "Base", "8abdab9f7c": "Reiniciar agora", "907131d741": "Reiniciando…", - "605a35d600": "Ainda não aplicado pelo renderizador do terminal — o xterm.js não tem um slot de cor para negrito. Um valor salvo é preservado." + "605a35d600": "Ainda não aplicado pelo renderizador do terminal — o xterm.js não tem um slot de cor para negrito. Um valor salvo é preservado.", + "color_overrides_dark_description": "Substitui cores individuais do tema escuro do terminal.", + "color_overrides_light_description": "Substitui cores individuais do tema claro do terminal.", + "reset_dark_color_overrides": "Redefinir as substituições de cor do tema escuro", + "reset_light_color_overrides": "Redefinir as substituições de cor do tema claro" }, "agents": { "search": { diff --git a/src/renderer/src/i18n/locales/ru.json b/src/renderer/src/i18n/locales/ru.json index 7478b03ec91..38b9824ec75 100644 --- a/src/renderer/src/i18n/locales/ru.json +++ b/src/renderer/src/i18n/locales/ru.json @@ -9682,7 +9682,6 @@ }, "TerminalWindowSection": { "1705318506": "Пурпурный цвет ANSI", - "03c855d15f": "Сбросить все переопределения цветов", "63f8d9336e": "Переопределения цветов", "e86e09b5c7": "Измените отдельные цвета терминала.", "1d1920dc8a": "Скрывать указатель мыши при вводе в терминале.", @@ -9749,7 +9748,11 @@ "cf37ff69f6": "Основные", "8abdab9f7c": "Перезапустить сейчас", "907131d741": "Перезапуск…", - "605a35d600": "Пока не применяется при отрисовке терминала: в xterm.js нет отдельного цвета жирного текста. Сохранённое значение сохраняется." + "605a35d600": "Пока не применяется при отрисовке терминала: в xterm.js нет отдельного цвета жирного текста. Сохранённое значение сохраняется.", + "color_overrides_dark_description": "Измените отдельные цвета тёмной темы терминала.", + "color_overrides_light_description": "Измените отдельные цвета светлой темы терминала.", + "reset_dark_color_overrides": "Сбросить переопределения цветов тёмной темы", + "reset_light_color_overrides": "Сбросить переопределения цветов светлой темы" }, "UIZoomControl": { "c2c64b24d0": "Сбросить" diff --git a/src/renderer/src/i18n/locales/tr.json b/src/renderer/src/i18n/locales/tr.json index 66b7a9ee9a1..95eb1cbf0ac 100644 --- a/src/renderer/src/i18n/locales/tr.json +++ b/src/renderer/src/i18n/locales/tr.json @@ -9310,7 +9310,6 @@ }, "TerminalWindowSection": { "1705318506": "ANSI eflatun rengi", - "03c855d15f": "Tüm renk geçersiz kılmalarını sıfırla", "63f8d9336e": "Renk Geçersiz Kılmaları", "e86e09b5c7": "Tek tek terminal renklerini geçersiz kılın.", "1d1920dc8a": "Terminalde yazarken fare imlecini gizleyin.", @@ -9377,7 +9376,11 @@ "cf37ff69f6": "Temel", "8abdab9f7c": "Şimdi yeniden başlat", "907131d741": "Yeniden başlatılıyor…", - "605a35d600": "Terminal render motoru tarafından henüz uygulanmıyor; xterm.js'de kalın renk yuvası yok. Kaydedilen değer korunur." + "605a35d600": "Terminal render motoru tarafından henüz uygulanmıyor; xterm.js'de kalın renk yuvası yok. Kaydedilen değer korunur.", + "color_overrides_dark_description": "Koyu terminal temasının renklerini tek tek geçersiz kılın.", + "color_overrides_light_description": "Açık terminal temasının renklerini tek tek geçersiz kılın.", + "reset_dark_color_overrides": "Koyu tema renk geçersiz kılmalarını sıfırla", + "reset_light_color_overrides": "Açık tema renk geçersiz kılmalarını sıfırla" }, "UIZoomControl": { "c2c64b24d0": "Sıfırla" diff --git a/src/renderer/src/i18n/locales/zh-Hant.json b/src/renderer/src/i18n/locales/zh-Hant.json index d525c86078f..cd67ec16b89 100644 --- a/src/renderer/src/i18n/locales/zh-Hant.json +++ b/src/renderer/src/i18n/locales/zh-Hant.json @@ -9147,7 +9147,6 @@ }, "TerminalWindowSection": { "1705318506": "ANSI 洋紅色", - "03c855d15f": "重設所有顏色覆寫設定", "63f8d9336e": "顏色覆寫設定", "e86e09b5c7": "覆寫個別終端機顏色。", "1d1920dc8a": "在終端機中輸入時隱藏滑鼠游標。", @@ -9214,7 +9213,11 @@ "cf37ff69f6": "基本", "8abdab9f7c": "立即重新啟動", "907131d741": "正在重新啟動...", - "605a35d600": "終端機算繪器尚未套用此設定,因為 xterm.js 沒有粗體顏色欄位。已儲存的值會保留。" + "605a35d600": "終端機算繪器尚未套用此設定,因為 xterm.js 沒有粗體顏色欄位。已儲存的值會保留。", + "color_overrides_dark_description": "覆寫深色終端機主題的個別顏色。", + "color_overrides_light_description": "覆寫淺色終端機主題的個別顏色。", + "reset_dark_color_overrides": "重設深色主題顏色覆寫設定", + "reset_light_color_overrides": "重設淺色主題顏色覆寫設定" }, "UIZoomControl": { "c2c64b24d0": "重設" diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 65af291409e..d84c2bccad1 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -8635,7 +8635,6 @@ }, "TerminalWindowSection": { "1705318506": "ANSI 洋红色", - "03c855d15f": "重置所有颜色覆盖", "63f8d9336e": "颜色覆盖", "e86e09b5c7": "覆盖各个终端颜色。", "1d1920dc8a": "在终端中输入时隐藏鼠标光标。", @@ -8702,7 +8701,11 @@ "cf37ff69f6": "根据", "8abdab9f7c": "立即重启", "907131d741": "正在重新启动...", - "605a35d600": "终端渲染器尚未应用此项 — xterm.js 没有粗体颜色槽位。已保存的值会被保留。" + "605a35d600": "终端渲染器尚未应用此项 — xterm.js 没有粗体颜色槽位。已保存的值会被保留。", + "color_overrides_dark_description": "覆盖深色终端主题的各个颜色。", + "color_overrides_light_description": "覆盖浅色终端主题的各个颜色。", + "reset_dark_color_overrides": "重置深色主题颜色覆盖", + "reset_light_color_overrides": "重置浅色主题颜色覆盖" }, "UIZoomControl": { "c2c64b24d0": "重置" diff --git a/src/renderer/src/lib/left-sidebar-appearance.ts b/src/renderer/src/lib/left-sidebar-appearance.ts index 56771c47ea3..c8a424718df 100644 --- a/src/renderer/src/lib/left-sidebar-appearance.ts +++ b/src/renderer/src/lib/left-sidebar-appearance.ts @@ -19,6 +19,7 @@ type LeftSidebarAppearanceSettings = Pick< | 'terminalCustomThemes' | 'terminalDividerColorLight' | 'terminalColorOverrides' + | 'terminalColorOverridesLight' | 'terminalBackgroundOpacity' > @@ -93,11 +94,10 @@ function resolveTerminalSurfaceVariables( ): LeftSidebarStyleVariables { const appearance = resolveEffectiveTerminalAppearance(settings, systemPrefersDark) const background = applyAlpha( - settings.terminalColorOverrides?.background ?? appearance.theme?.background ?? '#000000', + appearance.theme?.background ?? '#000000', settings.terminalBackgroundOpacity ) - const foreground = - settings.terminalColorOverrides?.foreground ?? appearance.theme?.foreground ?? '#fafafa' + const foreground = appearance.theme?.foreground ?? '#fafafa' return buildSurfaceVariables({ background, foreground, overrideTextTokens: true }) } diff --git a/src/renderer/src/lib/terminal-theme.ts b/src/renderer/src/lib/terminal-theme.ts index a6a0ad8ecb3..5a597e12446 100644 --- a/src/renderer/src/lib/terminal-theme.ts +++ b/src/renderer/src/lib/terminal-theme.ts @@ -12,8 +12,9 @@ import { DEFAULT_TERMINAL_THEME_DARK, DEFAULT_TERMINAL_THEME_LIGHT, lookupTerminalTheme, - type TerminalThemeLookupSettings, - selectTerminalTheme + resolveTerminalPalette, + type TerminalPaletteSettings, + type TerminalThemeLookupSettings } from '../../../shared/terminal-theme-selection' export const BUILTIN_TERMINAL_THEME_NAMES = getThemeNames() @@ -58,20 +59,6 @@ export function getTerminalTheme( return lookupTerminalTheme(settings, selection) } -function getTerminalThemePreview( - name: string, - settings?: TerminalThemeLookupSettings, - fallbackMode: 'dark' | 'light' = 'dark' -): ITheme | null { - const theme = getTerminalTheme(settings, name) - if (theme) { - return theme - } - return getTheme( - fallbackMode === 'light' ? DEFAULT_TERMINAL_THEME_LIGHT : DEFAULT_TERMINAL_THEME_DARK - ) -} - export function getAvailableTerminalThemeOptions( settings: Pick, pluginThemes: readonly PluginTerminalThemeRegistration[] = [] @@ -116,24 +103,23 @@ export function resolveEffectiveTerminalAppearance( | 'terminalPluginPalettes' | 'terminalDividerColorLight' > & - TerminalThemeLookupSettings, + TerminalPaletteSettings, systemPrefersDark = getSystemPrefersDark() ): EffectiveTerminalAppearance { - const { - mode: sourceTheme, - useLightVariant, - themeName - } = selectTerminalTheme(settings, systemPrefersDark) + const { mode, useLightVariant, themeName, theme } = resolveTerminalPalette( + settings, + systemPrefersDark + ) const dividerColor = useLightVariant ? normalizeColor(settings.terminalDividerColorLight, DEFAULT_TERMINAL_DIVIDER_LIGHT) : normalizeColor(settings.terminalDividerColorDark, DEFAULT_TERMINAL_DIVIDER_DARK) return { - mode: sourceTheme, + mode, sourceTheme: settings.theme, themeName, dividerColor, - theme: getTerminalThemePreview(themeName, settings, useLightVariant ? 'light' : 'dark'), + theme, systemPrefersDark } } diff --git a/src/renderer/src/runtime/sync-runtime-graph-terminal-surface-projection.test.ts b/src/renderer/src/runtime/sync-runtime-graph-terminal-surface-projection.test.ts index d5e20233b3e..acc96748e9f 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-terminal-surface-projection.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-terminal-surface-projection.test.ts @@ -536,7 +536,7 @@ describe('buildMobileSessionTabSnapshots', () => { ...getDefaultSettings('/tmp'), theme: 'light', terminalUseSeparateLightTheme: true, - terminalColorOverrides: { + terminalColorOverridesLight: { background: '#f8f8f8', foreground: '#101010', cursor: '#202020' diff --git a/src/renderer/src/runtime/sync-runtime-graph/mobile-terminal-theme.ts b/src/renderer/src/runtime/sync-runtime-graph/mobile-terminal-theme.ts index e1c60e66fa9..4c3853d4eb8 100644 --- a/src/renderer/src/runtime/sync-runtime-graph/mobile-terminal-theme.ts +++ b/src/renderer/src/runtime/sync-runtime-graph/mobile-terminal-theme.ts @@ -30,9 +30,8 @@ export function resolveMobileTerminalTheme( return undefined } const appearance = resolveEffectiveTerminalAppearance(settings, systemPrefersDark) - const resolvedTheme = appearance.theme - ? { ...appearance.theme, ...settings.terminalColorOverrides } - : undefined + // Copy: the opacity writes below must not mutate the shared theme catalog entry. + const resolvedTheme = appearance.theme ? { ...appearance.theme } : undefined if (!resolvedTheme) { return undefined } diff --git a/src/renderer/src/web/preload-api/web-preference-normalization.ts b/src/renderer/src/web/preload-api/web-preference-normalization.ts index 8bfb93d107b..4561b180b76 100644 --- a/src/renderer/src/web/preload-api/web-preference-normalization.ts +++ b/src/renderer/src/web/preload-api/web-preference-normalization.ts @@ -23,6 +23,7 @@ import type { PairedUiState, PairingLocalUiField } from '../../../../shared/pair import type { PersistedUIState } from '../../../../shared/persisted-ui-state-types' import { normalizeStatusBarUsageMode } from '../../../../shared/status-bar-usage-mode' import { normalizeTerminalCustomThemes } from '../../../../shared/terminal-custom-themes' +import { splitLegacyTerminalColorOverrides } from '../../../../shared/terminal-color-overrides' import { DEFAULT_TUI_AGENT_ARGS, DEFAULT_TUI_AGENT_ENV, @@ -165,6 +166,7 @@ export function migrateStoredWebSettings( ...normalizeTerminalCursorStyleDefault(stored), ...normalizeOsc52ClipboardDefaultOn(stored), terminalCustomThemes: normalizeTerminalCustomThemes(stored.terminalCustomThemes), + ...splitLegacyTerminalColorOverrides(stored), ...migratePortugueseUiLanguageOnLoad(stored) } } diff --git a/src/renderer/src/web/preload-api/web-preferences-store.ts b/src/renderer/src/web/preload-api/web-preferences-store.ts index 73baf20d33b..dfbcd4e633c 100644 --- a/src/renderer/src/web/preload-api/web-preferences-store.ts +++ b/src/renderer/src/web/preload-api/web-preferences-store.ts @@ -48,6 +48,7 @@ export function getStoredSettings(): GlobalSettings { stored.terminalAllowOsc52ClipboardDefaultedOnForAllUsers !== migratedStored.terminalAllowOsc52ClipboardDefaultedOnForAllUsers || stored.terminalCustomThemes !== migratedStored.terminalCustomThemes || + stored.terminalColorOverridesSplit !== migratedStored.terminalColorOverridesSplit || stored.uiLanguage !== migratedStored.uiLanguage || needsChatUiChoiceCarry(stored) || stored.uiLanguagePortugueseMigrationApplied !== true) diff --git a/src/shared/default-global-settings.ts b/src/shared/default-global-settings.ts index d84c8c23ee4..47cb02dcfea 100644 --- a/src/shared/default-global-settings.ts +++ b/src/shared/default-global-settings.ts @@ -85,6 +85,7 @@ export function buildDefaultSettings(args: { terminalThemeDark: 'Ghostty Default Style Dark', terminalDividerColorDark: '#3f3f46', terminalUseSeparateLightTheme: true, + terminalColorOverridesSplit: true, terminalThemeLight: 'Builtin Tango Light', terminalCustomThemes: [], terminalDividerColorLight: '#d4d4d8', diff --git a/src/shared/global-settings-base.ts b/src/shared/global-settings-base.ts index f84482239e5..42234248914 100644 --- a/src/shared/global-settings-base.ts +++ b/src/shared/global-settings-base.ts @@ -10,6 +10,7 @@ import type { PluginUpdatePolicySettings } from './plugins/catalog/plugin-update import type { PluginSystemSettings } from './plugins/plugin-system-settings' import type { WorktreeDeletionPreferences } from './worktree-deletion-preferences' import type { AgentPermissionGlobalSettings } from './tui-agent-permission-choice' +import type { TerminalColorOverrideGlobalSettings } from './terminal-color-overrides' /** Settings groups owned by their feature modules and composed into GlobalSettings. */ export type GlobalSettingsBase = ClaudeAgentViewSettings & @@ -23,4 +24,5 @@ export type GlobalSettingsBase = ClaudeAgentViewSettings & PluginAppearanceSettings & PluginFileTypeSuggestionSettings & PluginUpdatePolicySettings & - PluginSystemSettings + PluginSystemSettings & + TerminalColorOverrideGlobalSettings diff --git a/src/shared/global-settings-types.ts b/src/shared/global-settings-types.ts index 87c003315f9..1e5b994841a 100644 --- a/src/shared/global-settings-types.ts +++ b/src/shared/global-settings-types.ts @@ -29,7 +29,6 @@ import type { } from './managed-account-types' import type { NotificationSettings } from './notification-settings-types' import type { CtrlTabOrderMode } from './tab-types' -import type { TerminalColorOverrides } from './terminal-color-overrides' import type { TerminalQuickCommand } from './terminal-quick-command-types' import type { TuiAgent } from './tui-agent' import type { ZcodePlanSite } from './zcode-plan-sites' @@ -136,7 +135,6 @@ export type GlobalSettings = GlobalSettingsBase & { * background-luminance-gated floor (3 dark / 4.5 light); 1 disables contrast correction so TUIs * that rely on deliberately low contrast (Powerline seams, dimmed secondary text) render as sent. */ terminalMinimumContrastRatio?: number - terminalColorOverrides?: TerminalColorOverrides terminalPaddingX?: number terminalPaddingY?: number terminalMouseHideWhileTyping?: boolean diff --git a/src/shared/terminal-color-overrides.ts b/src/shared/terminal-color-overrides.ts index e0e3a31d1cb..b13c671db1e 100644 --- a/src/shared/terminal-color-overrides.ts +++ b/src/shared/terminal-color-overrides.ts @@ -1,3 +1,6 @@ +import type { ITheme } from '@xterm/xterm' +import type { GlobalSettings } from './global-settings-types' + export type TerminalColorOverrides = { foreground?: string background?: string @@ -30,3 +33,74 @@ export type TerminalPluginPalette = { id: `plugin:${string}` colors: TerminalColorOverrides } + +/** Terminal theme slot (the Dark/Light picker) an override bag tweaks; light mode paints the dark slot unless it has its own theme. */ +export type TerminalThemeSlot = 'dark' | 'light' + +export type TerminalColorOverrideGlobalSettings = { + /** Dark slot's overrides; light mode uses them too unless terminalUseSeparateLightTheme. */ + terminalColorOverrides?: TerminalColorOverrides + /** Light slot's overrides (#10561); older clients ignore them. */ + terminalColorOverridesLight?: TerminalColorOverrides + /** One-shot migration guard for splitting the legacy shared bag per slot. */ + terminalColorOverridesSplit?: boolean +} + +// Why the dark slot keeps the original key: older clients keep applying it in both modes, the pre-split behavior. +export const TERMINAL_COLOR_OVERRIDES_SETTING = { + dark: 'terminalColorOverrides', + light: 'terminalColorOverridesLight' +} as const satisfies Record + +export type TerminalColorOverrideSettings = Pick< + TerminalColorOverrideGlobalSettings, + 'terminalColorOverrides' | 'terminalColorOverridesLight' +> + +export function terminalColorOverridesForSlot( + settings: TerminalColorOverrideSettings, + slot: TerminalThemeSlot +): TerminalColorOverrides | undefined { + return settings[TERMINAL_COLOR_OVERRIDES_SETTING[slot]] +} + +export function hasTerminalColorOverrides( + settings: TerminalColorOverrideSettings, + slot: TerminalThemeSlot +): boolean { + return Object.values(terminalColorOverridesForSlot(settings, slot) ?? {}).some(Boolean) +} + +/** Cleared fields stay undefined in memory until reload; they must not blank the theme's color. */ +export function withTerminalColorOverrides( + theme: ITheme, + overrides: TerminalColorOverrides | undefined +): ITheme { + const painted = Object.entries(overrides ?? {}).filter( + ([, value]) => typeof value === 'string' && value.length > 0 + ) + return painted.length === 0 ? theme : { ...theme, ...Object.fromEntries(painted) } +} + +type LegacyTerminalColorOverrideSettings = TerminalColorOverrideGlobalSettings & + Partial> + +/** One-shot split of the pre-#10561 shared bag. It stays on the dark slot; when the light slot could have shown it + * (own light theme, app not pinned dark) it is copied there too, so nothing changes on upgrade and older clients, + * which only read the dark key, keep their behavior. A missing separate-light flag means its default, on. */ +export function splitLegacyTerminalColorOverrides( + settings: LegacyTerminalColorOverrideSettings | undefined +): TerminalColorOverrideGlobalSettings { + const shownInLight = + settings?.terminalColorOverridesSplit !== true && + settings?.theme !== 'dark' && + settings?.terminalUseSeparateLightTheme !== false && + settings?.terminalColorOverridesLight === undefined + return { + terminalColorOverrides: settings?.terminalColorOverrides, + terminalColorOverridesLight: shownInLight + ? settings?.terminalColorOverrides + : settings?.terminalColorOverridesLight, + terminalColorOverridesSplit: true + } +} diff --git a/src/shared/terminal-theme-selection.test.ts b/src/shared/terminal-theme-selection.test.ts index f0663ba02c6..bf4add78fbc 100644 --- a/src/shared/terminal-theme-selection.test.ts +++ b/src/shared/terminal-theme-selection.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest' -import { resolveConfiguredTerminalColors } from './terminal-theme-selection' +import { + resolveConfiguredTerminalColors, + resolveTerminalPalette, + withImportedTerminalColorOverrides +} from './terminal-theme-selection' import { TERMINAL_THEME_CATALOG } from './terminal-themes' const base = { @@ -45,6 +49,50 @@ describe('the terminal colours a host configures', () => { }) }) +describe('terminal colour overrides per theme slot (#10561)', () => { + const settings = { + ...base, + terminalColorOverrides: { background: '#101010' }, + terminalColorOverridesLight: { foreground: '#202020' } + } + + it('paints each mode with only its own slot overrides', () => { + expect(resolveTerminalPalette(settings, true).theme).toMatchObject({ + foreground: '#ffffff', + background: '#101010' + }) + expect(resolveTerminalPalette(settings, false).theme).toMatchObject({ + foreground: '#202020', + background: '#ffffff' + }) + }) + + it('lets light mode that matches dark share the dark slot overrides', () => { + expect( + resolveTerminalPalette({ ...settings, terminalUseSeparateLightTheme: false }, false).theme + ).toMatchObject({ foreground: '#ffffff', background: '#101010' }) + }) + + it('merges an imported palette into the slot on screen once the import applies', () => { + const imported = { terminalColorOverrides: { red: '#ff0000' } } + expect(withImportedTerminalColorOverrides(settings, imported, false)).toEqual({ + terminalColorOverridesLight: { foreground: '#202020', red: '#ff0000' } + }) + expect( + withImportedTerminalColorOverrides(settings, { ...imported, theme: 'dark' }, false) + ).toEqual({ theme: 'dark', terminalColorOverrides: { background: '#101010', red: '#ff0000' } }) + }) + + it('keeps the theme colour for a field cleared in memory', () => { + expect( + resolveConfiguredTerminalColors( + { ...base, terminalColorOverrides: { background: undefined } }, + true + ) + ).toEqual({ foreground: '#ffffff', background: '#282c34' }) + }) +}) + describe('plugin terminal palettes', () => { const id = 'plugin:samples.dusk/dusk' as const const colors = { background: '#1b1430', foreground: '#e8e0ff', red: '#ff6b8b' } diff --git a/src/shared/terminal-theme-selection.ts b/src/shared/terminal-theme-selection.ts index 4b699fa6284..4984f2403a3 100644 --- a/src/shared/terminal-theme-selection.ts +++ b/src/shared/terminal-theme-selection.ts @@ -1,5 +1,12 @@ +import type { ITheme } from '@xterm/xterm' import type { GlobalSettings } from './global-settings-types' import { isPluginContentSwitchedOff } from './plugins/plugin-system-settings' +import { + TERMINAL_COLOR_OVERRIDES_SETTING, + terminalColorOverridesForSlot, + withTerminalColorOverrides, + type TerminalColorOverrideSettings +} from './terminal-color-overrides' import { hasUsableTerminalThemeColors, normalizeTerminalColorOverrides, @@ -86,26 +93,51 @@ function pluginPalette(palettes: unknown, selection: string): TerminalThemeMap[s return hasUsableTerminalThemeColors(colors) ? colors : null } -function themeColors( - selection: string, - settings: TerminalThemeLookupSettings -): TerminalOscColorQueryReplyColors | null { - const theme = lookupTerminalTheme(settings, selection) - return theme ? { foreground: theme.foreground, background: theme.background } : null +export type TerminalPaletteSettings = TerminalThemeSelectionSettings & + TerminalThemeLookupSettings & + TerminalColorOverrideSettings + +export type TerminalPalette = TerminalThemeSelection & { theme: ITheme | null } + +/** The one place a mode's terminal palette is composed: its slot's theme (or that slot's default) plus the slot's overrides. */ +export function resolveTerminalPalette( + settings: TerminalPaletteSettings, + systemPrefersDark: boolean +): TerminalPalette { + const selection = selectTerminalTheme(settings, systemPrefersDark) + const slot = selection.useLightVariant ? 'light' : 'dark' + const base = + lookupTerminalTheme(settings, selection.themeName) ?? + lookupTerminalTheme( + settings, + selection.useLightVariant ? DEFAULT_TERMINAL_THEME_LIGHT : DEFAULT_TERMINAL_THEME_DARK + ) + return { + ...selection, + theme: base && withTerminalColorOverrides(base, terminalColorOverridesForSlot(settings, slot)) + } +} + +/** An imported palette (Ghostty) merges into the slot painting the terminal once the import applies, so it shows. */ +export function withImportedTerminalColorOverrides( + settings: TerminalThemeSelectionSettings & TerminalColorOverrideSettings, + diff: Partial, + systemPrefersDark: boolean +): Partial { + const { terminalColorOverrides: imported, ...rest } = diff + if (!imported) { + return diff + } + const { useLightVariant } = selectTerminalTheme({ ...settings, ...rest }, systemPrefersDark) + const setting = TERMINAL_COLOR_OVERRIDES_SETTING[useLightVariant ? 'light' : 'dark'] + return { ...rest, [setting]: { ...settings[setting], ...imported } } } /** Answer OSC 10/11 from saved settings without a renderer; queries report opaque theme colors, ignoring opacity. */ export function resolveConfiguredTerminalColors( - settings: TerminalThemeSelectionSettings & - TerminalThemeLookupSettings & - Pick, + settings: TerminalPaletteSettings, systemPrefersDark: boolean ): TerminalOscColorQueryReplyColors { - const { themeName, useLightVariant } = selectTerminalTheme(settings, systemPrefersDark) - const fallback = useLightVariant ? DEFAULT_TERMINAL_THEME_LIGHT : DEFAULT_TERMINAL_THEME_DARK - const base: TerminalOscColorQueryReplyColors = - themeColors(themeName, settings) ?? themeColors(fallback, settings) ?? {} - const foreground = settings.terminalColorOverrides?.foreground ?? base.foreground - const background = settings.terminalColorOverrides?.background ?? base.background + const { foreground, background } = resolveTerminalPalette(settings, systemPrefersDark).theme ?? {} return { ...(foreground ? { foreground } : {}), ...(background ? { background } : {}) } }