Fix terminal custom accent color (#864)
This commit is contained in:
21
App.tsx
21
App.tsx
@@ -17,7 +17,7 @@ import { I18nProvider, useI18n } from './application/i18n/I18nProvider';
|
|||||||
import { matchesKeyBinding } from './domain/models';
|
import { matchesKeyBinding } from './domain/models';
|
||||||
import { resolveGroupDefaults, applyGroupDefaults } from './domain/groupConfig';
|
import { resolveGroupDefaults, applyGroupDefaults } from './domain/groupConfig';
|
||||||
import { resolveHostAuth } from './domain/sshAuth';
|
import { resolveHostAuth } from './domain/sshAuth';
|
||||||
import { resolveHostTerminalThemeId } from './domain/terminalAppearance';
|
import { applyCustomAccentToTerminalTheme, resolveHostTerminalThemeId } from './domain/terminalAppearance';
|
||||||
import { collectSessionIds } from './domain/workspace';
|
import { collectSessionIds } from './domain/workspace';
|
||||||
import { resolveCloseIntent } from './application/state/resolveCloseIntent';
|
import { resolveCloseIntent } from './application/state/resolveCloseIntent';
|
||||||
import { resolveSnippetsShortcutIntent } from './application/state/resolveSnippetsShortcutIntent';
|
import { resolveSnippetsShortcutIntent } from './application/state/resolveSnippetsShortcutIntent';
|
||||||
@@ -207,6 +207,8 @@ function App({ settings }: { settings: SettingsState }) {
|
|||||||
theme,
|
theme,
|
||||||
setTheme,
|
setTheme,
|
||||||
resolvedTheme,
|
resolvedTheme,
|
||||||
|
accentMode,
|
||||||
|
customAccent,
|
||||||
terminalThemeId,
|
terminalThemeId,
|
||||||
setTerminalThemeId,
|
setTerminalThemeId,
|
||||||
followAppTerminalTheme,
|
followAppTerminalTheme,
|
||||||
@@ -366,14 +368,19 @@ function App({ settings }: { settings: SettingsState }) {
|
|||||||
if (activeTabId === 'vault' || activeTabId === 'sftp') return null;
|
if (activeTabId === 'vault' || activeTabId === 'sftp') return null;
|
||||||
|
|
||||||
const resolveTheme = (s: TerminalSession): TerminalTheme => {
|
const resolveTheme = (s: TerminalSession): TerminalTheme => {
|
||||||
|
let baseTheme: TerminalTheme;
|
||||||
// When "Follow Application Theme" is on, the UI-matched terminal
|
// When "Follow Application Theme" is on, the UI-matched terminal
|
||||||
// theme overrides everything — including per-host theme overrides.
|
// theme overrides everything — including per-host theme overrides.
|
||||||
// This ensures all terminals match the app chrome regardless of
|
// This ensures all terminals match the app chrome regardless of
|
||||||
// individual host settings.
|
// individual host settings.
|
||||||
if (followAppTerminalTheme) return currentTerminalTheme;
|
if (followAppTerminalTheme) {
|
||||||
const host = hostById.get(s.hostId) ?? null;
|
baseTheme = currentTerminalTheme;
|
||||||
const themeId = resolveHostTerminalThemeId(host, currentTerminalTheme.id);
|
} else {
|
||||||
return themeById.get(themeId) || currentTerminalTheme;
|
const host = hostById.get(s.hostId) ?? null;
|
||||||
|
const themeId = resolveHostTerminalThemeId(host, currentTerminalTheme.id);
|
||||||
|
baseTheme = themeById.get(themeId) || currentTerminalTheme;
|
||||||
|
}
|
||||||
|
return applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Workspace
|
// Workspace
|
||||||
@@ -403,7 +410,7 @@ function App({ settings }: { settings: SettingsState }) {
|
|||||||
const session = sessionById.get(activeTabId);
|
const session = sessionById.get(activeTabId);
|
||||||
if (!session) return null;
|
if (!session) return null;
|
||||||
return resolveTheme(session);
|
return resolveTheme(session);
|
||||||
}, [activeTabId, currentTerminalTheme, followAppTerminalTheme, hostById, sessionById, themeById, workspaceById]);
|
}, [accentMode, activeTabId, currentTerminalTheme, customAccent, followAppTerminalTheme, hostById, sessionById, themeById, workspaceById]);
|
||||||
|
|
||||||
useImmersiveMode({
|
useImmersiveMode({
|
||||||
activeTabId,
|
activeTabId,
|
||||||
@@ -1914,6 +1921,8 @@ function App({ settings }: { settings: SettingsState }) {
|
|||||||
draggingSessionId={draggingSessionId}
|
draggingSessionId={draggingSessionId}
|
||||||
terminalTheme={currentTerminalTheme}
|
terminalTheme={currentTerminalTheme}
|
||||||
followAppTerminalTheme={followAppTerminalTheme}
|
followAppTerminalTheme={followAppTerminalTheme}
|
||||||
|
accentMode={accentMode}
|
||||||
|
customAccent={customAccent}
|
||||||
terminalSettings={terminalSettings}
|
terminalSettings={terminalSettings}
|
||||||
terminalFontFamilyId={terminalFontFamilyId}
|
terminalFontFamilyId={terminalFontFamilyId}
|
||||||
fontSize={terminalFontSize}
|
fontSize={terminalFontSize}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type SetStateAction } from 'react';
|
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 {
|
import {
|
||||||
STORAGE_KEY_COLOR,
|
STORAGE_KEY_COLOR,
|
||||||
STORAGE_KEY_SYNC,
|
STORAGE_KEY_SYNC,
|
||||||
@@ -49,7 +49,7 @@ import {
|
|||||||
shouldApplyIncomingCustomKeyBindingsRecord,
|
shouldApplyIncomingCustomKeyBindingsRecord,
|
||||||
updateCustomKeyBinding as updateCustomKeyBindingRecord,
|
updateCustomKeyBinding as updateCustomKeyBindingRecord,
|
||||||
} from '../../domain/customKeyBindings';
|
} from '../../domain/customKeyBindings';
|
||||||
import { getTerminalThemeForUiTheme } from '../../domain/terminalAppearance';
|
import { applyCustomAccentToTerminalTheme, getTerminalThemeForUiTheme } from '../../domain/terminalAppearance';
|
||||||
import { customThemeStore, useCustomThemes } from '../state/customThemeStore';
|
import { customThemeStore, useCustomThemes } from '../state/customThemeStore';
|
||||||
import { DEFAULT_FONT_SIZE } from '../../infrastructure/config/fonts';
|
import { DEFAULT_FONT_SIZE } from '../../infrastructure/config/fonts';
|
||||||
import { DARK_UI_THEMES, LIGHT_UI_THEMES, UiThemeTokens, getUiThemeById } from '../../infrastructure/config/uiThemes';
|
import { DARK_UI_THEMES, LIGHT_UI_THEMES, UiThemeTokens, getUiThemeById } from '../../infrastructure/config/uiThemes';
|
||||||
@@ -1265,6 +1265,7 @@ export const useSettingsState = () => {
|
|||||||
const customThemes = useCustomThemes();
|
const customThemes = useCustomThemes();
|
||||||
|
|
||||||
const currentTerminalTheme = useMemo(() => {
|
const currentTerminalTheme = useMemo(() => {
|
||||||
|
let baseTheme: TerminalTheme;
|
||||||
// When "Follow Application Theme" is enabled, pick the terminal theme
|
// When "Follow Application Theme" is enabled, pick the terminal theme
|
||||||
// whose background matches the active UI theme preset.
|
// whose background matches the active UI theme preset.
|
||||||
if (followAppTerminalTheme) {
|
if (followAppTerminalTheme) {
|
||||||
@@ -1272,13 +1273,17 @@ export const useSettingsState = () => {
|
|||||||
const mapped = getTerminalThemeForUiTheme(activeUiThemeId);
|
const mapped = getTerminalThemeForUiTheme(activeUiThemeId);
|
||||||
if (mapped) {
|
if (mapped) {
|
||||||
const found = TERMINAL_THEMES.find(t => t.id === 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)
|
|| customThemes.find(t => t.id === terminalThemeId)
|
||||||
|| TERMINAL_THEMES[0];
|
|| 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>(
|
const updateTerminalSetting = useCallback(<K extends keyof TerminalSettings>(
|
||||||
key: K,
|
key: K,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
shouldScrollOnTerminalInput,
|
shouldScrollOnTerminalInput,
|
||||||
} from "../domain/terminalScroll";
|
} from "../domain/terminalScroll";
|
||||||
import {
|
import {
|
||||||
|
applyCustomAccentToTerminalTheme,
|
||||||
resolveHostTerminalThemeId,
|
resolveHostTerminalThemeId,
|
||||||
} from "../domain/terminalAppearance";
|
} from "../domain/terminalAppearance";
|
||||||
import { classifyDistroId } from "../domain/host";
|
import { classifyDistroId } from "../domain/host";
|
||||||
@@ -127,6 +128,8 @@ interface TerminalProps {
|
|||||||
fontSize: number;
|
fontSize: number;
|
||||||
terminalTheme: TerminalTheme;
|
terminalTheme: TerminalTheme;
|
||||||
followAppTerminalTheme?: boolean;
|
followAppTerminalTheme?: boolean;
|
||||||
|
accentMode?: "theme" | "custom";
|
||||||
|
customAccent?: string;
|
||||||
terminalSettings?: TerminalSettings;
|
terminalSettings?: TerminalSettings;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
startupCommand?: string;
|
startupCommand?: string;
|
||||||
@@ -225,6 +228,8 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
fontSize,
|
fontSize,
|
||||||
terminalTheme,
|
terminalTheme,
|
||||||
followAppTerminalTheme = false,
|
followAppTerminalTheme = false,
|
||||||
|
accentMode = "theme",
|
||||||
|
customAccent = "",
|
||||||
terminalSettings,
|
terminalSettings,
|
||||||
sessionId,
|
sessionId,
|
||||||
startupCommand,
|
startupCommand,
|
||||||
@@ -682,18 +687,21 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
// When "Follow Application Theme" is on and there's no active
|
// When "Follow Application Theme" is on and there's no active
|
||||||
// preview, skip per-host overrides — all terminals should use the
|
// preview, skip per-host overrides — all terminals should use the
|
||||||
// UI-matched theme passed via terminalTheme prop.
|
// UI-matched theme passed via terminalTheme prop.
|
||||||
if (followAppTerminalTheme && !themePreviewId) return terminalTheme;
|
if (followAppTerminalTheme && !themePreviewId) {
|
||||||
|
return applyCustomAccentToTerminalTheme(terminalTheme, accentMode, customAccent);
|
||||||
|
}
|
||||||
const themeId = themePreviewId ?? resolveHostTerminalThemeId(
|
const themeId = themePreviewId ?? resolveHostTerminalThemeId(
|
||||||
{ theme: host.theme, themeOverride: host.themeOverride } as Pick<Host, 'theme' | 'themeOverride'>,
|
{ theme: host.theme, themeOverride: host.themeOverride } as Pick<Host, 'theme' | 'themeOverride'>,
|
||||||
terminalTheme.id,
|
terminalTheme.id,
|
||||||
);
|
);
|
||||||
|
let baseTheme = terminalTheme;
|
||||||
if (themeId) {
|
if (themeId) {
|
||||||
const hostTheme = TERMINAL_THEMES.find((t) => t.id === themeId)
|
const hostTheme = TERMINAL_THEMES.find((t) => t.id === themeId)
|
||||||
|| customThemes.find((t) => t.id === themeId);
|
|| customThemes.find((t) => t.id === themeId);
|
||||||
if (hostTheme) return hostTheme;
|
if (hostTheme) baseTheme = hostTheme;
|
||||||
}
|
}
|
||||||
return terminalTheme;
|
return applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent);
|
||||||
}, [customThemes, followAppTerminalTheme, host.theme, host.themeOverride, terminalTheme, themePreviewId]);
|
}, [accentMode, customAccent, customThemes, followAppTerminalTheme, host.theme, host.themeOverride, terminalTheme, themePreviewId]);
|
||||||
|
|
||||||
const resolvedChainHosts =
|
const resolvedChainHosts =
|
||||||
chainHosts;
|
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-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' 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-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%))`,
|
['--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.foreground]);
|
}), [effectiveTheme.colors.background, effectiveTheme.colors.cursor, effectiveTheme.colors.foreground]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TerminalContextMenu
|
<TerminalContextMenu
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
resolveHostTerminalFontSize,
|
resolveHostTerminalFontSize,
|
||||||
resolveHostTerminalFontWeight,
|
resolveHostTerminalFontWeight,
|
||||||
resolveHostTerminalThemeId,
|
resolveHostTerminalThemeId,
|
||||||
|
applyCustomAccentToTerminalTheme,
|
||||||
} from '../domain/terminalAppearance';
|
} from '../domain/terminalAppearance';
|
||||||
import { cn, normalizeLineEndings } from '../lib/utils';
|
import { cn, normalizeLineEndings } from '../lib/utils';
|
||||||
import { detectLocalOs } from '../lib/localShell';
|
import { detectLocalOs } from '../lib/localShell';
|
||||||
@@ -395,6 +396,8 @@ interface TerminalLayerProps {
|
|||||||
draggingSessionId: string | null;
|
draggingSessionId: string | null;
|
||||||
terminalTheme: TerminalTheme;
|
terminalTheme: TerminalTheme;
|
||||||
followAppTerminalTheme?: boolean;
|
followAppTerminalTheme?: boolean;
|
||||||
|
accentMode?: 'theme' | 'custom';
|
||||||
|
customAccent?: string;
|
||||||
terminalSettings?: TerminalSettings;
|
terminalSettings?: TerminalSettings;
|
||||||
terminalFontFamilyId: string;
|
terminalFontFamilyId: string;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
@@ -455,6 +458,8 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
|||||||
draggingSessionId,
|
draggingSessionId,
|
||||||
terminalTheme,
|
terminalTheme,
|
||||||
followAppTerminalTheme = false,
|
followAppTerminalTheme = false,
|
||||||
|
accentMode = 'theme',
|
||||||
|
customAccent = '',
|
||||||
terminalSettings,
|
terminalSettings,
|
||||||
terminalFontFamilyId,
|
terminalFontFamilyId,
|
||||||
fontSize = 14,
|
fontSize = 14,
|
||||||
@@ -1580,35 +1585,37 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pane = document.querySelector<HTMLElement>(`[data-session-id="${sessionId}"]`);
|
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);
|
|| customThemes.find((entry) => entry.id === themeId);
|
||||||
if (!pane || !theme) {
|
if (!pane || !baseTheme) {
|
||||||
clearTerminalPreviewVars(sessionId);
|
clearTerminalPreviewVars(sessionId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const theme = applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent);
|
||||||
|
|
||||||
pane.style.setProperty('--terminal-preview-bg', theme.colors.background);
|
pane.style.setProperty('--terminal-preview-bg', theme.colors.background);
|
||||||
pane.style.setProperty('--terminal-preview-fg', theme.colors.foreground);
|
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-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', `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-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%)`);
|
pane.style.setProperty('--terminal-preview-toolbar-btn-active', `color-mix(in srgb, ${theme.colors.cursor} 78%, ${theme.colors.background} 22%)`);
|
||||||
}, [customThemes]);
|
}, [accentMode, customAccent, customThemes]);
|
||||||
const applyTopTabsPreviewVars = useCallback((themeId: string | null) => {
|
const applyTopTabsPreviewVars = useCallback((themeId: string | null) => {
|
||||||
if (!themeId || typeof document === 'undefined') {
|
if (!themeId || typeof document === 'undefined') {
|
||||||
clearTopTabsPreviewVars();
|
clearTopTabsPreviewVars();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tabsRoot = document.querySelector<HTMLElement>('[data-top-tabs-root]');
|
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);
|
|| customThemes.find((entry) => entry.id === themeId);
|
||||||
if (!tabsRoot || !theme) {
|
if (!tabsRoot || !baseTheme) {
|
||||||
clearTopTabsPreviewVars();
|
clearTopTabsPreviewVars();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const theme = applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent);
|
||||||
const bg = hexToHslToken(theme.colors.background);
|
const bg = hexToHslToken(theme.colors.background);
|
||||||
const fg = hexToHslToken(theme.colors.foreground);
|
const fg = hexToHslToken(theme.colors.foreground);
|
||||||
const accent = fg;
|
const accent = hexToHslToken(theme.colors.cursor);
|
||||||
const isDark = theme.type === 'dark';
|
const isDark = theme.type === 'dark';
|
||||||
const secondary = adjustLightnessToken(bg, isDark ? 6 : -5);
|
const secondary = adjustLightnessToken(bg, isDark ? 6 : -5);
|
||||||
const border = adjustLightnessToken(bg, isDark ? 12 : -10);
|
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-fg', 'hsl(var(--foreground))');
|
||||||
tabsRoot.style.setProperty('--top-tabs-muted', 'hsl(var(--muted-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-active-bg', 'hsl(var(--background))');
|
||||||
tabsRoot.style.setProperty('--top-tabs-accent', 'hsl(var(--foreground))');
|
tabsRoot.style.setProperty('--top-tabs-accent', 'hsl(var(--accent))');
|
||||||
}, [customThemes]);
|
}, [accentMode, customAccent, customThemes]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -1889,10 +1896,11 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
|||||||
|
|
||||||
const resolvedPreviewTheme = useMemo(() => {
|
const resolvedPreviewTheme = useMemo(() => {
|
||||||
const themeId = previewedOrVisibleThemeId;
|
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)
|
|| customThemes.find((theme) => theme.id === themeId)
|
||||||
|| terminalTheme;
|
|| terminalTheme;
|
||||||
}, [customThemes, previewedOrVisibleThemeId, terminalTheme]);
|
return applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent);
|
||||||
|
}, [accentMode, customAccent, customThemes, previewedOrVisibleThemeId, terminalTheme]);
|
||||||
const sessionLogConfig = useMemo(
|
const sessionLogConfig = useMemo(
|
||||||
() =>
|
() =>
|
||||||
sessionLogsEnabled && sessionLogsDir
|
sessionLogsEnabled && sessionLogsDir
|
||||||
@@ -2201,6 +2209,7 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
|||||||
style={{
|
style={{
|
||||||
['--terminal-sidepanel-bg' as never]: resolvedPreviewTheme.colors.background,
|
['--terminal-sidepanel-bg' as never]: resolvedPreviewTheme.colors.background,
|
||||||
['--terminal-sidepanel-fg' as never]: resolvedPreviewTheme.colors.foreground,
|
['--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-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%)`,
|
['--terminal-sidepanel-border' as never]: `color-mix(in srgb, ${resolvedPreviewTheme.colors.foreground} 12%, ${resolvedPreviewTheme.colors.background} 88%)`,
|
||||||
backgroundColor: 'var(--terminal-sidepanel-bg)',
|
backgroundColor: 'var(--terminal-sidepanel-bg)',
|
||||||
@@ -2223,6 +2232,9 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
|||||||
data-state={activeSidePanelTab === 'sftp' ? 'active' : 'inactive'}
|
data-state={activeSidePanelTab === 'sftp' ? 'active' : 'inactive'}
|
||||||
className="netcatty-tab h-7 w-7 rounded-md p-0 hover:bg-transparent"
|
className="netcatty-tab h-7 w-7 rounded-md p-0 hover:bg-transparent"
|
||||||
style={{
|
style={{
|
||||||
|
backgroundColor: activeSidePanelTab === 'sftp'
|
||||||
|
? 'color-mix(in srgb, var(--terminal-sidepanel-accent) 24%, transparent)'
|
||||||
|
: 'transparent',
|
||||||
color: activeSidePanelTab === 'sftp'
|
color: activeSidePanelTab === 'sftp'
|
||||||
? 'var(--terminal-sidepanel-fg)'
|
? 'var(--terminal-sidepanel-fg)'
|
||||||
: 'var(--terminal-sidepanel-muted)',
|
: 'var(--terminal-sidepanel-muted)',
|
||||||
@@ -2240,6 +2252,9 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
|||||||
data-state={activeSidePanelTab === 'scripts' ? 'active' : 'inactive'}
|
data-state={activeSidePanelTab === 'scripts' ? 'active' : 'inactive'}
|
||||||
className="netcatty-tab h-7 w-7 rounded-md p-0 hover:bg-transparent"
|
className="netcatty-tab h-7 w-7 rounded-md p-0 hover:bg-transparent"
|
||||||
style={{
|
style={{
|
||||||
|
backgroundColor: activeSidePanelTab === 'scripts'
|
||||||
|
? 'color-mix(in srgb, var(--terminal-sidepanel-accent) 24%, transparent)'
|
||||||
|
: 'transparent',
|
||||||
color: activeSidePanelTab === 'scripts'
|
color: activeSidePanelTab === 'scripts'
|
||||||
? 'var(--terminal-sidepanel-fg)'
|
? 'var(--terminal-sidepanel-fg)'
|
||||||
: 'var(--terminal-sidepanel-muted)',
|
: 'var(--terminal-sidepanel-muted)',
|
||||||
@@ -2257,6 +2272,9 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
|||||||
data-state={activeSidePanelTab === 'theme' ? 'active' : 'inactive'}
|
data-state={activeSidePanelTab === 'theme' ? 'active' : 'inactive'}
|
||||||
className="netcatty-tab h-7 w-7 rounded-md p-0 hover:bg-transparent"
|
className="netcatty-tab h-7 w-7 rounded-md p-0 hover:bg-transparent"
|
||||||
style={{
|
style={{
|
||||||
|
backgroundColor: activeSidePanelTab === 'theme'
|
||||||
|
? 'color-mix(in srgb, var(--terminal-sidepanel-accent) 24%, transparent)'
|
||||||
|
: 'transparent',
|
||||||
color: activeSidePanelTab === 'theme'
|
color: activeSidePanelTab === 'theme'
|
||||||
? 'var(--terminal-sidepanel-fg)'
|
? 'var(--terminal-sidepanel-fg)'
|
||||||
: 'var(--terminal-sidepanel-muted)',
|
: 'var(--terminal-sidepanel-muted)',
|
||||||
@@ -2274,6 +2292,9 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
|||||||
data-state={activeSidePanelTab === 'ai' ? 'active' : 'inactive'}
|
data-state={activeSidePanelTab === 'ai' ? 'active' : 'inactive'}
|
||||||
className="netcatty-tab h-7 w-7 rounded-md p-0 hover:bg-transparent"
|
className="netcatty-tab h-7 w-7 rounded-md p-0 hover:bg-transparent"
|
||||||
style={{
|
style={{
|
||||||
|
backgroundColor: activeSidePanelTab === 'ai'
|
||||||
|
? 'color-mix(in srgb, var(--terminal-sidepanel-accent) 24%, transparent)'
|
||||||
|
: 'transparent',
|
||||||
color: activeSidePanelTab === 'ai'
|
color: activeSidePanelTab === 'ai'
|
||||||
? 'var(--terminal-sidepanel-fg)'
|
? 'var(--terminal-sidepanel-fg)'
|
||||||
: 'var(--terminal-sidepanel-muted)',
|
: 'var(--terminal-sidepanel-muted)',
|
||||||
@@ -2525,6 +2546,8 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
|||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
terminalTheme={terminalTheme}
|
terminalTheme={terminalTheme}
|
||||||
followAppTerminalTheme={followAppTerminalTheme}
|
followAppTerminalTheme={followAppTerminalTheme}
|
||||||
|
accentMode={accentMode}
|
||||||
|
customAccent={customAccent}
|
||||||
terminalSettings={terminalSettings}
|
terminalSettings={terminalSettings}
|
||||||
sessionId={session.id}
|
sessionId={session.id}
|
||||||
startupCommand={session.startupCommand}
|
startupCommand={session.startupCommand}
|
||||||
@@ -2641,6 +2664,8 @@ const terminalLayerAreEqual = (prev: TerminalLayerProps, next: TerminalLayerProp
|
|||||||
prev.workspaces === next.workspaces &&
|
prev.workspaces === next.workspaces &&
|
||||||
prev.draggingSessionId === next.draggingSessionId &&
|
prev.draggingSessionId === next.draggingSessionId &&
|
||||||
prev.terminalTheme === next.terminalTheme &&
|
prev.terminalTheme === next.terminalTheme &&
|
||||||
|
prev.accentMode === next.accentMode &&
|
||||||
|
prev.customAccent === next.customAccent &&
|
||||||
prev.terminalSettings === next.terminalSettings &&
|
prev.terminalSettings === next.terminalSettings &&
|
||||||
prev.fontSize === next.fontSize &&
|
prev.fontSize === next.fontSize &&
|
||||||
prev.hotkeyScheme === next.hotkeyScheme &&
|
prev.hotkeyScheme === next.hotkeyScheme &&
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ const sshHost: Host = {
|
|||||||
protocol: "ssh",
|
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(
|
renderToStaticMarkup(
|
||||||
React.createElement(
|
React.createElement(
|
||||||
I18nProvider,
|
I18nProvider,
|
||||||
@@ -28,6 +32,7 @@ const renderToolbar = (host: Host, status: "connecting" | "connected" | "disconn
|
|||||||
onOpenSFTP: () => {},
|
onOpenSFTP: () => {},
|
||||||
onOpenScripts: () => {},
|
onOpenScripts: () => {},
|
||||||
onOpenTheme: () => {},
|
onOpenTheme: () => {},
|
||||||
|
...props,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -52,3 +57,15 @@ test("hides SFTP for local terminal sessions", () => {
|
|||||||
|
|
||||||
assert.equal(markup.includes('aria-label="Open SFTP"'), false);
|
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\)"/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
|
|||||||
const hidesSftp = isLocalTerminal || isSerialTerminal;
|
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 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 (
|
return (
|
||||||
<TooltipProvider delayDuration={500} skipDelayDuration={100} disableHoverableContent>
|
<TooltipProvider delayDuration={500} skipDelayDuration={100} disableHoverableContent>
|
||||||
@@ -111,6 +114,7 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
|
|||||||
aria-label={t("terminal.toolbar.composeBar")}
|
aria-label={t("terminal.toolbar.composeBar")}
|
||||||
aria-pressed={isComposeBarOpen}
|
aria-pressed={isComposeBarOpen}
|
||||||
onClick={onToggleComposeBar}
|
onClick={onToggleComposeBar}
|
||||||
|
style={isComposeBarOpen ? activeButtonStyle : undefined}
|
||||||
>
|
>
|
||||||
<TextCursorInput size={12} />
|
<TextCursorInput size={12} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -127,6 +131,7 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
|
|||||||
aria-label={t("terminal.toolbar.searchTerminal")}
|
aria-label={t("terminal.toolbar.searchTerminal")}
|
||||||
aria-pressed={isSearchOpen}
|
aria-pressed={isSearchOpen}
|
||||||
onClick={onToggleSearch}
|
onClick={onToggleSearch}
|
||||||
|
style={isSearchOpen ? activeButtonStyle : undefined}
|
||||||
>
|
>
|
||||||
<Search size={12} />
|
<Search size={12} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
48
domain/terminalAppearance.test.ts
Normal file
48
domain/terminalAppearance.test.ts
Normal 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);
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Host } from './models';
|
import { Host, TerminalTheme } from './models';
|
||||||
|
|
||||||
const hasLegacyStringValue = (value: string | undefined): boolean =>
|
const hasLegacyStringValue = (value: string | undefined): boolean =>
|
||||||
typeof value === 'string' && value.trim().length > 0;
|
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 =>
|
export const getTerminalThemeForUiTheme = (uiThemeId: string): string | undefined =>
|
||||||
UI_TO_TERMINAL_THEME[uiThemeId];
|
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 =>
|
export const resolveHostTerminalFontFamilyId = (host: Host | null | undefined, defaultFontFamilyId: string): string =>
|
||||||
hasHostFontFamilyOverride(host) && host?.fontFamily ? host.fontFamily : defaultFontFamilyId;
|
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 =>
|
export const resolveHostTerminalFontWeight = (host: Host | null | undefined, defaultFontWeight: number): number =>
|
||||||
hasHostFontWeightOverride(host) && host?.fontWeight != null ? host.fontWeight : defaultFontWeight;
|
hasHostFontWeightOverride(host) && host?.fontWeight != null ? host.fontWeight : defaultFontWeight;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user