diff --git a/package.json b/package.json
index 47f457f..a0ab678 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "open-terminal",
"productName": "OpenTerminal",
- "version": "1.0.1",
+ "version": "1.0.2",
"description": "Open-source terminal with SSH, split panes, themes and fonts",
"main": "out/main/index.js",
"author": "Bill",
diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx
index 5f44934..c4cb907 100644
--- a/src/renderer/src/App.tsx
+++ b/src/renderer/src/App.tsx
@@ -4,6 +4,8 @@ import { SettingsDialog } from '@renderer/settings/SettingsDialog'
import { TransferPanel } from '@renderer/sftp/TransferPanel'
import { useSettingsStore } from '@renderer/settings/store'
import { getThemeById } from '@shared/theme'
+import { applyChromeTheme, applyTabAccent } from '@renderer/theme/chrome'
+import appIconUrl from '../../../build/icon.png'
/** Custom window title bar: draggable strip themed with the active terminal
* theme; native min/max/close buttons come from BrowserWindow titleBarOverlay. */
@@ -15,6 +17,7 @@ function TitleBar(): React.JSX.Element {
className="app-titlebar"
style={{ background: theme.colors.background, color: theme.colors.foreground }}
>
+
OpenTerminal
)
@@ -22,12 +25,25 @@ function TitleBar(): React.JSX.Element {
export default function App(): React.JSX.Element {
const hydrate = useSettingsStore((s) => s.hydrate)
+ const themeId = useSettingsStore((s) => s.settings.terminal.themeId)
+ const customThemes = useSettingsStore((s) => s.settings.customThemes)
+ const tabAccentColor = useSettingsStore((s) => s.settings.terminal.tabAccentColor)
const [settingsOpen, setSettingsOpen] = useState(false)
useEffect(() => {
void hydrate()
}, [hydrate])
+ // Sidebar / tab bars / dividers follow the active terminal theme.
+ useEffect(() => {
+ applyChromeTheme(getThemeById(themeId, customThemes).colors)
+ }, [themeId, customThemes])
+
+ // Active-tab accent color (rail indicator, tab outline, SSH badges).
+ useEffect(() => {
+ applyTabAccent(tabAccentColor)
+ }, [tabAccentColor])
+
return (
diff --git a/src/renderer/src/connections/connections.css b/src/renderer/src/connections/connections.css
index 62476b3..8a2027c 100644
--- a/src/renderer/src/connections/connections.css
+++ b/src/renderer/src/connections/connections.css
@@ -346,7 +346,7 @@
}
.connections-local-item.is-active {
- background: rgba(63, 185, 80, 0.12);
+ background: color-mix(in srgb, var(--tab-accent, #3fb950) 12%, transparent);
}
.connections-local-name {
diff --git a/src/renderer/src/env.d.ts b/src/renderer/src/env.d.ts
index e8e6140..40cab1c 100644
--- a/src/renderer/src/env.d.ts
+++ b/src/renderer/src/env.d.ts
@@ -1,3 +1,5 @@
+///
+
import type { AppApi } from '../shared/api'
declare global {
diff --git a/src/renderer/src/global.css b/src/renderer/src/global.css
index 5551b7f..e2f001b 100644
--- a/src/renderer/src/global.css
+++ b/src/renderer/src/global.css
@@ -20,16 +20,16 @@ body,
font-size: 13px;
}
-/* App-wide slim dark scrollbars: the native Windows style renders light
+/* App-wide slim slate scrollbars: the native Windows style renders light
tracks + arrow buttons that look broken on the dark surface. */
* {
scrollbar-width: thin;
- scrollbar-color: rgba(121, 121, 121, 0.45) transparent;
+ scrollbar-color: rgba(125, 145, 170, 0.5) transparent;
}
*::-webkit-scrollbar {
- width: 10px;
- height: 10px;
+ width: 12px;
+ height: 12px;
}
*::-webkit-scrollbar-track {
@@ -37,12 +37,17 @@ body,
}
*::-webkit-scrollbar-thumb {
- background: rgba(121, 121, 121, 0.45);
- border-radius: 5px;
+ background: rgba(125, 145, 170, 0.5);
+ border: 3px solid transparent;
+ border-radius: 6px;
+ background-clip: padding-box;
+ min-height: 28px;
}
*::-webkit-scrollbar-thumb:hover {
- background: rgba(160, 160, 160, 0.6);
+ background: rgba(150, 172, 200, 0.7);
+ border: 3px solid transparent;
+ background-clip: padding-box;
}
*::-webkit-scrollbar-button,
@@ -80,6 +85,14 @@ button {
letter-spacing: 0.2px;
}
+.app-titlebar-icon {
+ width: 16px;
+ height: 16px;
+ margin-right: 7px;
+ border-radius: 4px;
+ pointer-events: none;
+}
+
.app-titlebar-title {
opacity: 0.75;
font-weight: 500;
diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx
index d707d1f..a7fdabc 100644
--- a/src/renderer/src/main.tsx
+++ b/src/renderer/src/main.tsx
@@ -1,12 +1,14 @@
import ReactDOM from 'react-dom/client'
-import { useEffect } from 'react'
+import { useEffect, type ReactNode } from 'react'
import { App as AntdApp, ConfigProvider, theme as antdTheme } from 'antd'
import type { AppApi } from '@shared/api'
import { DEFAULT_SETTINGS } from '@shared/settings'
+import { getThemeById } from '@shared/theme'
import App from './App'
import { ErrorBoundary } from './ErrorBoundary'
import ZmodemOffers from './workspace/ZmodemOffers'
import { useSettingsStore } from './settings/store'
+import { mix } from './theme/chrome'
import './global.css'
// Dev convenience: allow opening the renderer in a plain browser (vite page)
@@ -148,13 +150,33 @@ function FontHotkeyListener(): null {
// antd
provides the context that App.useApp() consumers (workspace
// message/notification) rely on; without it they get empty stubs and crash.
// ErrorBoundary keeps any render crash from black-screening the whole window.
+
+/** antd surfaces (settings dialog, menus, popovers) follow the terminal theme:
+ * base/container/elevated backgrounds derive from the theme's background. */
+function ThemedConfigProvider({ children }: { children: ReactNode }): React.JSX.Element {
+ const themeId = useSettingsStore((s) => s.settings.terminal.themeId)
+ const customThemes = useSettingsStore((s) => s.settings.customThemes)
+ const colors = getThemeById(themeId, customThemes).colors
+ return (
+
+ {children}
+
+ )
+}
+
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
-
+
@@ -162,5 +184,5 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
-
+
)
diff --git a/src/renderer/src/settings/ThemeSettingsTab.tsx b/src/renderer/src/settings/ThemeSettingsTab.tsx
index 5ab1610..52941d6 100644
--- a/src/renderer/src/settings/ThemeSettingsTab.tsx
+++ b/src/renderer/src/settings/ThemeSettingsTab.tsx
@@ -1,5 +1,5 @@
import { useMemo } from 'react'
-import { Button, Popconfirm } from 'antd'
+import { Button, ColorPicker, Popconfirm } from 'antd'
import type { TerminalSettings } from '@shared/settings'
import { DEFAULT_SETTINGS } from '@shared/settings'
import type { TerminalTheme, ThemeColors } from '@shared/theme'
@@ -46,7 +46,26 @@ export function ThemeSettingsTab({
}
return (
-
+
+
+
+
标签强调色
+
活动标签边框、侧栏指示条与 SSH 徽章的强调颜色
+
+
+ void updateTerminal({ tabAccentColor: color.toHexString() })}
+ />
+
+
+
+
{themeCards.map((t) => {
const selected = t.id === activeTheme.id
const isCustom = t.builtin === false
@@ -116,6 +135,7 @@ export function ThemeSettingsTab({
+
新建主题
+
)
}
\ No newline at end of file
diff --git a/src/renderer/src/settings/settings.css b/src/renderer/src/settings/settings.css
index 51607a2..99ba909 100644
--- a/src/renderer/src/settings/settings.css
+++ b/src/renderer/src/settings/settings.css
@@ -5,13 +5,13 @@
/* ---------- settings dialog scaffolding ---------- */
.ant-modal-header {
- border-bottom: 1px solid #2d2d2d;
+ border-bottom: 1px solid var(--chrome-border);
padding-bottom: 14px;
}
.ant-modal .ant-tabs-nav {
margin-bottom: 8px;
- border-bottom: 1px solid #2d2d2d;
+ border-bottom: 1px solid var(--chrome-border);
}
.settings-footer {
@@ -116,7 +116,7 @@
margin-top: 14px;
border-radius: 6px;
padding: 12px 14px;
- border: 1px solid #2d2d2d;
+ border: 1px solid var(--chrome-border);
overflow: hidden;
}
@@ -140,6 +140,30 @@
/* ---------- theme gallery ---------- */
+/* ---- theme tab: accent picker + gallery ---- */
+.theme-pane {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ min-height: 0;
+}
+
+.theme-accent-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 10px 12px;
+ border: 1px solid var(--chrome-border);
+ border-radius: 8px;
+}
+
+.theme-accent-controls {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
.theme-gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
@@ -333,7 +357,7 @@
.theme-editor-preview-box {
border-radius: 6px;
padding: 10px 14px;
- border: 1px solid #2d2d2d;
+ border: 1px solid var(--chrome-border);
font-family: 'Cascadia Mono', Consolas, 'JetBrains Mono', monospace;
font-size: 12px;
}
@@ -356,7 +380,7 @@
width: 48px;
height: 48px;
border-radius: 12px;
- background: #1e2531;
+ background: var(--chrome-hover);
color: #3fd06a;
display: flex;
align-items: center;
diff --git a/src/renderer/src/terminal/TerminalView.tsx b/src/renderer/src/terminal/TerminalView.tsx
index 6340def..7801f92 100644
--- a/src/renderer/src/terminal/TerminalView.tsx
+++ b/src/renderer/src/terminal/TerminalView.tsx
@@ -44,6 +44,13 @@ const SEARCH_DECORATIONS: NonNullable> 16) & 255, (n >> 8) & 255, n & 255]
+}
+
+function toHex(v: number): string {
+ return Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')
+}
+
+/** t=0 → a, t=1 → b */
+export function mix(a: string, b: string, t: number): string | null {
+ const ca = parseHex(a)
+ const cb = parseHex(b)
+ if (!ca || !cb) return null
+ return `#${toHex(ca[0] + (cb[0] - ca[0]) * t)}${toHex(ca[1] + (cb[1] - ca[1]) * t)}${toHex(ca[2] + (cb[2] - ca[2]) * t)}`
+}
+
+/** Derive the app chrome palette (sidebar, tab bars, dividers) from the active
+ * terminal theme so the whole window follows theme switches instead of only
+ * the terminal canvas. Values land on :root CSS variables; anything the theme
+ * can't provide keeps the static fallback in global.css. */
+export function applyChromeTheme(colors: ThemeColors): void {
+ const style = document.documentElement.style
+ const set = (key: string, value: string | null): void => {
+ if (value) style.setProperty(key, value)
+ }
+ set('--chrome-bg', colors.background)
+ set('--chrome-fg', colors.foreground)
+ set('--chrome-bg-deep', mix(colors.background, '#000000', 0.35))
+ set('--chrome-hover', mix(colors.background, colors.foreground, 0.08))
+ set('--chrome-border', mix(colors.background, colors.foreground, 0.16))
+}
+
+/** Tab accent: base color + a lightened foreground variant for text on dark
+ * tinted backgrounds. Consumed via var(--tab-accent*) in workspace.css. */
+export function applyTabAccent(color: string): void {
+ const style = document.documentElement.style
+ // Settings hydrated from an older main process may lack the key; clear the
+ // vars so the CSS fallbacks apply instead of an invalid "undefined" value.
+ if (!color) {
+ style.removeProperty('--tab-accent')
+ style.removeProperty('--tab-accent-fg')
+ return
+ }
+ style.setProperty('--tab-accent', color)
+ style.setProperty('--tab-accent-fg', mix(color, '#ffffff', 0.18) ?? color)
+}
diff --git a/src/renderer/src/workspace/workspace.css b/src/renderer/src/workspace/workspace.css
index de9ce66..28bd0c0 100644
--- a/src/renderer/src/workspace/workspace.css
+++ b/src/renderer/src/workspace/workspace.css
@@ -55,8 +55,8 @@
/* ---- M6.1 rail active state (mode entries) ---- */
.workspace-rail-btn.is-active {
color: var(--dv-active-tab-foreground-color, #e8e8e8);
- box-shadow: inset 2px 0 0 #3fb950;
- background: rgba(63, 185, 80, 0.10);
+ box-shadow: inset 2px 0 0 var(--tab-accent, #3fb950);
+ background: color-mix(in srgb, var(--tab-accent, #3fb950) 10%, transparent);
}
.workspace-rail-spacer {
@@ -173,19 +173,22 @@
/* Align terminal chrome with the app chrome. */
.dockview-theme-abyss {
--dv-group-view-background-color: var(--chrome-bg);
- --dv-tabs-and-actions-container-background-color: #10141a;
- --dv-tabs-and-actions-container-height: 32px;
+ --dv-tabs-and-actions-container-background-color: var(--chrome-bg-deep, #10141a);
+ --dv-tabs-and-actions-container-height: 28px;
--dv-tab-divider-color: transparent;
+ /* Pane split dividers: soft slate band at rest, accent blue on hover/drag. */
+ --dv-sash-color: rgba(148, 163, 184, 0.28);
+ --dv-active-sash-color: rgba(59, 142, 234, 0.75);
}
-/* ---- tab polish: rounded pills, quiet inactive, green active ---- */
+/* ---- tab polish: low rectangular tabs, quiet inactive, green active ---- */
.workspace-dockview .dv-tabs-and-actions-container {
padding: 4px 5px 0;
box-sizing: border-box;
}
.workspace-dockview .dv-tab {
- border-radius: 9px;
+ border-radius: 3px;
padding: 0 8px;
margin-right: 4px;
background: rgba(255, 255, 255, 0.04);
@@ -202,14 +205,14 @@
/* XTerminal-style active tab: green outline + tinted bg + green title. */
.workspace-dockview .dv-tab.dv-active-tab {
- background: rgba(63, 185, 80, 0.10);
- box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.75);
- color: #3fd06a;
+ background: color-mix(in srgb, var(--tab-accent, #3fb950) 10%, transparent);
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 75%, transparent);
+ color: var(--tab-accent-fg, #3fd06a);
}
.workspace-dockview .dv-tab.dv-active-tab:hover {
- box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.95);
- color: #4ad476;
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 95%, transparent);
+ color: var(--tab-accent-fg, #4ad476);
}
.workspace-dockview .dv-tab .workspace-terminal-tab {
@@ -217,15 +220,15 @@
}
.workspace-dockview .dv-tab .workspace-terminal-tab-title {
- font-size: 12.5px;
+ font-size: 12px;
line-height: 1;
}
/* Close glyph as its own mini outlined pill, mirroring the tab border. */
.workspace-dockview .dv-tab .workspace-terminal-tab-close {
- width: 17px;
- height: 17px;
- border-radius: 4px;
+ width: 15px;
+ height: 15px;
+ border-radius: 2px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
opacity: 0.8;
transition: background-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
@@ -233,9 +236,9 @@
.workspace-dockview .dv-tab .workspace-terminal-tab-close:hover {
opacity: 1;
- color: #3fd06a;
- background: rgba(63, 185, 80, 0.08);
- box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.65);
+ color: var(--tab-accent-fg, #3fd06a);
+ background: color-mix(in srgb, var(--tab-accent, #3fb950) 8%, transparent);
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 65%, transparent);
}
/* ---- empty state overlay ---- */
@@ -377,11 +380,11 @@ body.ssh-resizing .ssh-bottom-divider {
.workspace-terminal-tab-icon {
flex: 0 0 auto;
font-size: 12px;
- color: #3fb950;
+ color: var(--tab-accent, #3fb950);
}
.workspace-dockview .dv-tab .workspace-terminal-tab-ssh {
- border-left: 2px solid #3fb950;
+ border-left: 2px solid var(--tab-accent, #3fb950);
padding-left: 5px !important;
gap: 5px;
}
@@ -397,9 +400,9 @@ body.ssh-resizing .ssh-bottom-divider {
padding: 1px 6px 2px;
box-sizing: border-box;
border-radius: 9px;
- border: 1px solid rgba(63, 185, 80, 0.38);
- background: rgba(63, 185, 80, 0.08);
- color: #3fb950;
+ border: 1px solid color-mix(in srgb, var(--tab-accent, #3fb950) 38%, transparent);
+ background: color-mix(in srgb, var(--tab-accent, #3fb950) 8%, transparent);
+ color: var(--tab-accent, #3fb950);
font-family: 'Cascadia Mono', 'Consolas', 'SFMono-Regular', monospace;
font-size: 10.5px;
line-height: 1;
@@ -407,9 +410,9 @@ body.ssh-resizing .ssh-bottom-divider {
}
.workspace-dockview .dv-tab.dv-active-tab .workspace-terminal-tab-ssh .ssh-host-badge {
- border-color: rgba(63, 185, 80, 0.6);
- background: rgba(63, 185, 80, 0.14);
- color: #46c96a;
+ border-color: color-mix(in srgb, var(--tab-accent, #3fb950) 60%, transparent);
+ background: color-mix(in srgb, var(--tab-accent, #3fb950) 14%, transparent);
+ color: var(--tab-accent-fg, #46c96a);
}
/* ---- per-group "+" right after the last tab ----
@@ -438,9 +441,9 @@ body.ssh-resizing .ssh-bottom-divider {
}
.workspace-tabbar-add:hover {
- background: rgba(63, 185, 80, 0.08);
- box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.65);
- color: #3fd06a;
+ background: color-mix(in srgb, var(--tab-accent, #3fb950) 8%, transparent);
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 65%, transparent);
+ color: var(--tab-accent-fg, #3fd06a);
}
/* ---- M6 tab-bar workspace actions (broadcast + quick-input) ---- */
@@ -476,15 +479,15 @@ body.ssh-resizing .ssh-bottom-divider {
}
.workspace-tabbar-btn2.is-active {
- color: #3fd06a;
- background: rgba(63, 185, 80, 0.12);
- box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.55);
+ color: var(--tab-accent-fg, #3fd06a);
+ background: color-mix(in srgb, var(--tab-accent, #3fb950) 12%, transparent);
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 55%, transparent);
}
.workspace-tabbar-bcast-count {
font-size: 10px;
font-weight: 600;
- color: #3fb950;
+ color: var(--tab-accent, #3fb950);
line-height: 1;
}
@@ -565,9 +568,9 @@ body.ssh-resizing .ssh-bottom-divider {
font-size: 10px;
font-weight: 600;
line-height: 1;
- color: #3fb950;
- background: rgba(63, 185, 80, 0.1);
- box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.35);
+ color: var(--tab-accent, #3fb950);
+ background: color-mix(in srgb, var(--tab-accent, #3fb950) 10%, transparent);
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 35%, transparent);
user-select: none;
}
diff --git a/src/shared/settings.ts b/src/shared/settings.ts
index 49341a0..cdf0886 100644
--- a/src/shared/settings.ts
+++ b/src/shared/settings.ts
@@ -36,6 +36,8 @@ export interface TerminalSettings {
pasteRiskConfirm: boolean
/** inline command suggestion popup while typing (history + library) */
suggestEnabled: boolean
+ /** accent color for the active tab outline, rail indicator and SSH badges */
+ tabAccentColor: string
}
export interface SystemSettings {
@@ -95,12 +97,13 @@ export const DEFAULT_SETTINGS: AppSettings = {
cursorBlink: true,
cursorStyle: 'block',
cursorInactiveStyle: 'outline',
- themeId: 'default-dark',
+ themeId: 'material-dark',
rendererMode: 'auto',
autoWrap: true,
copyOnSelect: false,
pasteRiskConfirm: true,
- suggestEnabled: true
+ suggestEnabled: true,
+ tabAccentColor: '#3fb950'
},
customThemes: [],
highlightRules: DEFAULT_HIGHLIGHT_RULES,