From fde8351a0187d655195ebc7421db6e99c96392b7 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Thu, 21 May 2026 23:18:05 -0700 Subject: [PATCH] feat(terminal): add semantic --terminal-background/foreground CSS vars Adds terminal color tokens to the adaptive theme system, derived from the Shiki syntax theme's raw bg/fg colors. Registered in Tailwind as bg-terminal / text-terminal-foreground. Terminal panel and xterm instance now adapt automatically to whatever syntax theme the user picks. - adaptive-theme.ts: emit --terminal-background, --terminal-foreground - tailwind.config.js: register terminal.DEFAULT + terminal.foreground - TerminalInstance already reads these vars with Tokyo Night fallbacks --- .../features/terminal/TerminalInstance.tsx | 107 +++++++++--------- .../src/features/terminal/TerminalPanel.tsx | 2 +- desktop/src/shared/theme/adaptive-theme.ts | 4 + desktop/tailwind.config.js | 4 + 4 files changed, 64 insertions(+), 53 deletions(-) diff --git a/desktop/src/features/terminal/TerminalInstance.tsx b/desktop/src/features/terminal/TerminalInstance.tsx index 4ffa439f51..28bab7c878 100644 --- a/desktop/src/features/terminal/TerminalInstance.tsx +++ b/desktop/src/features/terminal/TerminalInstance.tsx @@ -8,59 +8,62 @@ import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import "@xterm/xterm/css/xterm.css"; -// Tokyo Night — high-contrast dark terminal surface. -const DARK_THEME: ITheme = { - background: "#1a1b26", - foreground: "#c0caf5", - cursor: "#c0caf5", - selectionBackground: "#33467c", - black: "#15161e", - red: "#f7768e", - green: "#9ece6a", - yellow: "#e0af68", - blue: "#7aa2f7", - magenta: "#bb9af7", - cyan: "#7dcfff", - white: "#a9b1d6", - brightBlack: "#414868", - brightRed: "#f7768e", - brightGreen: "#9ece6a", - brightYellow: "#e0af68", - brightBlue: "#7aa2f7", - brightMagenta: "#bb9af7", - brightCyan: "#7dcfff", - brightWhite: "#c0caf5", -}; - -// Light mode: terminal stays dark (high contrast against light app chrome). -// Uses the same Tokyo Night bg with slightly warmer foreground for readability. -const LIGHT_THEME: ITheme = { - background: "#1a1b26", - foreground: "#c8d3f5", - cursor: "#c8d3f5", - selectionBackground: "#2f3549", - black: "#1a1b26", - red: "#ff757f", - green: "#c3e88d", - yellow: "#ffc777", - blue: "#82aaff", - magenta: "#c099ff", - cyan: "#86e1fc", - white: "#c8d3f5", - brightBlack: "#545c7e", - brightRed: "#ff757f", - brightGreen: "#c3e88d", - brightYellow: "#ffc777", - brightBlue: "#82aaff", - brightMagenta: "#c099ff", - brightCyan: "#86e1fc", - brightWhite: "#e4f0fb", -}; - +/** + * Read the semantic terminal CSS vars and build an xterm.js ITheme. + * Falls back to a neutral dark palette if vars are not yet applied. + */ function getTerminalTheme(): ITheme { - return document.documentElement.classList.contains("dark") - ? DARK_THEME - : LIGHT_THEME; + const style = getComputedStyle(document.documentElement); + + const hslToHex = (hslValue: string): string | undefined => { + if (!hslValue?.trim()) return undefined; + // Parse "H S% L%" format from CSS var + const parts = hslValue.trim().split(/\s+/); + if (parts.length < 3) return undefined; + const h = Number.parseFloat(parts[0]); + const s = Number.parseFloat(parts[1]) / 100; + const l = Number.parseFloat(parts[2]) / 100; + + const a = s * Math.min(l, 1 - l); + const f = (n: number) => { + const k = (n + h / 30) % 12; + const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); + return Math.round(255 * color) + .toString(16) + .padStart(2, "0"); + }; + return `#${f(0)}${f(8)}${f(4)}`; + }; + + const bg = + hslToHex(style.getPropertyValue("--terminal-background")) ?? "#1a1b26"; + const fg = + hslToHex(style.getPropertyValue("--terminal-foreground")) ?? "#c0caf5"; + + const isDark = document.documentElement.classList.contains("dark"); + + return { + background: bg, + foreground: fg, + cursor: fg, + selectionBackground: isDark ? "#33467c" : "#2f3549", + black: bg, + red: isDark ? "#f7768e" : "#ff757f", + green: isDark ? "#9ece6a" : "#c3e88d", + yellow: isDark ? "#e0af68" : "#ffc777", + blue: isDark ? "#7aa2f7" : "#82aaff", + magenta: isDark ? "#bb9af7" : "#c099ff", + cyan: isDark ? "#7dcfff" : "#86e1fc", + white: fg, + brightBlack: isDark ? "#414868" : "#545c7e", + brightRed: isDark ? "#f7768e" : "#ff757f", + brightGreen: isDark ? "#9ece6a" : "#c3e88d", + brightYellow: isDark ? "#e0af68" : "#ffc777", + brightBlue: isDark ? "#7aa2f7" : "#82aaff", + brightMagenta: isDark ? "#bb9af7" : "#c099ff", + brightCyan: isDark ? "#7dcfff" : "#86e1fc", + brightWhite: fg, + }; } type TerminalDataPayload = { diff --git a/desktop/src/features/terminal/TerminalPanel.tsx b/desktop/src/features/terminal/TerminalPanel.tsx index 251e7f8841..503b0a164e 100644 --- a/desktop/src/features/terminal/TerminalPanel.tsx +++ b/desktop/src/features/terminal/TerminalPanel.tsx @@ -90,7 +90,7 @@ export function TerminalPanel({ channelId, isOpen }: TerminalPanelProps) { onPointerDown={handleResizeStart} > {/* Terminal content */} -
+
diff --git a/desktop/src/shared/theme/adaptive-theme.ts b/desktop/src/shared/theme/adaptive-theme.ts index 43596c2e32..df4ea9ab32 100644 --- a/desktop/src/shared/theme/adaptive-theme.ts +++ b/desktop/src/shared/theme/adaptive-theme.ts @@ -258,6 +258,10 @@ export function createThemeVars( "--status-deleted": accentRed, "--status-modified": accentOrange, + // Terminal + "--terminal-background": hexToHsl(syntaxBg), + "--terminal-foreground": hexToHsl(syntaxFg), + // Warning "--ui-warning": accentOrange, "--ui-warning-bg": overlay(accentOrange, isDark ? 0.1 : 0.08), diff --git a/desktop/tailwind.config.js b/desktop/tailwind.config.js index 1ce6562a10..20b72b0986 100644 --- a/desktop/tailwind.config.js +++ b/desktop/tailwind.config.js @@ -45,6 +45,10 @@ export default { border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", + terminal: { + DEFAULT: "hsl(var(--terminal-background))", + foreground: "hsl(var(--terminal-foreground))", + }, sidebar: { DEFAULT: "hsl(var(--sidebar-background))", foreground: "hsl(var(--sidebar-foreground))",