xfy 246d8a6d67 feat(libs): add xterm-terminal IIFE library (xterm.js 6.0)
New pnpm workspace package @yggdrasil/xterm-terminal, output to
public/xterm/. Output-only terminal (disableStdin) for streaming
container stdout/stderr via SSE.

- @xterm/xterm@^6.0.0 + @xterm/addon-fit@^0.11.0 (released same day,
  2025-12-22; 0.10.0's peerDependency is ^5.0.0 and incompatible)
- convertEol: containers emit \n, xterm needs \r\n
- stderr wrapped in ANSI red for visual distinction
- Catppuccin Latte/Mocha themes matching project highlight.css
- Mirrors codemirror-editor IIFE pattern: object literal default export,
  XtermOptions as class (survives TS erasure for wasm-bindgen)
- css.d.ts declares *.css module for tsc side-effect import
2026-07-10 11:37:04 +08:00

109 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;