Initial commit: OpenTerminal v0.1.0

Open-source terminal app (local + SSH): split panes, themes, SFTP,
server monitoring, broadcast input, ZMODEM, system tray, single instance.
Electron + React + TypeScript. MIT License.
This commit is contained in:
Bill committed 2026-09-07 16:15:50 +08:00
commit a9acdbe500
99 files changed
+23940

No files matched your search

+108
View File
@@ -0,0 +1,108 @@
import type {
AppInfo,
LayoutMeta,
PtyCreateOptions,
PtyCreateResult,
PtyDataEvent,
PtyExitEvent,
SessionOpenResult
} from './ipc'
import type { AppSettings } from './settings'
import type {
HostKeyAction,
HostKeyPromptEvent,
SessionOpenOptions,
SshConnection,
SshConnectionInput
} from './connections'
import type { SysinfoMeta, SysinfoSample } from './sysinfo'
import type { SftpEntry, TransferProgressEvent } from './sftp'
import type { CommandItem, SessionLogMeta } from './commands'
import type { ZmodemDoneEvent, ZmodemOfferEvent, ZmodemResponse } from './ipc'
/**
* The single API surface exposed on `window.api` by the preload script.
* Main process implements the handlers; renderer consumes this contract.
*/
export interface AppApi {
appInfo(): Promise<AppInfo>
// ---- sessions (pty data plane is shared by local and ssh sessions) ----
createPty(opts?: PtyCreateOptions): Promise<PtyCreateResult>
/** open a local or ssh session; resolves once the session is ready to stream */
openSession(opts: SessionOpenOptions): Promise<SessionOpenResult>
/** output a session produced before this renderer subscribed (capped ring buffer) */
getSessionReplay(id: string): Promise<string>
writePty(id: string, data: string): void
resizePty(id: string, cols: number, rows: number): void
killPty(id: string): void
/** subscribe to all session output; returns unsubscribe */
onPtyData(cb: (e: PtyDataEvent) => void): () => void
onPtyExit(cb: (e: PtyExitEvent) => void): () => void
// ---- sysinfo (M3: ssh sessions only; main polls the remote and broadcasts) ----
/** start polling for an ssh session; stop happens automatically on session close */
sysinfoStart(id: string): void
sysinfoStop(id: string): void
onSysinfoMeta(cb: (e: { id: string; meta: SysinfoMeta }) => void): () => void
onSysinfoSample(cb: (e: { id: string; sample: SysinfoSample }) => void): () => void
// ---- sftp (M4: bound to an established ssh session) ----
listRemote(sessionId: string, dir: string): Promise<SftpEntry[]>
mkdirRemote(sessionId: string, dir: string, name: string): Promise<void>
renameRemote(sessionId: string, from: string, to: string): Promise<void>
deleteRemote(sessionId: string, paths: string[]): Promise<void>
/** mode = octal string, e.g. "755" or "0644" */
chmodRemote(sessionId: string, path: string, mode: string): Promise<void>
chownRemote(sessionId: string, path: string, uid: number, gid: number): Promise<void>
/** upload local files into remote dir; returns transferId */
uploadRemote(sessionId: string, localPaths: string[], remoteDir: string): Promise<string>
/** download remote paths into local dir; returns transferId */
downloadRemote(sessionId: string, remotePaths: string[], localDir: string): Promise<string>
cancelTransfer(transferId: string): void
onTransferProgress(cb: (e: TransferProgressEvent) => void): () => void
/** native open dialog; returns picked file paths (empty when cancelled) */
pickFiles(): Promise<string[]>
pickDirectory(): Promise<string>
// ---- command history / library + session logs (M5) ----
/** record one executed command (deduped, newest first, capped) */
recordCommand(cmd: string): Promise<void>
listHistory(): Promise<CommandItem[]>
clearHistory(): Promise<void>
listLibrary(): Promise<CommandItem[]>
saveLibraryItem(item: CommandItem): Promise<CommandItem>
deleteLibraryItem(id: string): Promise<void>
/** session output logging */
logStart(sessionId: string): Promise<SessionLogMeta>
logStop(sessionId: string): Promise<void>
listSessionLogs(): Promise<SessionLogMeta[]>
openLogsDir(): void
// ---- ssh connections ----
listConnections(): Promise<SshConnection[]>
saveConnection(input: SshConnectionInput): Promise<SshConnection>
deleteConnection(id: string): Promise<void>
onHostKeyPrompt(cb: (e: HostKeyPromptEvent) => void): () => void
respondHostKey(promptId: string, action: HostKeyAction): void
// ---- zmodem (M6: main-process engine over ssh sessions; progress reuses transfer events) ----
onZmodemOffer(cb: (e: ZmodemOfferEvent) => void): () => void
zmodemRespond(resp: ZmodemResponse): void
onZmodemDone(cb: (e: ZmodemDoneEvent) => void): () => void
// ---- settings ----
getSettings(): Promise<AppSettings>
saveSettings(next: AppSettings): Promise<AppSettings>
onSettingsChanged(cb: (s: AppSettings) => void): () => void
// ---- fonts ----
listFonts(): Promise<string[]>
// ---- layout templates ----
listLayouts(): Promise<LayoutMeta[]>
getLayout(id: string): Promise<string | null>
/** json = serialized dockview layout (DockviewApi.toJSON()) */
saveLayout(meta: LayoutMeta, json: string): Promise<void>
deleteLayout(id: string): Promise<void>
}
+26
View File
@@ -0,0 +1,26 @@
/** Command history / command library + session output logs (M5). */
export interface CommandItem {
id: string
/** library items have a name; history items don't */
name?: string
command: string
note?: string
group?: string
/** library items support {{var}} placeholders; recorded purely as metadata */
params?: string[]
createdAt: number
/** epoch ms of last execution (history ordering) */
lastUsedAt?: number
}
export interface SessionLogMeta {
sessionId: string
/** absolute file path of the plain-text log */
file: string
fileName: string
/** epoch ms when logging started */
startedAt: number
/** undefined while still logging */
endedAt?: number
}
+86
View File
@@ -0,0 +1,86 @@
import type { LayoutMeta } from './ipc'
/**
* SSH connection model. Secrets (password / key content / passphrase) are only
* ever persisted encrypted (Electron safeStorage, base64) inside the main
* process. The renderer receives connections with secret fields stripped and
* replaced by the `savedAuth` flags, so plaintext secrets never cross the
* contextBridge.
*/
export type SshAuthMethod = 'password' | 'privateKey' | 'agent'
export interface SshSavedAuthFlags {
hasPassword: boolean
hasKeyContent: boolean
hasPassphrase: boolean
}
export interface SshConnection {
id: string
name: string
/** optional group for the bookmark tree; undefined = root */
group?: string
host: string
port: number
username: string
auth: SshAuthMethod
/** prompt for password in a dialog at connect time instead of using the stored one */
askPasswordAtConnect: boolean
/** prompt for key passphrase at connect time */
askPassphraseAtConnect: boolean
keyPath?: string
/** seconds between keepalive packets; 0 disables */
keepaliveIntervalSec: number
createdAt: number
lastConnectedAt?: number
savedAuth: SshSavedAuthFlags
}
/** Renderer-supplied connection data on save; secrets here are plaintext on purpose (one-way, encrypted before persisting). */
export interface SshConnectionInput {
id?: string
name: string
group?: string
host: string
port: number
username: string
auth: SshAuthMethod
askPasswordAtConnect: boolean
askPassphraseAtConnect: boolean
keyPath?: string
keepaliveIntervalSec: number
/** plain password to store; omit to keep the previously stored one */
password?: string
/** private key content to store inline; omit to keep the previously stored one */
keyContent?: string
passphrase?: string
}
/** Secrets typed at connect time when ask*AtConnect is set. */
export interface SshSecretOverride {
password?: string
passphrase?: string
}
export interface SessionOpenOptions {
kind: 'local' | 'ssh'
/** ssh only */
connectionId?: string
secretOverride?: SshSecretOverride
}
export type HostKeyAction = 'accept' | 'reject'
export interface HostKeyPromptEvent {
promptId: string
host: string
port: number
/** sha256 fingerprint, base64 (no padding), ssh "SHA256:..." style */
fingerprint: string
reason: 'new' | 'changed'
}
/** Reuse LayoutMeta shape for layout templates only; groups are a plain string field on SshConnection. */
export type ConnectionMeta = Pick<SshConnection, 'id' | 'name' | 'group'>
export type { LayoutMeta }
+161
View File
@@ -0,0 +1,161 @@
/**
* IPC channel names + payload contracts shared between main / preload / renderer.
* DO NOT rename channels or change payload shapes without updating all consumers.
*/
export const Ipc = {
// ---- PTY session lifecycle ----
// NOTE: since M2 these channels are session-generic: `id` addresses both
// local pty sessions and ssh sessions; data plane is identical.
PTY_CREATE: 'pty:create',
PTY_WRITE: 'pty:write',
PTY_RESIZE: 'pty:resize',
PTY_KILL: 'pty:kill',
/** main -> renderer broadcast: { id, data } */
PTY_DATA: 'pty:data',
/** main -> renderer broadcast: { id, exitCode } */
PTY_EXIT: 'pty:exit',
// ---- settings ----
SETTINGS_GET: 'settings:get',
SETTINGS_SET: 'settings:set',
/** main -> renderer broadcast after any save */
SETTINGS_CHANGED: 'settings:changed',
// ---- fonts ----
FONTS_LIST: 'fonts:list',
// ---- layouts ----
LAYOUTS_LIST: 'layouts:list',
LAYOUTS_GET: 'layouts:get',
LAYOUTS_SAVE: 'layouts:save',
LAYOUTS_DELETE: 'layouts:delete',
// ---- sessions (openSession unifies local pty and ssh) ----
SESSION_OPEN: 'session:open',
/** main keeps a short replay buffer per session so late subscribers catch up */
SESSION_REPLAY: 'session:replay',
// ---- ssh connections (bookmarks) ----
CONNECTIONS_LIST: 'connections:list',
CONNECTIONS_SAVE: 'connections:save',
CONNECTIONS_DELETE: 'connections:delete',
// ---- host key verification ----
/** main -> renderer: needs user decision before connect proceeds */
HOSTKEY_PROMPT: 'hostkey:prompt',
HOSTKEY_RESPOND: 'hostkey:respond',
// ---- sysinfo (M3) ----
SYSINFO_START: 'sysinfo:start',
SYSINFO_STOP: 'sysinfo:stop',
/** main -> renderer broadcast: { id, meta } (once per session) */
SYSINFO_META: 'sysinfo:meta',
/** main -> renderer broadcast: { id, sample } (poll interval) */
SYSINFO_SAMPLE: 'sysinfo:sample',
// ---- sftp (M4) ----
SFTP_LIST: 'sftp:list',
SFTP_MKDIR: 'sftp:mkdir',
SFTP_RENAME: 'sftp:rename',
SFTP_DELETE: 'sftp:delete',
SFTP_CHMOD: 'sftp:chmod',
SFTP_CHOWN: 'sftp:chown',
SFTP_UPLOAD: 'sftp:upload',
SFTP_DOWNLOAD: 'sftp:download',
TRANSFER_CANCEL: 'transfer:cancel',
/** main -> renderer broadcast: TransferProgressEvent */
TRANSFER_PROGRESS: 'transfer:progress',
DIALOG_PICK_FILES: 'dialog:pickFiles',
DIALOG_PICK_DIR: 'dialog:pickDirectory',
// ---- commands + logs (M5) ----
COMMANDS_RECORD: 'commands:record',
COMMANDS_HISTORY_LIST: 'commands:historyList',
COMMANDS_HISTORY_CLEAR: 'commands:historyClear',
COMMANDS_LIBRARY_LIST: 'commands:libraryList',
COMMANDS_LIBRARY_SAVE: 'commands:librarySave',
COMMANDS_LIBRARY_DELETE: 'commands:libraryDelete',
LOG_START: 'log:start',
LOG_STOP: 'log:stop',
LOG_LIST: 'log:list',
LOG_OPEN_DIR: 'log:openDir',
// ---- zmodem (M6: main-process engine, ssh sessions only) ----
/** main -> renderer: remote ran rz (mode=send) or sz (mode=receive); renderer must ask the user */
ZMODEM_OFFER: 'zmodem:offer',
/** renderer -> main: user answered the zmodem offer */
ZMODEM_RESPOND: 'zmodem:respond',
/** main -> renderer: zmodem session finished (ok, cancelled or failed) */
ZMODEM_DONE: 'zmodem:done',
// ---- misc ----
APP_INFO: 'app:info'
} as const
export interface PtyCreateOptions {
cwd?: string
/** executable; omit for platform default shell */
shell?: string
env?: Record<string, string>
}
export interface PtyCreateResult {
id: string
shell: string
cwd: string
}
export interface PtyDataEvent {
id: string
data: string
}
export interface PtyExitEvent {
id: string
exitCode: number
}
export interface AppInfo {
platform: NodeJS.Platform | string
appVersion: string
homeDir: string
}
export interface LayoutMeta {
id: string
name: string
createdAt: number
}
export interface SessionOpenResult {
id: string
}
export interface HostKeyResponse {
promptId: string
action: 'accept' | 'reject'
}
// ---- zmodem (M6) ----
/** 'send' = remote ran `rz`, we upload files; 'receive' = remote ran `sz`, we download */
export interface ZmodemOfferEvent {
id: string
mode: 'send' | 'receive'
}
export interface ZmodemResponse {
id: string
cancelled: boolean
/** local files to upload (mode=send) */
paths?: string[]
/** local directory to save into (mode=receive) */
dir?: string
}
export interface ZmodemDoneEvent {
id: string
ok: boolean
/** human-readable failure reason when ok=false */
message?: string
}
+106
View File
@@ -0,0 +1,106 @@
import type { TerminalTheme } from './theme'
/** One terminal keyword-highlight rule (regex → ANSI color injection). */
export interface HighlightRule {
id: string
/** regex source; compiled with 'g' flag at use site */
pattern: string
enabled: boolean
/** 1..100, lower runs first and claims its spans first */
priority: number
/** truecolor hex (#rrggbb); fg required, bg optional */
color: { fg: string; bg?: string }
note?: string
/** part of the factory preset set (restored by 恢复默认) */
builtin?: boolean
}
export interface TerminalSettings {
fontFamily: string
fontSize: number
/** 100..900 */
fontWeight: number
/** 100..900 */
boldFontWeight: number
letterSpacing: number
lineHeight: number
scrollback: number
cursorBlink: boolean
cursorStyle: 'bar' | 'block' | 'underline'
cursorInactiveStyle: 'outline' | 'block' | 'bar' | 'underline' | 'none'
themeId: string
rendererMode: 'auto' | 'webgl' | 'dom'
autoWrap: boolean
copyOnSelect: boolean
/** confirm before pasting multi-line / suspicious text */
pasteRiskConfirm: boolean
/** inline command suggestion popup while typing (history + library) */
suggestEnabled: boolean
}
export interface SystemSettings {
/** register the app to launch at OS login */
launchAtLogin: boolean
/** hold a powerSaveBlocker so the OS never auto-suspends while running */
preventSleep: boolean
/** Electron accelerator that toggles window visibility; '' = disabled (M6) */
globalShowHide?: string
/** close-button behavior: prompt every time / hide to tray / quit directly */
closeAction?: 'ask' | 'tray' | 'exit'
}
export interface AppSettings {
terminal: TerminalSettings
/** user-created themes; builtin themes are immutable and live in shared/theme.ts */
customThemes: TerminalTheme[]
/** terminal keyword highlight rules, applied at write time in priority order */
highlightRules: HighlightRule[]
system: SystemSettings
}
const RULE = (
id: string,
pattern: string,
priority: number,
fg: string,
note: string,
bg?: string
): HighlightRule => ({ id, pattern, enabled: true, priority, color: { fg, bg }, note, builtin: true })
export const DEFAULT_HIGHLIGHT_RULES: HighlightRule[] = [
RULE('perm', '^[-dlbcps][rwxstST-]{9}', 1, '#3fb950', 'Linux 权限与用户'),
RULE('path', '(?:^|[\\s:=])(?:/[A-Za-z0-9_.\\-/@+]+|~[A-Za-z0-9_.\\-/]*)', 2, '#58a6ff', 'Linux 文件路径'),
RULE('shellkw', '\\b(if|then|else|elif|fi|for|while|do|done|case|esac|function|return|break|continue|export|source)\\b', 3, '#d2a8ff', 'Shell 关键字与流程'),
RULE('okstate', '\\b(SUCCESS|PASS|OK|DONE|PASSED)\\b', 5, '#3fb950', '成功状态'),
RULE('badstate', '\\b(FAILED|ERROR|FAIL|FATAL|WARN|WARNING|DENIED)\\b', 6, '#f85149', '错误/警告状态'),
RULE('quoted', '"[^"\\n]*"|\'[^\'\\n]*\'', 8, '#e3b341', '字符串与引号'),
RULE('envvar', '\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?', 10, '#79c0ff', '环境变量与参数'),
RULE('ipv4', '\\b(?:\\d{1,3}\\.){3}\\d{1,3}(?::\\d{1,5})?\\b', 15, '#58a6ff', '网络与 IP 地址'),
RULE('datetime', '\\b\\d{4}[-/]\\d{1,2}[-/]\\d{1,2}(?:[ T]\\d{1,2}:\\d{2}(?::\\d{2})?)?\\b|\\b\\d{1,2}:\\d{2}(?::\\d{2})?\\b', 20, '#bc8cff', '时间与日期'),
RULE('numbers', '\\b\\d+(?:\\.\\d+)?(?:%)?\\b', 25, '#f2cc60', '数字与计数'),
RULE('url', 'https?://[^\\s]+', 30, '#58a6ff', '网址链接')
]
export const DEFAULT_SETTINGS: AppSettings = {
terminal: {
fontFamily: "'Cascadia Mono', Consolas, 'JetBrains Mono', 'Courier New', monospace",
fontSize: 14,
fontWeight: 400,
boldFontWeight: 700,
letterSpacing: 0,
lineHeight: 1,
scrollback: 5000,
cursorBlink: true,
cursorStyle: 'block',
cursorInactiveStyle: 'outline',
themeId: 'default-dark',
rendererMode: 'auto',
autoWrap: true,
copyOnSelect: false,
pasteRiskConfirm: true,
suggestEnabled: true
},
customThemes: [],
highlightRules: DEFAULT_HIGHLIGHT_RULES,
system: { launchAtLogin: false, preventSleep: false, globalShowHide: '', closeAction: 'ask' }
}
+31
View File
@@ -0,0 +1,31 @@
/** SFTP file model + transfer progress (M4). */
export interface SftpEntry {
name: string
/** absolute remote path */
path: string
isDir: boolean
/** bytes (0 for dirs) */
size: number
/** epoch ms */
mtime: number
/** e.g. "drwxr-xr-x" (best effort) */
mode?: string
/** numeric uid/gid (best effort) */
uid?: number
gid?: number
}
export type TransferKind = 'upload' | 'download' | 'zmodem-upload' | 'zmodem-download'
export type TransferState = 'running' | 'done' | 'error' | 'cancelled'
export interface TransferProgressEvent {
transferId: string
kind: TransferKind
state: TransferState
/** file currently being transferred */
file: string
bytes: number
totalBytes: number
error?: string
}
+34
View File
@@ -0,0 +1,34 @@
/** Remote server hardware monitoring sample (M3, ssh sessions only). */
export interface SysinfoMeta {
/** e.g. "Linux 5.15.0 x86_64" */
os: string
hostname: string
}
export interface SysinfoSample {
ts: number
cpu: {
/** 0..100 overall */
usage: number
cores: number
loadavg: [number, number, number]
}
mem: {
totalMb: number
usedMb: number
}
disks: Array<{
mount: string
totalMb: number
usedMb: number
}>
/** kilobytes per second since previous sample */
net: {
rxKbs: number
txKbs: number
}
uptimeSec: number
/** when set, the last collection failed (render an error state, keep old data) */
error?: string
}
+382
View File
@@ -0,0 +1,382 @@
/**
* Terminal color theme model. Palette layout mirrors xterm.js ITheme.
* Values of builtin themes come from the public-domain iTerm2-Color-Schemes / xterm-theme collections.
*/
export interface ThemeColors {
background: string
foreground: string
cursor: string
/** accent block behind cursor text; falls back to foreground if omitted */
cursorAccent?: string
selectionBackground: string
black: string
red: string
green: string
yellow: string
blue: string
magenta: string
cyan: string
white: string
brightBlack: string
brightRed: string
brightGreen: string
brightYellow: string
brightBlue: string
brightMagenta: string
brightCyan: string
brightWhite: string
}
export interface TerminalTheme {
id: string
name: string
builtin: boolean
colors: ThemeColors
}
export const DEFAULT_DARK: TerminalTheme = {
id: 'default-dark',
name: 'OpenTerminal Dark',
builtin: true,
colors: {
background: '#1e1e1e',
foreground: '#cccccc',
cursor: '#ffffff',
cursorAccent: '#1e1e1e',
selectionBackground: '#264f78',
black: '#000000',
red: '#cd3131',
green: '#0dbc79',
yellow: '#e5e510',
blue: '#2472c8',
magenta: '#bc3fbc',
cyan: '#11a8cd',
white: '#e5e5e5',
brightBlack: '#666666',
brightRed: '#f14c4c',
brightGreen: '#23d18b',
brightYellow: '#f5f543',
brightBlue: '#3b8eea',
brightMagenta: '#d670d6',
brightCyan: '#29b8db',
brightWhite: '#ffffff'
}
}
export const BUILTIN_THEMES: TerminalTheme[] = [
DEFAULT_DARK,
{
id: 'dracula',
name: 'Dracula',
builtin: true,
colors: {
background: '#282a36',
foreground: '#f8f8f2',
cursor: '#f8f8f2',
cursorAccent: '#282a36',
selectionBackground: '#44475a',
black: '#21222c',
red: '#ff5555',
green: '#50fa7b',
yellow: '#f1fa8c',
blue: '#bd93f9',
magenta: '#ff79c6',
cyan: '#8be9fd',
white: '#f8f8f2',
brightBlack: '#6272a4',
brightRed: '#ff6e6e',
brightGreen: '#69ff94',
brightYellow: '#ffffa5',
brightBlue: '#d6acff',
brightMagenta: '#ff92df',
brightCyan: '#a4ffff',
brightWhite: '#ffffff'
}
},
{
id: 'one-half-dark',
name: 'One Half Dark',
builtin: true,
colors: {
background: '#282c34',
foreground: '#dcdfe4',
cursor: '#61afef',
cursorAccent: '#282c34',
selectionBackground: '#3e4451',
black: '#282c34',
red: '#e06c75',
green: '#98c379',
yellow: '#e5c07b',
blue: '#61afef',
magenta: '#c678dd',
cyan: '#56b6c2',
white: '#dcdfe4',
brightBlack: '#5c6370',
brightRed: '#e06c75',
brightGreen: '#98c379',
brightYellow: '#e5c07b',
brightBlue: '#61afef',
brightMagenta: '#c678dd',
brightCyan: '#56b6c2',
brightWhite: '#ffffff'
}
},
{
id: 'one-half-light',
name: 'One Half Light',
builtin: true,
colors: {
background: '#fafafa',
foreground: '#383a42',
cursor: '#383a42',
cursorAccent: '#fafafa',
selectionBackground: '#d2d6db',
black: '#383a42',
red: '#e45649',
green: '#50a14f',
yellow: '#c18401',
blue: '#0184bc',
magenta: '#a626a4',
cyan: '#0997b3',
white: '#fafafa',
brightBlack: '#4f525e',
brightRed: '#e06c75',
brightGreen: '#98c379',
brightYellow: '#e5c07b',
brightBlue: '#61afef',
brightMagenta: '#c678dd',
brightCyan: '#56b6c2',
brightWhite: '#ffffff'
}
},
{
id: 'solarized-dark',
name: 'Solarized Dark',
builtin: true,
colors: {
background: '#002b36',
foreground: '#839496',
cursor: '#93a1a1',
cursorAccent: '#002b36',
selectionBackground: '#073642',
black: '#002b36',
red: '#dc322f',
green: '#859900',
yellow: '#b58900',
blue: '#268bd2',
magenta: '#d33682',
cyan: '#2aa198',
white: '#eee8d5',
brightBlack: '#073642',
brightRed: '#cb4b16',
brightGreen: '#586e75',
brightYellow: '#657b83',
brightBlue: '#839496',
brightMagenta: '#6c71c4',
brightCyan: '#93a1a1',
brightWhite: '#fdf6e3'
}
},
{
id: 'solarized-light',
name: 'Solarized Light',
builtin: true,
colors: {
background: '#fdf6e3',
foreground: '#657b83',
cursor: '#586e75',
cursorAccent: '#fdf6e3',
selectionBackground: '#eee8d5',
black: '#073642',
red: '#dc322f',
green: '#859900',
yellow: '#b58900',
blue: '#268bd2',
magenta: '#d33682',
cyan: '#2aa198',
white: '#eee8d5',
brightBlack: '#93a1a1',
brightRed: '#cb4b16',
brightGreen: '#586e75',
brightYellow: '#657b83',
brightBlue: '#839496',
brightMagenta: '#6c71c4',
brightCyan: '#93a1a1',
brightWhite: '#fdf6e3'
}
},
{
id: 'gruvbox-dark',
name: 'Gruvbox Dark',
builtin: true,
colors: {
background: '#282828',
foreground: '#ebdbb2',
cursor: '#ebdbb2',
cursorAccent: '#282828',
selectionBackground: '#504945',
black: '#282828',
red: '#cc241d',
green: '#98971a',
yellow: '#d79921',
blue: '#458588',
magenta: '#b16286',
cyan: '#689d6a',
white: '#a89984',
brightBlack: '#928374',
brightRed: '#fb4934',
brightGreen: '#b8bb26',
brightYellow: '#fabd2f',
brightBlue: '#83a598',
brightMagenta: '#d3869b',
brightCyan: '#8ec07c',
brightWhite: '#ebdbb2'
}
},
{
id: 'nord',
name: 'Nord',
builtin: true,
colors: {
background: '#2e3440',
foreground: '#d8dee9',
cursor: '#d8dee9',
cursorAccent: '#2e3440',
selectionBackground: '#434c5e',
black: '#3b4252',
red: '#bf616a',
green: '#a3be8c',
yellow: '#ebcb8b',
blue: '#81a1c1',
magenta: '#b48ead',
cyan: '#88c0d0',
white: '#e5e9f0',
brightBlack: '#4c566a',
brightRed: '#bf616a',
brightGreen: '#a3be8c',
brightYellow: '#ebcb8b',
brightBlue: '#81a1c1',
brightMagenta: '#b48ead',
brightCyan: '#8fbcbb',
brightWhite: '#eceff4'
}
},
{
id: 'monokai',
name: 'Monokai',
builtin: true,
colors: {
background: '#272822',
foreground: '#f8f8f2',
cursor: '#f8f8f0',
cursorAccent: '#272822',
selectionBackground: '#49483e',
black: '#272822',
red: '#f92672',
green: '#a6e22e',
yellow: '#f4bf75',
blue: '#66d9ef',
magenta: '#ae81ff',
cyan: '#a1efe4',
white: '#f8f8f2',
brightBlack: '#75715e',
brightRed: '#f92672',
brightGreen: '#a6e22e',
brightYellow: '#f4bf75',
brightBlue: '#66d9ef',
brightMagenta: '#ae81ff',
brightCyan: '#a1efe4',
brightWhite: '#f9f8f5'
}
},
{
id: 'github-light',
name: 'GitHub Light',
builtin: true,
colors: {
background: '#ffffff',
foreground: '#4d4d4c',
cursor: '#4d4d4c',
cursorAccent: '#ffffff',
selectionBackground: '#dcdcdc',
black: '#3e3e3e',
red: '#c91b00',
green: '#00c200',
yellow: '#c7c400',
blue: '#0225c7',
magenta: '#c930c7',
cyan: '#00c5c7',
white: '#c7c7c7',
brightBlack: '#676767',
brightRed: '#ff6d67',
brightGreen: '#5ff967',
brightYellow: '#ffff6c',
brightBlue: '#6871ff',
brightMagenta: '#ff76ff',
brightCyan: '#5ffdff',
brightWhite: '#ffffff'
}
},
{
id: 'afterglow',
name: 'Afterglow',
builtin: true,
colors: {
background: '#1f1f1f',
foreground: '#d0d0d0',
cursor: '#f5f5f5',
cursorAccent: '#1f1f1f',
selectionBackground: '#4a4a4a',
black: '#1f1f1f',
red: '#ac4142',
green: '#7e8e50',
yellow: '#e5b567',
blue: '#6c99bb',
magenta: '#9f4e85',
cyan: '#508451',
white: '#d0d0d0',
brightBlack: '#5a5a5a',
brightRed: '#ac4142',
brightGreen: '#7e8e50',
brightYellow: '#e5b567',
brightBlue: '#6c99bb',
brightMagenta: '#9f4e85',
brightCyan: '#508451',
brightWhite: '#f5f5f5'
}
},
{
id: 'material-dark',
name: 'Material Dark',
builtin: true,
colors: {
background: '#263238',
foreground: '#eeffff',
cursor: '#ffffff',
cursorAccent: '#263238',
selectionBackground: '#2a4153',
black: '#263238',
red: '#f07178',
green: '#c3e88d',
yellow: '#ffcb6b',
blue: '#82aaff',
magenta: '#c792ea',
cyan: '#89ddff',
white: '#eeeeee',
brightBlack: '#546e7a',
brightRed: '#f07178',
brightGreen: '#c3e88d',
brightYellow: '#ffcb6b',
brightBlue: '#82aaff',
brightMagenta: '#c792ea',
brightCyan: '#89ddff',
brightWhite: '#ffffff'
}
}
]
/** Resolve a theme by id; falls back to the default dark theme. */
export function getThemeById(id: string, customThemes: TerminalTheme[] = []): TerminalTheme {
return [...BUILTIN_THEMES, ...customThemes].find((t) => t.id === id) ?? DEFAULT_DARK
}