OpenTerminal v0.2.1: terminal suite with SSH, split panes, themes and keyword highlighting

- Local terminal (node-pty) + SSH sessions (ssh2) with unified data plane
- dockview split panes, per-group '+' tab bar button, layout templates
- Session replay buffer (late-subscriber catch-up), host key pinning,
  credential encryption via safeStorage
- 12 builtin themes + custom theme editor, system font enumeration
- Keyword highlighting: regex rules with priority, ANSI truecolor injection,
  chunk-boundary-safe stream (carry-over of partial escape sequences)
- Dark antd theme app-wide, settings dialog redesign, error boundary
- Tests: ssh loopback, session e2e, highlight split-chunk regression
This commit is contained in:
954801926@qq.com committed 2026-09-06 11:19:08 +08:00
commit 551723c2d4
58 files changed
+11929

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
import { contextBridge, ipcRenderer } from 'electron'
import { Ipc } from '../shared/ipc'
import type { AppSettings } from '../shared/settings'
import type { AppApi } from '../shared/api'
import type { LayoutMeta, PtyCreateOptions, PtyDataEvent, PtyExitEvent } from '../shared/ipc'
import type {
HostKeyAction,
HostKeyPromptEvent,
SessionOpenOptions,
SshConnectionInput
} from '../shared/connections'
const api: AppApi = {
appInfo: () => ipcRenderer.invoke(Ipc.APP_INFO),
createPty: (opts?: PtyCreateOptions) => ipcRenderer.invoke(Ipc.PTY_CREATE, opts),
openSession: (opts: SessionOpenOptions) => ipcRenderer.invoke(Ipc.SESSION_OPEN, opts),
getSessionReplay: (id: string) => ipcRenderer.invoke(Ipc.SESSION_REPLAY, id),
writePty: (id: string, data: string) => ipcRenderer.send(Ipc.PTY_WRITE, id, data),
resizePty: (id: string, cols: number, rows: number) =>
ipcRenderer.send(Ipc.PTY_RESIZE, id, cols, rows),
killPty: (id: string) => ipcRenderer.send(Ipc.PTY_KILL, id),
onPtyData: (cb: (e: PtyDataEvent) => void) => {
const listener = (_: unknown, e: PtyDataEvent): void => cb(e)
ipcRenderer.on(Ipc.PTY_DATA, listener)
return () => ipcRenderer.removeListener(Ipc.PTY_DATA, listener)
},
onPtyExit: (cb: (e: PtyExitEvent) => void) => {
const listener = (_: unknown, e: PtyExitEvent): void => cb(e)
ipcRenderer.on(Ipc.PTY_EXIT, listener)
return () => ipcRenderer.removeListener(Ipc.PTY_EXIT, listener)
},
listConnections: () => ipcRenderer.invoke(Ipc.CONNECTIONS_LIST),
saveConnection: (input: SshConnectionInput) => ipcRenderer.invoke(Ipc.CONNECTIONS_SAVE, input),
deleteConnection: (id: string) => ipcRenderer.invoke(Ipc.CONNECTIONS_DELETE, id),
onHostKeyPrompt: (cb: (e: HostKeyPromptEvent) => void) => {
const listener = (_: unknown, e: HostKeyPromptEvent): void => cb(e)
ipcRenderer.on(Ipc.HOSTKEY_PROMPT, listener)
return () => ipcRenderer.removeListener(Ipc.HOSTKEY_PROMPT, listener)
},
respondHostKey: (promptId: string, action: HostKeyAction) =>
ipcRenderer.send(Ipc.HOSTKEY_RESPOND, promptId, action),
getSettings: () => ipcRenderer.invoke(Ipc.SETTINGS_GET),
saveSettings: (next: AppSettings) => ipcRenderer.invoke(Ipc.SETTINGS_SET, next),
onSettingsChanged: (cb: (s: AppSettings) => void) => {
const listener = (_: unknown, s: AppSettings): void => cb(s)
ipcRenderer.on(Ipc.SETTINGS_CHANGED, listener)
return () => ipcRenderer.removeListener(Ipc.SETTINGS_CHANGED, listener)
},
listFonts: () => ipcRenderer.invoke(Ipc.FONTS_LIST),
listLayouts: () => ipcRenderer.invoke(Ipc.LAYOUTS_LIST),
getLayout: (id: string) => ipcRenderer.invoke(Ipc.LAYOUTS_GET, id),
saveLayout: (meta: LayoutMeta, json: string) => ipcRenderer.invoke(Ipc.LAYOUTS_SAVE, meta, json),
deleteLayout: (id: string) => ipcRenderer.invoke(Ipc.LAYOUTS_DELETE, id)
}
contextBridge.exposeInMainWorld('api', api)