Fix terminal custom accent color (#864)

This commit is contained in:
陈大猫
2026-04-29 11:21:29 +08:00
committed by GitHub
parent 4dd2465388
commit e4e1b54374
8 changed files with 236 additions and 31 deletions

17
App.tsx
View File

@@ -17,7 +17,7 @@ import { I18nProvider, useI18n } from './application/i18n/I18nProvider';
import { matchesKeyBinding } from './domain/models';
import { resolveGroupDefaults, applyGroupDefaults } from './domain/groupConfig';
import { resolveHostAuth } from './domain/sshAuth';
import { resolveHostTerminalThemeId } from './domain/terminalAppearance';
import { applyCustomAccentToTerminalTheme, resolveHostTerminalThemeId } from './domain/terminalAppearance';
import { collectSessionIds } from './domain/workspace';
import { resolveCloseIntent } from './application/state/resolveCloseIntent';
import { resolveSnippetsShortcutIntent } from './application/state/resolveSnippetsShortcutIntent';
@@ -207,6 +207,8 @@ function App({ settings }: { settings: SettingsState }) {
theme,
setTheme,
resolvedTheme,
accentMode,
customAccent,
terminalThemeId,
setTerminalThemeId,
followAppTerminalTheme,
@@ -366,14 +368,19 @@ function App({ settings }: { settings: SettingsState }) {
if (activeTabId === 'vault' || activeTabId === 'sftp') return null;
const resolveTheme = (s: TerminalSession): TerminalTheme => {
let baseTheme: TerminalTheme;
// When "Follow Application Theme" is on, the UI-matched terminal
// theme overrides everything — including per-host theme overrides.
// This ensures all terminals match the app chrome regardless of
// individual host settings.
if (followAppTerminalTheme) return currentTerminalTheme;
if (followAppTerminalTheme) {
baseTheme = currentTerminalTheme;
} else {
const host = hostById.get(s.hostId) ?? null;
const themeId = resolveHostTerminalThemeId(host, currentTerminalTheme.id);
return themeById.get(themeId) || currentTerminalTheme;
baseTheme = themeById.get(themeId) || currentTerminalTheme;
}
return applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent);
};
// Workspace
@@ -403,7 +410,7 @@ function App({ settings }: { settings: SettingsState }) {
const session = sessionById.get(activeTabId);
if (!session) return null;
return resolveTheme(session);
}, [activeTabId, currentTerminalTheme, followAppTerminalTheme, hostById, sessionById, themeById, workspaceById]);
}, [accentMode, activeTabId, currentTerminalTheme, customAccent, followAppTerminalTheme, hostById, sessionById, themeById, workspaceById]);
useImmersiveMode({
activeTabId,
@@ -1914,6 +1921,8 @@ function App({ settings }: { settings: SettingsState }) {
draggingSessionId={draggingSessionId}
terminalTheme={currentTerminalTheme}
followAppTerminalTheme={followAppTerminalTheme}
accentMode={accentMode}
customAccent={customAccent}
terminalSettings={terminalSettings}
terminalFontFamilyId={terminalFontFamilyId}
fontSize={terminalFontSize}

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type SetStateAction } from 'react';
import { SyncConfig, TerminalSettings, HotkeyScheme, CustomKeyBindings, DEFAULT_KEY_BINDINGS, KeyBinding, UILanguage, SessionLogFormat, normalizeTerminalSettings } from '../../domain/models';
import { SyncConfig, TerminalTheme, TerminalSettings, HotkeyScheme, CustomKeyBindings, DEFAULT_KEY_BINDINGS, KeyBinding, UILanguage, SessionLogFormat, normalizeTerminalSettings } from '../../domain/models';
import {
STORAGE_KEY_COLOR,
STORAGE_KEY_SYNC,
@@ -49,7 +49,7 @@ import {
shouldApplyIncomingCustomKeyBindingsRecord,
updateCustomKeyBinding as updateCustomKeyBindingRecord,
} from '../../domain/customKeyBindings';
import { getTerminalThemeForUiTheme } from '../../domain/terminalAppearance';
import { applyCustomAccentToTerminalTheme, getTerminalThemeForUiTheme } from '../../domain/terminalAppearance';
import { customThemeStore, useCustomThemes } from '../state/customThemeStore';
import { DEFAULT_FONT_SIZE } from '../../infrastructure/config/fonts';
import { DARK_UI_THEMES, LIGHT_UI_THEMES, UiThemeTokens, getUiThemeById } from '../../infrastructure/config/uiThemes';
@@ -1265,6 +1265,7 @@ export const useSettingsState = () => {
const customThemes = useCustomThemes();
const currentTerminalTheme = useMemo(() => {
let baseTheme: TerminalTheme;
// When "Follow Application Theme" is enabled, pick the terminal theme
// whose background matches the active UI theme preset.
if (followAppTerminalTheme) {
@@ -1272,13 +1273,17 @@ export const useSettingsState = () => {
const mapped = getTerminalThemeForUiTheme(activeUiThemeId);
if (mapped) {
const found = TERMINAL_THEMES.find(t => t.id === mapped);
if (found) return found;
if (found) {
baseTheme = found;
return applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent);
}
}
return TERMINAL_THEMES.find(t => t.id === terminalThemeId)
}
baseTheme = TERMINAL_THEMES.find(t => t.id === terminalThemeId)
|| customThemes.find(t => t.id === terminalThemeId)
|| TERMINAL_THEMES[0];
}, [terminalThemeId, customThemes, followAppTerminalTheme, resolvedTheme, lightUiThemeId, darkUiThemeId]);
return applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent);
}, [terminalThemeId, customThemes, followAppTerminalTheme, resolvedTheme, lightUiThemeId, darkUiThemeId, accentMode, customAccent]);
const updateTerminalSetting = useCallback(<K extends keyof TerminalSettings>(
key: K,

View File

@@ -26,6 +26,7 @@ import {
shouldScrollOnTerminalInput,
} from "../domain/terminalScroll";
import {
applyCustomAccentToTerminalTheme,
resolveHostTerminalThemeId,
} from "../domain/terminalAppearance";
import { classifyDistroId } from "../domain/host";
@@ -127,6 +128,8 @@ interface TerminalProps {
fontSize: number;
terminalTheme: TerminalTheme;
followAppTerminalTheme?: boolean;
accentMode?: "theme" | "custom";
customAccent?: string;
terminalSettings?: TerminalSettings;
sessionId: string;
startupCommand?: string;
@@ -225,6 +228,8 @@ const TerminalComponent: React.FC<TerminalProps> = ({
fontSize,
terminalTheme,
followAppTerminalTheme = false,
accentMode = "theme",
customAccent = "",
terminalSettings,
sessionId,
startupCommand,
@@ -682,18 +687,21 @@ const TerminalComponent: React.FC<TerminalProps> = ({
// When "Follow Application Theme" is on and there's no active
// preview, skip per-host overrides — all terminals should use the
// UI-matched theme passed via terminalTheme prop.
if (followAppTerminalTheme && !themePreviewId) return terminalTheme;
if (followAppTerminalTheme && !themePreviewId) {
return applyCustomAccentToTerminalTheme(terminalTheme, accentMode, customAccent);
}
const themeId = themePreviewId ?? resolveHostTerminalThemeId(
{ theme: host.theme, themeOverride: host.themeOverride } as Pick<Host, 'theme' | 'themeOverride'>,
terminalTheme.id,
);
let baseTheme = terminalTheme;
if (themeId) {
const hostTheme = TERMINAL_THEMES.find((t) => t.id === themeId)
|| customThemes.find((t) => t.id === themeId);
if (hostTheme) return hostTheme;
if (hostTheme) baseTheme = hostTheme;
}
return terminalTheme;
}, [customThemes, followAppTerminalTheme, host.theme, host.themeOverride, terminalTheme, themePreviewId]);
return applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent);
}, [accentMode, customAccent, customThemes, followAppTerminalTheme, host.theme, host.themeOverride, terminalTheme, themePreviewId]);
const resolvedChainHosts =
chainHosts;
@@ -1725,8 +1733,8 @@ const TerminalComponent: React.FC<TerminalProps> = ({
['--terminal-ui-border' as never]: `var(--terminal-preview-border, color-mix(in srgb, ${effectiveTheme.colors.foreground} 8%, ${effectiveTheme.colors.background} 92%))`,
['--terminal-ui-toolbar-btn' as never]: `var(--terminal-preview-toolbar-btn, color-mix(in srgb, ${effectiveTheme.colors.background} 88%, ${effectiveTheme.colors.foreground} 12%))`,
['--terminal-ui-toolbar-btn-hover' as never]: `var(--terminal-preview-toolbar-btn-hover, color-mix(in srgb, ${effectiveTheme.colors.background} 78%, ${effectiveTheme.colors.foreground} 22%))`,
['--terminal-ui-toolbar-btn-active' as never]: `var(--terminal-preview-toolbar-btn-active, color-mix(in srgb, ${effectiveTheme.colors.background} 68%, ${effectiveTheme.colors.foreground} 32%))`,
}), [effectiveTheme.colors.background, effectiveTheme.colors.foreground]);
['--terminal-ui-toolbar-btn-active' as never]: `var(--terminal-preview-toolbar-btn-active, color-mix(in srgb, ${effectiveTheme.colors.cursor} 78%, ${effectiveTheme.colors.background} 22%))`,
}), [effectiveTheme.colors.background, effectiveTheme.colors.cursor, effectiveTheme.colors.foreground]);
return (
<TerminalContextMenu

View File

@@ -24,6 +24,7 @@ import {
resolveHostTerminalFontSize,
resolveHostTerminalFontWeight,
resolveHostTerminalThemeId,
applyCustomAccentToTerminalTheme,
} from '../domain/terminalAppearance';
import { cn, normalizeLineEndings } from '../lib/utils';
import { detectLocalOs } from '../lib/localShell';
@@ -395,6 +396,8 @@ interface TerminalLayerProps {
draggingSessionId: string | null;
terminalTheme: TerminalTheme;
followAppTerminalTheme?: boolean;
accentMode?: 'theme' | 'custom';
customAccent?: string;
terminalSettings?: TerminalSettings;
terminalFontFamilyId: string;
fontSize?: number;
@@ -455,6 +458,8 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
draggingSessionId,
terminalTheme,
followAppTerminalTheme = false,
accentMode = 'theme',
customAccent = '',
terminalSettings,
terminalFontFamilyId,
fontSize = 14,
@@ -1580,35 +1585,37 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
return;
}
const pane = document.querySelector<HTMLElement>(`[data-session-id="${sessionId}"]`);
const theme = TERMINAL_THEMES.find((entry) => entry.id === themeId)
const baseTheme = TERMINAL_THEMES.find((entry) => entry.id === themeId)
|| customThemes.find((entry) => entry.id === themeId);
if (!pane || !theme) {
if (!pane || !baseTheme) {
clearTerminalPreviewVars(sessionId);
return;
}
const theme = applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent);
pane.style.setProperty('--terminal-preview-bg', theme.colors.background);
pane.style.setProperty('--terminal-preview-fg', theme.colors.foreground);
pane.style.setProperty('--terminal-preview-border', `color-mix(in srgb, ${theme.colors.foreground} 8%, ${theme.colors.background} 92%)`);
pane.style.setProperty('--terminal-preview-toolbar-btn', `color-mix(in srgb, ${theme.colors.background} 88%, ${theme.colors.foreground} 12%)`);
pane.style.setProperty('--terminal-preview-toolbar-btn-hover', `color-mix(in srgb, ${theme.colors.background} 78%, ${theme.colors.foreground} 22%)`);
pane.style.setProperty('--terminal-preview-toolbar-btn-active', `color-mix(in srgb, ${theme.colors.background} 68%, ${theme.colors.foreground} 32%)`);
}, [customThemes]);
pane.style.setProperty('--terminal-preview-toolbar-btn-active', `color-mix(in srgb, ${theme.colors.cursor} 78%, ${theme.colors.background} 22%)`);
}, [accentMode, customAccent, customThemes]);
const applyTopTabsPreviewVars = useCallback((themeId: string | null) => {
if (!themeId || typeof document === 'undefined') {
clearTopTabsPreviewVars();
return;
}
const tabsRoot = document.querySelector<HTMLElement>('[data-top-tabs-root]');
const theme = TERMINAL_THEMES.find((entry) => entry.id === themeId)
const baseTheme = TERMINAL_THEMES.find((entry) => entry.id === themeId)
|| customThemes.find((entry) => entry.id === themeId);
if (!tabsRoot || !theme) {
if (!tabsRoot || !baseTheme) {
clearTopTabsPreviewVars();
return;
}
const theme = applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent);
const bg = hexToHslToken(theme.colors.background);
const fg = hexToHslToken(theme.colors.foreground);
const accent = fg;
const accent = hexToHslToken(theme.colors.cursor);
const isDark = theme.type === 'dark';
const secondary = adjustLightnessToken(bg, isDark ? 6 : -5);
const border = adjustLightnessToken(bg, isDark ? 12 : -10);
@@ -1625,8 +1632,8 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
tabsRoot.style.setProperty('--top-tabs-fg', 'hsl(var(--foreground))');
tabsRoot.style.setProperty('--top-tabs-muted', 'hsl(var(--muted-foreground))');
tabsRoot.style.setProperty('--top-tabs-active-bg', 'hsl(var(--background))');
tabsRoot.style.setProperty('--top-tabs-accent', 'hsl(var(--foreground))');
}, [customThemes]);
tabsRoot.style.setProperty('--top-tabs-accent', 'hsl(var(--accent))');
}, [accentMode, customAccent, customThemes]);
useEffect(() => {
return () => {
@@ -1889,10 +1896,11 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
const resolvedPreviewTheme = useMemo(() => {
const themeId = previewedOrVisibleThemeId;
return TERMINAL_THEMES.find((theme) => theme.id === themeId)
const baseTheme = TERMINAL_THEMES.find((theme) => theme.id === themeId)
|| customThemes.find((theme) => theme.id === themeId)
|| terminalTheme;
}, [customThemes, previewedOrVisibleThemeId, terminalTheme]);
return applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent);
}, [accentMode, customAccent, customThemes, previewedOrVisibleThemeId, terminalTheme]);
const sessionLogConfig = useMemo(
() =>
sessionLogsEnabled && sessionLogsDir
@@ -2201,6 +2209,7 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
style={{
['--terminal-sidepanel-bg' as never]: resolvedPreviewTheme.colors.background,
['--terminal-sidepanel-fg' as never]: resolvedPreviewTheme.colors.foreground,
['--terminal-sidepanel-accent' as never]: resolvedPreviewTheme.colors.cursor,
['--terminal-sidepanel-muted' as never]: `color-mix(in srgb, ${resolvedPreviewTheme.colors.foreground} 62%, ${resolvedPreviewTheme.colors.background} 38%)`,
['--terminal-sidepanel-border' as never]: `color-mix(in srgb, ${resolvedPreviewTheme.colors.foreground} 12%, ${resolvedPreviewTheme.colors.background} 88%)`,
backgroundColor: 'var(--terminal-sidepanel-bg)',
@@ -2223,6 +2232,9 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
data-state={activeSidePanelTab === 'sftp' ? 'active' : 'inactive'}
className="netcatty-tab h-7 w-7 rounded-md p-0 hover:bg-transparent"
style={{
backgroundColor: activeSidePanelTab === 'sftp'
? 'color-mix(in srgb, var(--terminal-sidepanel-accent) 24%, transparent)'
: 'transparent',
color: activeSidePanelTab === 'sftp'
? 'var(--terminal-sidepanel-fg)'
: 'var(--terminal-sidepanel-muted)',
@@ -2240,6 +2252,9 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
data-state={activeSidePanelTab === 'scripts' ? 'active' : 'inactive'}
className="netcatty-tab h-7 w-7 rounded-md p-0 hover:bg-transparent"
style={{
backgroundColor: activeSidePanelTab === 'scripts'
? 'color-mix(in srgb, var(--terminal-sidepanel-accent) 24%, transparent)'
: 'transparent',
color: activeSidePanelTab === 'scripts'
? 'var(--terminal-sidepanel-fg)'
: 'var(--terminal-sidepanel-muted)',
@@ -2257,6 +2272,9 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
data-state={activeSidePanelTab === 'theme' ? 'active' : 'inactive'}
className="netcatty-tab h-7 w-7 rounded-md p-0 hover:bg-transparent"
style={{
backgroundColor: activeSidePanelTab === 'theme'
? 'color-mix(in srgb, var(--terminal-sidepanel-accent) 24%, transparent)'
: 'transparent',
color: activeSidePanelTab === 'theme'
? 'var(--terminal-sidepanel-fg)'
: 'var(--terminal-sidepanel-muted)',
@@ -2274,6 +2292,9 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
data-state={activeSidePanelTab === 'ai' ? 'active' : 'inactive'}
className="netcatty-tab h-7 w-7 rounded-md p-0 hover:bg-transparent"
style={{
backgroundColor: activeSidePanelTab === 'ai'
? 'color-mix(in srgb, var(--terminal-sidepanel-accent) 24%, transparent)'
: 'transparent',
color: activeSidePanelTab === 'ai'
? 'var(--terminal-sidepanel-fg)'
: 'var(--terminal-sidepanel-muted)',
@@ -2525,6 +2546,8 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
fontSize={fontSize}
terminalTheme={terminalTheme}
followAppTerminalTheme={followAppTerminalTheme}
accentMode={accentMode}
customAccent={customAccent}
terminalSettings={terminalSettings}
sessionId={session.id}
startupCommand={session.startupCommand}
@@ -2641,6 +2664,8 @@ const terminalLayerAreEqual = (prev: TerminalLayerProps, next: TerminalLayerProp
prev.workspaces === next.workspaces &&
prev.draggingSessionId === next.draggingSessionId &&
prev.terminalTheme === next.terminalTheme &&
prev.accentMode === next.accentMode &&
prev.customAccent === next.customAccent &&
prev.terminalSettings === next.terminalSettings &&
prev.fontSize === next.fontSize &&
prev.hotkeyScheme === next.hotkeyScheme &&

View File

@@ -17,7 +17,11 @@ const sshHost: Host = {
protocol: "ssh",
};
const renderToolbar = (host: Host, status: "connecting" | "connected" | "disconnected" = "connected") =>
const renderToolbar = (
host: Host,
status: "connecting" | "connected" | "disconnected" = "connected",
props: Partial<React.ComponentProps<typeof TerminalToolbar>> = {},
) =>
renderToStaticMarkup(
React.createElement(
I18nProvider,
@@ -28,6 +32,7 @@ const renderToolbar = (host: Host, status: "connecting" | "connected" | "disconn
onOpenSFTP: () => {},
onOpenScripts: () => {},
onOpenTheme: () => {},
...props,
}),
),
);
@@ -52,3 +57,15 @@ test("hides SFTP for local terminal sessions", () => {
assert.equal(markup.includes('aria-label="Open SFTP"'), false);
});
test("uses the terminal active button color for pressed toolbar actions", () => {
const markup = renderToolbar(sshHost, "connected", {
isSearchOpen: true,
onToggleSearch: () => {},
});
assert.match(
markup,
/aria-label="Search terminal"[^>]*style="background-color:var\(--terminal-toolbar-btn-active\)"/,
);
});

View File

@@ -71,6 +71,9 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
const hidesSftp = isLocalTerminal || isSerialTerminal;
const menuItemClass = "w-full flex items-center gap-2 px-2 py-1.5 text-xs rounded-sm hover:bg-secondary transition-colors";
const activeButtonStyle: React.CSSProperties = {
backgroundColor: 'var(--terminal-toolbar-btn-active)',
};
return (
<TooltipProvider delayDuration={500} skipDelayDuration={100} disableHoverableContent>
@@ -111,6 +114,7 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
aria-label={t("terminal.toolbar.composeBar")}
aria-pressed={isComposeBarOpen}
onClick={onToggleComposeBar}
style={isComposeBarOpen ? activeButtonStyle : undefined}
>
<TextCursorInput size={12} />
</Button>
@@ -127,6 +131,7 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
aria-label={t("terminal.toolbar.searchTerminal")}
aria-pressed={isSearchOpen}
onClick={onToggleSearch}
style={isSearchOpen ? activeButtonStyle : undefined}
>
<Search size={12} />
</Button>

View File

@@ -0,0 +1,48 @@
import test from "node:test";
import assert from "node:assert/strict";
import { applyCustomAccentToTerminalTheme } from "./terminalAppearance";
import type { TerminalTheme } from "./models";
const baseTheme: TerminalTheme = {
id: "ui-snow",
name: "Snow",
type: "light",
colors: {
background: "#f1f4f8",
foreground: "#24292f",
cursor: "#0969da",
selection: "#add6ff",
black: "#24292f",
red: "#cf222e",
green: "#116329",
yellow: "#9a6700",
blue: "#0969da",
magenta: "#8250df",
cyan: "#0e7574",
white: "#6e7781",
brightBlack: "#57606a",
brightRed: "#a40e26",
brightGreen: "#1a7f37",
brightYellow: "#7d4e00",
brightBlue: "#218bff",
brightMagenta: "#a475f9",
brightCyan: "#0c7875",
brightWhite: "#8c959f",
},
};
test("applies a custom accent to terminal cursor and selection colors", () => {
const accented = applyCustomAccentToTerminalTheme(baseTheme, "custom", "160 70% 40%");
assert.notEqual(accented, baseTheme);
assert.equal(accented.colors.cursor, "#1fad7e");
assert.equal(accented.colors.selection, "#b1f1dc");
assert.equal(baseTheme.colors.cursor, "#0969da");
assert.equal(baseTheme.colors.selection, "#add6ff");
});
test("keeps terminal theme unchanged without a valid custom accent", () => {
assert.equal(applyCustomAccentToTerminalTheme(baseTheme, "theme", "160 70% 40%"), baseTheme);
assert.equal(applyCustomAccentToTerminalTheme(baseTheme, "custom", "not-a-color"), baseTheme);
});

View File

@@ -1,4 +1,4 @@
import { Host } from './models';
import { Host, TerminalTheme } from './models';
const hasLegacyStringValue = (value: string | undefined): boolean =>
typeof value === 'string' && value.trim().length > 0;
@@ -69,6 +69,95 @@ const UI_TO_TERMINAL_THEME: Record<string, string> = {
export const getTerminalThemeForUiTheme = (uiThemeId: string): string | undefined =>
UI_TO_TERMINAL_THEME[uiThemeId];
type ParsedHslToken = {
hue: number;
saturation: number;
lightness: number;
};
const parseHslToken = (value: string): ParsedHslToken | null => {
const match = value.trim().match(/^(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%$/);
if (!match) return null;
const hue = Number(match[1]);
const saturation = Number(match[2]);
const lightness = Number(match[3]);
if (!Number.isFinite(hue) || !Number.isFinite(saturation) || !Number.isFinite(lightness)) return null;
if (saturation < 0 || saturation > 100 || lightness < 0 || lightness > 100) return null;
return {
hue: ((hue % 360) + 360) % 360,
saturation,
lightness,
};
};
const toHexChannel = (value: number): string =>
Math.round(Math.max(0, Math.min(255, value)))
.toString(16)
.padStart(2, '0');
const hslToHex = ({ hue, saturation, lightness }: ParsedHslToken): string => {
const s = saturation / 100;
const l = lightness / 100;
const c = (1 - Math.abs(2 * l - 1)) * s;
const hp = hue / 60;
const x = c * (1 - Math.abs((hp % 2) - 1));
let r = 0;
let g = 0;
let b = 0;
if (hp < 1) {
r = c;
g = x;
} else if (hp < 2) {
r = x;
g = c;
} else if (hp < 3) {
g = c;
b = x;
} else if (hp < 4) {
g = x;
b = c;
} else if (hp < 5) {
r = x;
b = c;
} else {
r = c;
b = x;
}
const m = l - c / 2;
return `#${toHexChannel((r + m) * 255)}${toHexChannel((g + m) * 255)}${toHexChannel((b + m) * 255)}`;
};
const terminalSelectionFromAccent = (accent: ParsedHslToken, type: TerminalTheme['type']): ParsedHslToken => ({
...accent,
lightness: type === 'dark'
? Math.max(18, Math.min(32, accent.lightness * 0.55))
: Math.max(72, Math.min(88, accent.lightness + 42)),
});
export const applyCustomAccentToTerminalTheme = (
theme: TerminalTheme,
accentMode: 'theme' | 'custom',
customAccent: string,
): TerminalTheme => {
if (accentMode !== 'custom') return theme;
const accent = parseHslToken(customAccent);
if (!accent) return theme;
return {
...theme,
colors: {
...theme.colors,
cursor: hslToHex(accent),
selection: hslToHex(terminalSelectionFromAccent(accent, theme.type)),
},
};
};
export const resolveHostTerminalFontFamilyId = (host: Host | null | undefined, defaultFontFamilyId: string): string =>
hasHostFontFamilyOverride(host) && host?.fontFamily ? host.fontFamily : defaultFontFamilyId;
@@ -86,4 +175,3 @@ export const clearHostFontWeightOverride = (host: Host): Host => ({
export const resolveHostTerminalFontWeight = (host: Host | null | undefined, defaultFontWeight: number): number =>
hasHostFontWeightOverride(host) && host?.fontWeight != null ? host.fontWeight : defaultFontWeight;