From 68b3da1924e3485911458221513d5f647a52adfe Mon Sep 17 00:00:00 2001 From: RaspberryLee <35322749+RaspberryLee@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:39:15 +0800 Subject: [PATCH] feat(desktop): sync terminal chrome with app theme --- desktop/src/pages/TerminalSettings.test.tsx | 39 +++- desktop/src/pages/TerminalSettings.tsx | 197 +++++++++++++------- desktop/src/theme/globals.css | 32 ++-- 3 files changed, 184 insertions(+), 84 deletions(-) diff --git a/desktop/src/pages/TerminalSettings.test.tsx b/desktop/src/pages/TerminalSettings.test.tsx index c98b563859..42e5ba5b87 100644 --- a/desktop/src/pages/TerminalSettings.test.tsx +++ b/desktop/src/pages/TerminalSettings.test.tsx @@ -3,6 +3,7 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import '@testing-library/jest-dom' import { beforeEach, describe, expect, it, vi } from 'vitest' import { useSettingsStore } from '../stores/settingsStore' +import { useUIStore } from '../stores/uiStore' import { destroyTerminalRuntime } from '../lib/terminalRuntime' import { browserHost } from '../lib/desktopHost/browserHost' @@ -22,6 +23,7 @@ const terminalMocks = vi.hoisted(() => { getSelection: vi.fn(), hasSelection: vi.fn(), paste: vi.fn(), + options: { theme: {} }, } const fitInstance = { fit: vi.fn(), @@ -30,6 +32,7 @@ const terminalMocks = vi.hoisted(() => { available: false, terminalInstance, fitInstance, + terminalOptions: [] as unknown[], spawn: vi.fn(), write: vi.fn(), resize: vi.fn(), @@ -42,7 +45,11 @@ const terminalMocks = vi.hoisted(() => { }) vi.mock('@xterm/xterm', () => ({ - Terminal: vi.fn(() => terminalMocks.terminalInstance), + Terminal: vi.fn((options) => { + terminalMocks.terminalOptions.push(options) + terminalMocks.terminalInstance.options = options + return terminalMocks.terminalInstance + }), })) vi.mock('@xterm/addon-fit', () => ({ @@ -69,6 +76,7 @@ describe('TerminalSettings', () => { beforeEach(() => { vi.restoreAllMocks() useSettingsStore.setState({ locale: 'en' }) + useUIStore.getState().setTheme('white') useSettingsStore.setState({ desktopTerminal: { startupShell: 'system', @@ -97,6 +105,8 @@ describe('TerminalSettings', () => { terminalMocks.terminalInstance.getSelection.mockReset() terminalMocks.terminalInstance.hasSelection.mockReset() terminalMocks.terminalInstance.paste.mockClear() + terminalMocks.terminalInstance.options = { theme: {} } + terminalMocks.terminalOptions.length = 0 terminalMocks.terminalInstance.getSelection.mockReturnValue('') terminalMocks.terminalInstance.hasSelection.mockReturnValue(false) terminalMocks.fitInstance.fit.mockClear() @@ -190,10 +200,37 @@ describe('TerminalSettings', () => { await waitFor(() => expect(terminalMocks.spawn).toHaveBeenCalled()) expect(screen.getByTestId('settings-terminal-toolbar')).toHaveTextContent('/bin/zsh') + expect(screen.getByTestId('settings-terminal-toolbar')).toHaveAttribute('data-terminal-chrome', 'integrated') + expect(screen.getByTestId('terminal-toolbar-identity')).toBeInTheDocument() + expect(document.querySelector('[class*="color-terminal-danger"]')).not.toBeInTheDocument() + expect(document.querySelector('[class*="color-terminal-warning"]')).not.toBeInTheDocument() expect(screen.getByTestId('settings-terminal-frame')).toBeInTheDocument() expect(screen.queryByText('Host shell')).not.toBeInTheDocument() }) + it('uses the app theme for xterm and updates a running terminal when the theme changes', async () => { + terminalMocks.available = true + + render() + + await waitFor(() => expect(terminalMocks.spawn).toHaveBeenCalled()) + expect(terminalMocks.terminalOptions.at(-1)).toMatchObject({ + theme: { + background: '#FFFFFF', + foreground: '#242424', + }, + }) + + act(() => useUIStore.getState().setTheme('dark')) + + await waitFor(() => { + expect(terminalMocks.terminalInstance.options.theme).toMatchObject({ + background: '#121212', + foreground: '#D7D2D0', + }) + }) + }) + it('shows setup guidance from the terminal info button', () => { render() diff --git a/desktop/src/pages/TerminalSettings.tsx b/desktop/src/pages/TerminalSettings.tsx index 500c6e863c..2ca6759067 100644 --- a/desktop/src/pages/TerminalSettings.tsx +++ b/desktop/src/pages/TerminalSettings.tsx @@ -1,12 +1,23 @@ import { useCallback, useEffect, useId, useMemo, useRef, useState, type KeyboardEvent, type WheelEvent } from 'react' -import { Info } from 'lucide-react' +import { + Eraser, + ExternalLink, + Info, + Plus, + RotateCcw, + SquareTerminal, + X, + type LucideIcon, +} from 'lucide-react' +import type { ITheme } from '@xterm/xterm' import { useTranslation, type TranslationKey } from '../i18n' import { terminalApi } from '../api/terminal' import { useSettingsStore } from '../stores/settingsStore' +import { useUIStore } from '../stores/uiStore' import { Dropdown } from '../components/shared/Dropdown' import { Input } from '../components/shared/Input' import { Button } from '../components/shared/Button' -import type { DesktopTerminalStartupShell } from '../types/settings' +import type { DesktopTerminalStartupShell, ThemeMode } from '../types/settings' import { getDesktopHost } from '../lib/desktopHost' import { attachTerminalRuntime, @@ -29,6 +40,57 @@ const STATUS_LABEL_KEYS: Record = { unavailable: 'settings.terminal.status.unavailable', } +function getTerminalTheme(theme: ThemeMode): ITheme { + if (theme === 'dark') { + return { + background: '#121212', + foreground: '#D7D2D0', + cursor: '#F5F5F5', + selectionBackground: '#4A4746', + black: '#1F1F1F', + red: '#FF6D67', + green: '#7EF18A', + yellow: '#F8C55F', + blue: '#77A8FF', + magenta: '#D699FF', + cyan: '#61D6D6', + white: '#D7D2D0', + brightBlack: '#8F8683', + brightRed: '#FF8A85', + brightGreen: '#9FF7A7', + brightYellow: '#FFDD7A', + brightBlue: '#A6C5FF', + brightMagenta: '#E3B8FF', + brightCyan: '#8CEEEE', + brightWhite: '#FFFFFF', + } + } + + const warm = theme === 'light' + return { + background: warm ? '#FBFAF6' : '#FFFFFF', + foreground: warm ? '#2B2926' : '#242424', + cursor: warm ? '#2B2926' : '#242424', + selectionBackground: warm ? '#DDD9CF' : '#DDE8F7', + black: '#242424', + red: '#C94747', + green: '#237B4B', + yellow: '#8A6200', + blue: '#2563EB', + magenta: '#7C3AED', + cyan: '#087F8C', + white: '#6B6B6B', + brightBlack: '#737373', + brightRed: '#DC4C4C', + brightGreen: '#16845B', + brightYellow: '#9A6C00', + brightBlue: '#1D67E8', + brightMagenta: '#8B4BE8', + brightCyan: '#057A87', + brightWhite: '#171717', + } +} + function findScrollableAncestor(element: HTMLElement, deltaY: number): HTMLElement | null { let parent = element.parentElement while (parent) { @@ -74,6 +136,9 @@ export function TerminalSettings({ const t = useTranslation() const desktopTerminal = useSettingsStore((state) => state.desktopTerminal) const setDesktopTerminal = useSettingsStore((state) => state.setDesktopTerminal) + const theme = useUIStore((state) => state.theme) + const themeRef = useRef(theme) + themeRef.current = theme const hostRef = useRef(null) const lifecycleVersionRef = useRef(0) const localRuntimeIdRef = useRef(null) @@ -226,28 +291,7 @@ export function TerminalSettings({ fontSize: 12, lineHeight: 1.25, scrollback: 4000, - theme: { - background: '#121212', - foreground: '#d7d2d0', - cursor: '#ffb59f', - selectionBackground: '#5f4a40', - black: '#1f1f1f', - red: '#ff6d67', - green: '#7ef18a', - yellow: '#f8c55f', - blue: '#77a8ff', - magenta: '#d699ff', - cyan: '#61d6d6', - white: '#d7d2d0', - brightBlack: '#8f8683', - brightRed: '#ff8a85', - brightGreen: '#9ff7a7', - brightYellow: '#ffdd7a', - brightBlue: '#a6c5ff', - brightMagenta: '#e3b8ff', - brightCyan: '#8ceeee', - brightWhite: '#ffffff', - }, + theme: getTerminalTheme(themeRef.current), }) fit = new FitAddonModule.FitAddon() const activeTerminal = terminal @@ -376,6 +420,11 @@ export function TerminalSettings({ } }, [active, resizeSession]) + useEffect(() => { + if (!runtime.terminal) return + runtime.terminal.options.theme = getTerminalTheme(theme) + }, [runtime, theme]) + const clearTerminal = () => { runtime.terminal?.clear() } @@ -443,19 +492,24 @@ export function TerminalSettings({ return (
-
-
+
{onOpenInTab && ( - + /> )} {onNewTerminal && ( - + /> )} - - + /> {onClose && ( - + /> )}
- {error && (
{error} @@ -624,7 +662,7 @@ export function TerminalSettings({ data-testid="settings-terminal-frame" onKeyDownCapture={handleTerminalKeyDownCapture} onWheelCapture={handleTerminalWheelCapture} - className="min-h-0 flex-1 overflow-hidden rounded-[var(--radius-sm)] border border-[var(--color-terminal-border)] bg-[var(--color-terminal-bg)] shadow-[var(--shadow-dropdown)]" + className={`${docked ? 'mt-2 rounded-[var(--radius-lg)] shadow-none' : 'rounded-[var(--radius-lg)] shadow-[var(--shadow-dropdown)]'} min-h-0 flex-1 overflow-hidden border border-[var(--color-terminal-border)] bg-[var(--color-terminal-bg)]`} >
void + disabled?: boolean +}) { + return ( + + ) +} + function StatusPill({ status, label, compact = false }: { status: TerminalStatus; label: string; compact?: boolean }) { const color = status === 'running' diff --git a/desktop/src/theme/globals.css b/desktop/src/theme/globals.css index adb6fd86b3..2d586e7f9d 100644 --- a/desktop/src/theme/globals.css +++ b/desktop/src/theme/globals.css @@ -574,14 +574,14 @@ --color-diff-title-border: #DAC1BA; /* Terminal */ - --color-terminal-header: #2D2D2D; - --color-terminal-bg: #1E1E1E; - --color-terminal-border: #1A1A1A; - --color-terminal-fg: #D4D4D4; - --color-terminal-muted: #999999; - --color-terminal-accent: #28C840; - --color-terminal-danger: #FF5F57; - --color-terminal-warning: #FEBC2E; + --color-terminal-header: rgba(255, 255, 255, 0.82); + --color-terminal-bg: #FBFAF6; + --color-terminal-border: rgba(214, 211, 203, 0.72); + --color-terminal-fg: #2B2926; + --color-terminal-muted: #7B7770; + --color-terminal-accent: #237B4B; + --color-terminal-danger: #C94747; + --color-terminal-warning: #8A6200; /* Misc */ --color-window-close-hover: #E81123; @@ -783,14 +783,14 @@ --color-diff-title-color: #667085; --color-diff-title-border: #DDE3EA; - --color-terminal-header: #1F2937; - --color-terminal-bg: #111827; - --color-terminal-border: #0F172A; - --color-terminal-fg: #E5E7EB; - --color-terminal-muted: #9CA3AF; - --color-terminal-accent: #32D583; - --color-terminal-danger: #F97066; - --color-terminal-warning: #FDB022; + --color-terminal-header: rgba(255, 255, 255, 0.92); + --color-terminal-bg: #FFFFFF; + --color-terminal-border: rgba(227, 227, 227, 0.88); + --color-terminal-fg: #242424; + --color-terminal-muted: #737373; + --color-terminal-accent: #237B4B; + --color-terminal-danger: #C94747; + --color-terminal-warning: #8A6200; --color-window-close-hover: #E5484D; --color-selection-bg: rgba(255, 219, 208, 0.9);