Skip to main content

yggdrasil/bridges/
xterm.rs

1//! xterm.js 终端桥接:输出专用(无 stdin),配合 SSE 流式渲染容器 stdout/stderr。
2//!
3//! 镜像 `codemirror.rs` 范式:
4//! - `window.XtermTerminal` 是 IIFE 产物挂在 window 上的对象字面量(含 create 方法),
5//!   不是函数。用 `js_sys::Reflect::get` 做属性访问拿到模块对象,再 `unchecked_into`。
6//! - `XtermOptions` 是 class(非 interface),TS 擦除后存活,wasm 侧能 `new`。
7//! - `TerminalHandle` 持有实例 + onReady 闭包,Drop → destroy()。
8//!
9//! WASM-only:所有 extern 与 handle 都在 `#[cfg(target_arch = "wasm32")] mod wasm` 内,
10//! server 构建整体剥离。无跨目标共享数据(纯渲染层)。
11
12#[cfg(target_arch = "wasm32")]
13pub mod wasm {
14    use wasm_bindgen::prelude::*;
15    use wasm_bindgen::JsCast;
16
17    // —— window.XtermTerminal 模块对象 ——
18    //
19    // XtermTerminal 是 IIFE 产物挂在 window 上的对象字面量(含 create 方法),
20    // 不是函数。wasm-bindgen 对 `fn get_module() -> T` 形式的 extern 会生成
21    // `window.XtermTerminal()`(函数调用),会因 "not a function" 失败。
22    // 因此用 js_sys::Reflect::get 做属性访问拿到模块对象,再 unchecked_into。
23    #[wasm_bindgen]
24    extern "C" {
25        /// `window.XtermTerminal` 模块对象的 Rust 映射(IIFE 产物挂在 window 上的对象字面量)。
26        /// 不是函数——通过 [`get_module`] 用 Reflect::get 取属性而非 extern fn 调用拿到。
27        pub type XtermTerminalModule;
28
29        /// 调用 `XtermTerminal.create(containerId, opts)`。
30        /// 找不到容器返回 null(被 Option 捕获);构造失败抛异常(被 catch 捕获)。
31        #[wasm_bindgen(method, catch)]
32        pub fn create(
33            this: &XtermTerminalModule,
34            container_id: &str,
35            opts: &XtermOptions,
36        ) -> Result<Option<TerminalInstance>, JsValue>;
37    }
38
39    /// 读取 `window.XtermTerminal`(IIFE 默认导出,顶层 var 即 window 属性)。
40    /// 用 Reflect::get 做属性访问——extern fn 形式会被 wasm-bindgen 编成函数调用。
41    ///
42    /// 用 unchecked_into 而非 dyn_into:XtermTerminal 是 JS 对象字面量,
43    /// 不是 wasm-bindgen 注册的构造函数实例,dyn_into 的 instanceof 检查必然失败。
44    /// unchecked_into 只做编译期类型标注,不做运行时校验
45    /// (Reflect.get 已保证拿到的是目标对象)。
46    pub fn get_module() -> XtermTerminalModule {
47        // 缺 window:本函数只应在浏览器环境的 wasm32 前端调用,不应在其它上下文触发。
48        let window = web_sys::window().expect("no window: get_module 只能在浏览器 wasm32 前端调用");
49        // 调用方先等待 use_browser_library 就绪,再构造 Options 并调用 create。
50        let val = js_sys::Reflect::get(&window, &"XtermTerminal".into()).expect(
51            "window.XtermTerminal missing: /xterm/terminal.js 未加载,检查该静态资源是否随构建产物部署",
52        );
53        val.unchecked_into::<XtermTerminalModule>()
54    }
55
56    // —— 终端实例(TerminalInstance)——
57    #[wasm_bindgen]
58    extern "C" {
59        /// `XtermTerminal.create` 返回的终端实例对象,承载 xterm.js Terminal。
60        pub type TerminalInstance;
61
62        /// 流式写入 stdout 块(SSE stdout 事件)。
63        #[wasm_bindgen(method, js_name = writeStdout)]
64        pub fn write_stdout(this: &TerminalInstance, data: &str);
65
66        /// 流式写入 stderr 块(SSE stderr 事件),红色显示。
67        #[wasm_bindgen(method, js_name = writeStderr)]
68        pub fn write_stderr(this: &TerminalInstance, data: &str);
69
70        /// 整段写入(轮询兜底路径):清屏后重写 stdout + stderr。
71        #[wasm_bindgen(method, js_name = writeAll)]
72        pub fn write_all(this: &TerminalInstance, stdout: &str, stderr: &str);
73
74        /// 热切换主题(Catppuccin Latte/Mocha)。
75        #[wasm_bindgen(method, js_name = setTheme)]
76        pub fn set_theme(this: &TerminalInstance, theme: &str);
77
78        /// 手动强制重新计算列宽。容器尺寸变化已由 JS 侧内部 ResizeObserver 自动处理,
79        /// 这里保留给需要立即刷新的边界场景(如容器从 display:none 切到可见)。
80        #[wasm_bindgen(method)]
81        pub fn fit(this: &TerminalInstance);
82
83        /// 清屏(新一轮运行前调用)。
84        #[wasm_bindgen(method)]
85        pub fn clear(this: &TerminalInstance);
86
87        /// 销毁终端,释放 JS 侧资源。
88        #[wasm_bindgen(method)]
89        pub fn destroy(this: &TerminalInstance);
90    }
91
92    // —— XtermOptions:用 builder 模式(setter)构造 JS 对象 ——
93    #[wasm_bindgen]
94    extern "C" {
95        /// 传给 `XtermTerminal.create` 的配置对象,对应 JS 侧的 XtermOptions。
96        /// 用 `new()` 创建空对象后通过 setter 链式设置字段。
97        pub type XtermOptions;
98
99        /// 构造一个空的 XtermOptions,随后用各 setter 填充。
100        #[wasm_bindgen(constructor)]
101        pub fn new() -> XtermOptions;
102
103        /// 主题:'light'(Catppuccin Latte)或 'dark'(Catppuccin Mocha)。
104        #[wasm_bindgen(method, setter, js_name = theme)]
105        pub fn set_theme(this: &XtermOptions, v: &str);
106
107        /// 字号(默认 13)。
108        #[wasm_bindgen(method, setter, js_name = fontSize)]
109        pub fn set_font_size(this: &XtermOptions, v: u32);
110
111        /// 终端就绪回调(构造末尾同步触发一次)。
112        #[wasm_bindgen(method, setter, js_name = onReady)]
113        pub fn set_on_ready(this: &XtermOptions, cb: &Closure<dyn FnMut()>);
114    }
115
116    /// 终端实例句柄:持有 instance + onReady 闭包,Drop 时销毁实例并释放闭包。
117    ///
118    /// 闭包字段 `_` 前缀表示仅用于保持生命周期——它被注入 JS 后,JS 侧持有
119    /// 函数引用;只要 [`TerminalHandle`] 存活,闭包就不会被回收。Drop 时随结构释放。
120    pub struct TerminalHandle {
121        instance: TerminalInstance,
122        _on_ready: Closure<dyn FnMut()>,
123    }
124
125    impl TerminalHandle {
126        /// 调用方须先把 on_ready set 进 XtermOptions,再 create,
127        /// 然后把返回的 instance + 同一 closure 一起传入 new。
128        pub fn new(instance: TerminalInstance, on_ready: Closure<dyn FnMut()>) -> Self {
129            Self {
130                instance,
131                _on_ready: on_ready,
132            }
133        }
134
135        /// 借用底层实例,供宿主调 writeStdout/writeStderr/setTheme 等。
136        pub fn instance(&self) -> &TerminalInstance {
137            &self.instance
138        }
139    }
140
141    impl Drop for TerminalHandle {
142        fn drop(&mut self) {
143            // 销毁 JS 侧终端;随后 _on_ready 字段释放,释放 wasm-bindgen 函数表槽位。
144            self.instance.destroy();
145        }
146    }
147}
148
149#[cfg(target_arch = "wasm32")]
150pub use wasm::*;