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:
commit
a9acdbe500
99 files changed
+23940
No files matched your search
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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' }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user