// xterm.js 终端实例:输出专用(无 stdin),配合 SSE 流式渲染容器 stdout/stderr。 // // 设计约束: // - disableStdin: true —— 读者侧不可交互输入,纯输出展示。 // - convertEol: true —— 容器输出的 \n 自动转 \r\n(终端换行语义)。 // - stderr 用 ANSI 红色前缀包裹,与 stdout 视觉区分。 // // 镜像 codemirror-editor/src/editor.ts 的范式:Options 用 class(非 interface) // 以便 TS 擦除后存活,wasm 侧能用 new XtermOptions() 构造。 import { FitAddon } from '@xterm/addon-fit'; import { Terminal } from '@xterm/xterm'; import '@xterm/xterm/css/xterm.css'; import { DARK_THEME, LIGHT_THEME, type ThemeName } from './themes'; /** 传给 XtermTerminal.create 的配置。 * 必须是 class(非 interface),以便 TS 擦除后存活, * wasm 侧能用 `new XtermOptions()` 构造,并通过 setter 填充字段。 */ export class XtermOptions { theme?: ThemeName; fontFamily?: string; fontSize?: number; onReady?: () => void; } /** ANSI 红色 + reset 包裹 stderr 文本。 */ function red(text: string): string { return `\x1b[31m${text}\x1b[0m`; } /** xterm.js 终端实例封装。输出专用,提供 stdout/stderr 流式写入与整段写入两种模式。 */ export class TerminalInstance { private term: Terminal; private fitAddon: FitAddon; constructor(container: HTMLElement, options: XtermOptions) { this.term = new Terminal({ // 容器输出是 \n,终端需要 \r\n 才能回车换行;convertEol 自动转换。 convertEol: true, // 输出专用:不接收键盘输入,禁用光标闪烁。 disableStdin: true, cursorBlink: false, cursorStyle: 'bar', fontFamily: options.fontFamily ?? 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace', fontSize: options.fontSize ?? 13, lineHeight: 1.3, scrollback: 5000, theme: options.theme === 'dark' ? DARK_THEME : LIGHT_THEME, }); this.fitAddon = new FitAddon(); this.term.loadAddon(this.fitAddon); this.term.open(container); // 初始 fit,让列宽适配容器宽度。 this.fitAddon.fit(); options.onReady?.(); } /** 流式写入 stdout 块(SSE stdout 事件)。 */ writeStdout(data: string): void { this.term.write(data); } /** 流式写入 stderr 块(SSE stderr 事件),红色显示。 */ writeStderr(data: string): void { this.term.write(red(data)); } /** 整段写入(轮询兜底路径拿到完整结果时):清屏后重写 stdout + stderr。 */ writeAll(stdout: string, stderr: string): void { this.term.reset(); this.term.write(stdout); if (stderr) { this.term.write(red(stderr)); } } /** 切换主题(热切换,无需重建实例)。 */ setTheme(theme: ThemeName): void { this.term.options.theme = theme === 'dark' ? DARK_THEME : LIGHT_THEME; } /** 重新计算列宽以适配容器(如父容器尺寸变化时调用)。 */ fit(): void { this.fitAddon.fit(); } /** 清屏(新一轮运行前调用)。 */ clear(): void { this.term.reset(); } /** 销毁实例,释放 DOM 与事件监听。EditorHandle::drop → destroy。 */ destroy(): void { this.term.dispose(); } } // 暴露 XtermOptions 到 window,供 wasm-bindgen 用 new XtermOptions()。 // IIFE 的 name 只能挂一个全局(XtermTerminal),故手动 hoist XtermOptions。 declare global { interface Window { XtermOptions: typeof XtermOptions; } } window.XtermOptions = XtermOptions;