[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
323
components/terminalLayer/useTerminalThemePanelState.ts
Normal file
323
components/terminalLayer/useTerminalThemePanelState.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
import { startTransition, useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { useAppearanceChromeStore } from '../../application/state/appearanceChromeStore';
|
||||
import type { ResolvedAppearance, TerminalAppearanceHostScope } from '../../domain/terminalAppearanceRuntime';
|
||||
import {
|
||||
clearHostFontFamilyOverride,
|
||||
clearHostFontSizeOverride,
|
||||
clearHostFontWeightOverride,
|
||||
clearHostThemeOverride,
|
||||
hasHostFontFamilyOverride,
|
||||
hasHostFontSizeOverride,
|
||||
hasHostFontWeightOverride,
|
||||
hasHostThemeOverride,
|
||||
resolveHostTerminalFontFamilyId,
|
||||
resolveHostTerminalFontSize,
|
||||
resolveHostTerminalFontWeight,
|
||||
resolveHostTerminalThemeId,
|
||||
} from '../../domain/terminalAppearance';
|
||||
import { isSavedVaultHost } from '../../domain/ephemeralHosts';
|
||||
import { isSameResolvedTerminalFont } from '../../infrastructure/config/fonts';
|
||||
import type { Host, TerminalSession, TerminalTheme, Workspace } from '../../types';
|
||||
import { getScopedTopTabsThemeId } from '../terminalTopTabsTheme';
|
||||
import type { SidePanelTab } from './TerminalLayerSupport';
|
||||
|
||||
const navigatorPlatform = typeof navigator !== 'undefined' ? navigator.platform : '';
|
||||
|
||||
interface UseTerminalThemePanelStateOptions {
|
||||
activeSession: TerminalSession | undefined;
|
||||
activeSidePanelTab: SidePanelTab | null;
|
||||
activeWorkspace: Workspace | undefined;
|
||||
clearIntent: () => void;
|
||||
followAppTerminalTheme: boolean;
|
||||
focusedSessionId: string | undefined;
|
||||
fontSize: number;
|
||||
hostMap: Map<string, Host>;
|
||||
isSidePanelOpenForCurrentTab: boolean;
|
||||
isVisible: boolean;
|
||||
onUpdateHost: (host: Host) => void;
|
||||
onUpdateTerminalFontFamilyId?: (fontFamilyId: string) => void;
|
||||
onUpdateTerminalFontSize?: (fontSize: number) => void;
|
||||
onUpdateTerminalFontWeight?: (fontWeight: number) => void;
|
||||
onUpdateTerminalThemeId?: (themeId: string) => void;
|
||||
onUpdateSessionFontSize?: (sessionId: string, fontSize: number) => void;
|
||||
onClearSessionFontSizeOverride?: (sessionId: string) => void;
|
||||
pickTheme: (themeId: string) => void;
|
||||
resolveFocusedAppearance: (hostScope: TerminalAppearanceHostScope) => ResolvedAppearance;
|
||||
sessionHostsMap: Map<string, Host>;
|
||||
terminalFontFamilyId: string;
|
||||
terminalSettings?: { fontWeight?: number };
|
||||
terminalTheme: TerminalTheme;
|
||||
}
|
||||
|
||||
export function useTerminalThemePanelState({
|
||||
activeSession,
|
||||
activeSidePanelTab,
|
||||
activeWorkspace,
|
||||
clearIntent,
|
||||
followAppTerminalTheme,
|
||||
focusedSessionId,
|
||||
fontSize,
|
||||
hostMap,
|
||||
isSidePanelOpenForCurrentTab,
|
||||
isVisible,
|
||||
onUpdateHost,
|
||||
onUpdateTerminalFontFamilyId,
|
||||
onUpdateTerminalFontSize,
|
||||
onUpdateTerminalFontWeight,
|
||||
onUpdateTerminalThemeId,
|
||||
onUpdateSessionFontSize,
|
||||
onClearSessionFontSizeOverride,
|
||||
pickTheme,
|
||||
resolveFocusedAppearance,
|
||||
sessionHostsMap,
|
||||
terminalFontFamilyId,
|
||||
terminalSettings,
|
||||
terminalTheme,
|
||||
}: UseTerminalThemePanelStateOptions) {
|
||||
useEffect(() => {
|
||||
if (isSidePanelOpenForCurrentTab) {
|
||||
if (!followAppTerminalTheme && activeSidePanelTab !== 'theme') {
|
||||
clearIntent();
|
||||
}
|
||||
return;
|
||||
}
|
||||
clearIntent();
|
||||
}, [activeSidePanelTab, clearIntent, followAppTerminalTheme, isSidePanelOpenForCurrentTab]);
|
||||
|
||||
const focusedHost = useMemo((): Host | null => {
|
||||
if (activeWorkspace && focusedSessionId) {
|
||||
return sessionHostsMap.get(focusedSessionId) ?? null;
|
||||
}
|
||||
if (activeSession) {
|
||||
return sessionHostsMap.get(activeSession.id) ?? null;
|
||||
}
|
||||
return null;
|
||||
}, [activeWorkspace, focusedSessionId, activeSession, sessionHostsMap]);
|
||||
|
||||
const isFocusedHostLocal = useMemo(() => {
|
||||
return focusedHost?.protocol === 'local' || !!focusedHost?.id?.startsWith('local-');
|
||||
}, [focusedHost]);
|
||||
|
||||
const isFocusedHostEphemeral = useMemo(() => {
|
||||
if (isFocusedHostLocal) return true;
|
||||
if (!focusedHost) return true;
|
||||
return !isSavedVaultHost(hostMap.get(focusedHost.id));
|
||||
}, [focusedHost, isFocusedHostLocal, hostMap]);
|
||||
|
||||
const rawFocusedHost = useMemo(() => {
|
||||
if (!focusedHost) return null;
|
||||
return hostMap.get(focusedHost.id) ?? null;
|
||||
}, [focusedHost, hostMap]);
|
||||
|
||||
const focusedHostScope = useMemo((): TerminalAppearanceHostScope => ({
|
||||
host: focusedHost,
|
||||
isEphemeral: isFocusedHostEphemeral,
|
||||
}), [focusedHost, isFocusedHostEphemeral]);
|
||||
|
||||
// Subscribe to accent store so compose-bar / manual chrome recompute when
|
||||
// color-picker changes, even though resolveFocusedAppearance identity is
|
||||
// intentionally stable across accent drag (and TerminalLayer memo ignores
|
||||
// accent props).
|
||||
const appearanceChrome = useAppearanceChromeStore();
|
||||
const focusedAppearance = useMemo(() => {
|
||||
// resolveFocusedAppearance reads accent from the chrome store; pin these
|
||||
// deps so compose-bar chrome updates while focusedHostScope stays stable.
|
||||
void appearanceChrome.accentMode;
|
||||
void appearanceChrome.customAccent;
|
||||
return resolveFocusedAppearance(focusedHostScope);
|
||||
}, [
|
||||
appearanceChrome.accentMode,
|
||||
appearanceChrome.customAccent,
|
||||
focusedHostScope,
|
||||
resolveFocusedAppearance,
|
||||
]);
|
||||
|
||||
const previewTargetSessionId = activeWorkspace?.focusedSessionId ?? activeSession?.id ?? null;
|
||||
|
||||
const focusedThemeId = resolveHostTerminalThemeId(focusedHost, terminalTheme.id);
|
||||
const focusedFontFamilyId = resolveHostTerminalFontFamilyId(focusedHost, terminalFontFamilyId);
|
||||
const focusedFontSize = resolveHostTerminalFontSize(focusedHost, fontSize);
|
||||
const focusedThemeOverridden = hasHostThemeOverride(focusedHost);
|
||||
const focusedFontFamilyOverridden = hasHostFontFamilyOverride(focusedHost);
|
||||
const focusedFontSizeOverridden = hasHostFontSizeOverride(focusedHost);
|
||||
const focusedFontWeight = resolveHostTerminalFontWeight(focusedHost, terminalSettings?.fontWeight ?? 400);
|
||||
const focusedFontWeightOverridden = hasHostFontWeightOverride(focusedHost);
|
||||
const visibleFocusedThemeId = followAppTerminalTheme ? terminalTheme.id : focusedThemeId;
|
||||
const listSelectedThemeId = followAppTerminalTheme
|
||||
? terminalTheme.id
|
||||
: focusedAppearance.themeId;
|
||||
const previewedOrVisibleThemeId = listSelectedThemeId;
|
||||
const resolvedPreviewTheme = focusedAppearance.theme;
|
||||
|
||||
const activeTopTabsThemeId = useMemo(
|
||||
() => getScopedTopTabsThemeId({
|
||||
activeSidePanelTab,
|
||||
activeThemePreviewId: null,
|
||||
activeWorkspace,
|
||||
followAppTerminalTheme,
|
||||
isVisible,
|
||||
previewTargetSessionId,
|
||||
previewedOrVisibleThemeId,
|
||||
resolveSessionThemeId: (sessionId) => {
|
||||
const host = sessionHostsMap.get(sessionId) ?? null;
|
||||
const isEphemeral = !host || !isSavedVaultHost(hostMap.get(host.id));
|
||||
return resolveFocusedAppearance({ host, isEphemeral }).themeId;
|
||||
},
|
||||
}),
|
||||
[
|
||||
activeSidePanelTab,
|
||||
activeWorkspace,
|
||||
followAppTerminalTheme,
|
||||
hostMap,
|
||||
isVisible,
|
||||
previewTargetSessionId,
|
||||
previewedOrVisibleThemeId,
|
||||
resolveFocusedAppearance,
|
||||
sessionHostsMap,
|
||||
],
|
||||
);
|
||||
|
||||
const handleThemeChangeForFocusedSession = useCallback((themeId: string) => {
|
||||
if (themeId === listSelectedThemeId) return;
|
||||
if (!focusedHost && !followAppTerminalTheme) return;
|
||||
|
||||
if (followAppTerminalTheme) {
|
||||
pickTheme(themeId);
|
||||
return;
|
||||
}
|
||||
|
||||
pickTheme(themeId, {
|
||||
followApp: false,
|
||||
scopeHostId: rawFocusedHost?.id ?? focusedHost?.id ?? null,
|
||||
});
|
||||
if (isFocusedHostEphemeral) {
|
||||
onUpdateTerminalThemeId?.(themeId);
|
||||
} else if (rawFocusedHost) {
|
||||
onUpdateHost({ ...rawFocusedHost, theme: themeId, themeOverride: true });
|
||||
}
|
||||
}, [
|
||||
focusedHost,
|
||||
followAppTerminalTheme,
|
||||
isFocusedHostEphemeral,
|
||||
listSelectedThemeId,
|
||||
onUpdateHost,
|
||||
onUpdateTerminalThemeId,
|
||||
pickTheme,
|
||||
rawFocusedHost,
|
||||
]);
|
||||
|
||||
const handleThemeResetForFocusedSession = useCallback(() => {
|
||||
clearIntent();
|
||||
if (!focusedHost || isFocusedHostEphemeral || !rawFocusedHost) return;
|
||||
onUpdateHost(clearHostThemeOverride(rawFocusedHost));
|
||||
}, [clearIntent, focusedHost, isFocusedHostEphemeral, onUpdateHost, rawFocusedHost]);
|
||||
|
||||
const handleFontFamilyChangeForFocusedSession = useCallback((fontFamilyId: string) => {
|
||||
if (!focusedHost || isSameResolvedTerminalFont(fontFamilyId, focusedFontFamilyId, navigatorPlatform)) return;
|
||||
startTransition(() => {
|
||||
if (isFocusedHostEphemeral) {
|
||||
onUpdateTerminalFontFamilyId?.(fontFamilyId);
|
||||
return;
|
||||
}
|
||||
if (rawFocusedHost) {
|
||||
onUpdateHost({ ...rawFocusedHost, fontFamily: fontFamilyId, fontFamilyOverride: true });
|
||||
}
|
||||
});
|
||||
}, [focusedHost, focusedFontFamilyId, isFocusedHostEphemeral, onUpdateTerminalFontFamilyId, onUpdateHost, rawFocusedHost]);
|
||||
|
||||
const handleFontFamilyResetForFocusedSession = useCallback(() => {
|
||||
if (!focusedHost || isFocusedHostEphemeral || !rawFocusedHost) return;
|
||||
onUpdateHost(clearHostFontFamilyOverride(rawFocusedHost));
|
||||
}, [focusedHost, isFocusedHostEphemeral, onUpdateHost, rawFocusedHost]);
|
||||
|
||||
const handleFontSizeChangeForFocusedSession = useCallback((newFontSize: number) => {
|
||||
if (!focusedHost || newFontSize === focusedFontSize) return;
|
||||
startTransition(() => {
|
||||
if (activeWorkspace && focusedSessionId) {
|
||||
onUpdateSessionFontSize?.(focusedSessionId, newFontSize);
|
||||
return;
|
||||
}
|
||||
if (isFocusedHostEphemeral) {
|
||||
// Ephemeral hosts cannot persist host-level overrides; keep the
|
||||
// change per-session (same path Ctrl+zoom uses) when possible.
|
||||
const targetSessionId = focusedSessionId ?? activeSession?.id;
|
||||
if (targetSessionId) {
|
||||
onUpdateSessionFontSize?.(targetSessionId, newFontSize);
|
||||
return;
|
||||
}
|
||||
onUpdateTerminalFontSize?.(newFontSize);
|
||||
return;
|
||||
}
|
||||
if (rawFocusedHost) {
|
||||
onUpdateHost({ ...rawFocusedHost, fontSize: newFontSize, fontSizeOverride: true });
|
||||
}
|
||||
});
|
||||
}, [activeSession, activeWorkspace, focusedHost, focusedFontSize, focusedSessionId, isFocusedHostEphemeral, onUpdateSessionFontSize, onUpdateTerminalFontSize, onUpdateHost, rawFocusedHost]);
|
||||
|
||||
const handleFontSizeResetForFocusedSession = useCallback(() => {
|
||||
if (!focusedHost) return;
|
||||
if (activeWorkspace && focusedSessionId) {
|
||||
onClearSessionFontSizeOverride?.(focusedSessionId);
|
||||
return;
|
||||
}
|
||||
if (isFocusedHostEphemeral) {
|
||||
const targetSessionId = focusedSessionId ?? activeSession?.id;
|
||||
if (targetSessionId) onClearSessionFontSizeOverride?.(targetSessionId);
|
||||
return;
|
||||
}
|
||||
if (!rawFocusedHost) return;
|
||||
onUpdateHost(clearHostFontSizeOverride(rawFocusedHost));
|
||||
}, [activeSession, activeWorkspace, focusedHost, focusedSessionId, isFocusedHostEphemeral, onClearSessionFontSizeOverride, onUpdateHost, rawFocusedHost]);
|
||||
|
||||
const handleFontWeightChangeForFocusedSession = useCallback((newFontWeight: number) => {
|
||||
if (!focusedHost || newFontWeight === focusedFontWeight) return;
|
||||
startTransition(() => {
|
||||
if (isFocusedHostEphemeral) {
|
||||
onUpdateTerminalFontWeight?.(newFontWeight);
|
||||
return;
|
||||
}
|
||||
const rawHost = hostMap.get(focusedHost.id);
|
||||
if (rawHost) {
|
||||
onUpdateHost({ ...rawHost, fontWeight: newFontWeight, fontWeightOverride: true });
|
||||
}
|
||||
});
|
||||
}, [focusedHost, focusedFontWeight, isFocusedHostEphemeral, onUpdateTerminalFontWeight, onUpdateHost, hostMap]);
|
||||
|
||||
const handleFontWeightResetForFocusedSession = useCallback(() => {
|
||||
if (!focusedHost || isFocusedHostEphemeral) return;
|
||||
const rawHost = hostMap.get(focusedHost.id);
|
||||
if (rawHost) {
|
||||
onUpdateHost(clearHostFontWeightOverride(rawHost));
|
||||
}
|
||||
}, [focusedHost, isFocusedHostEphemeral, onUpdateHost, hostMap]);
|
||||
|
||||
const composeBarThemeColors = useMemo(() => {
|
||||
if (!activeWorkspace || !focusedSessionId) return terminalTheme.colors;
|
||||
return resolvedPreviewTheme.colors;
|
||||
}, [activeWorkspace, focusedSessionId, resolvedPreviewTheme.colors, terminalTheme.colors]);
|
||||
|
||||
return {
|
||||
activeTopTabsThemeId,
|
||||
composeBarThemeColors,
|
||||
focusedFontFamilyId,
|
||||
focusedFontFamilyOverridden,
|
||||
focusedFontSize,
|
||||
focusedFontSizeOverridden,
|
||||
focusedFontWeight,
|
||||
focusedFontWeightOverridden,
|
||||
focusedThemeOverridden,
|
||||
handleFontFamilyChangeForFocusedSession,
|
||||
handleFontFamilyResetForFocusedSession,
|
||||
handleFontSizeChangeForFocusedSession,
|
||||
handleFontSizeResetForFocusedSession,
|
||||
handleFontWeightChangeForFocusedSession,
|
||||
handleFontWeightResetForFocusedSession,
|
||||
handleThemeChangeForFocusedSession,
|
||||
handleThemeResetForFocusedSession,
|
||||
previewedOrVisibleThemeId,
|
||||
previewTargetSessionId,
|
||||
resolvedPreviewTheme,
|
||||
visibleFocusedThemeId,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user