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//!   `create` 以 js_namespace 绑定为成员调用,调用时才解析全局,脚本未加载时返回 Err。
6//! - `XtermOptions` 是普通 JS 对象(TS 侧为 interface):`XtermOptions::default()` 得到空对象,再用 setter 填充。
7//! - `TerminalHandle` 持有实例 + onReady 闭包,Drop → destroy()。
8//!
9//! WASM-only:所有 extern 与 handle 都在 `#[cfg(target_arch = "wasm32")] mod wasm` 内,
10//! server 构建整体剥离。无跨目标共享数据(纯渲染层)。
11//!
12//! 用法(Leptos):挂载点用 `NodeRef` 取元素,先用 `use_browser_library(BrowserLibrary::Xterm, ..)` 等库就绪,
13//! 再在 `Effect` 内构造 `XtermOptions` 并 `create(&element, &opts)`;`TerminalHandle` 含 JS 引用
14//! (非 `Send + Sync`),放进 `StoredValue::new_local`/`RwSignal::new_local`,`on_cleanup` 里 `take`
15//! 出来丢弃即触发 `Drop` → `destroy`。
16
17#[cfg(target_arch = "wasm32")]
18pub mod wasm {
19    use wasm_bindgen::prelude::*;
20    use wasm_bindgen::JsCast;
21
22    // —— window.XtermTerminal.create ——
23    //
24    // `js_namespace` 把 `create` 绑成 `XtermTerminal.create(...)` 的成员调用,
25    // 全局在调用时才解析:脚本未加载时返回 Err(TypeError),而不是在绑定阶段 panic。
26    #[wasm_bindgen]
27    extern "C" {
28        /// `XtermTerminal.create(container, options)`:在元素上创建终端,替换该元素上的旧实例。
29        /// 构造失败抛异常(被 catch 捕获)。
30        #[wasm_bindgen(js_namespace = XtermTerminal, js_name = create, catch)]
31        pub fn create(
32            container: &web_sys::HtmlElement,
33            options: &XtermOptions,
34        ) -> Result<TerminalInstance, JsValue>;
35    }
36
37    // —— 终端实例(TerminalInstance)——
38    #[wasm_bindgen]
39    extern "C" {
40        /// `XtermTerminal.create` 返回的终端实例对象,承载 xterm.js Terminal。
41        pub type TerminalInstance;
42
43        /// 流式写入 stdout 块(SSE stdout 事件)。
44        #[wasm_bindgen(method, js_name = writeStdout)]
45        pub fn write_stdout(this: &TerminalInstance, data: &str);
46
47        /// 流式写入 stderr 块(SSE stderr 事件),红色显示。
48        #[wasm_bindgen(method, js_name = writeStderr)]
49        pub fn write_stderr(this: &TerminalInstance, data: &str);
50
51        /// 整段写入(轮询兜底路径):清屏后重写 stdout + stderr。
52        #[wasm_bindgen(method, js_name = writeAll)]
53        pub fn write_all(this: &TerminalInstance, stdout: &str, stderr: &str);
54
55        /// 热切换明暗主题。
56        #[wasm_bindgen(method, js_name = setTheme)]
57        pub fn set_theme(this: &TerminalInstance, theme: &str);
58
59        /// 手动强制重新计算列宽。容器尺寸变化已由 JS 侧内部 ResizeObserver 自动处理,
60        /// 这里保留给需要立即刷新的边界场景(如容器从 display:none 切到可见)。
61        #[wasm_bindgen(method)]
62        pub fn fit(this: &TerminalInstance);
63
64        /// 清屏(新一轮运行前调用)。
65        #[wasm_bindgen(method)]
66        pub fn clear(this: &TerminalInstance);
67
68        /// 销毁终端,释放 JS 侧资源。
69        #[wasm_bindgen(method)]
70        pub fn destroy(this: &TerminalInstance);
71    }
72
73    // —— XtermOptions:用 builder 模式(setter)构造 JS 对象 ——
74    #[wasm_bindgen]
75    extern "C" {
76        /// 传给 `XtermTerminal.create` 的配置对象,对应 JS 侧的 XtermOptions。
77        /// 用 `default()` 创建空对象后通过 setter 设置字段。
78        pub type XtermOptions;
79
80        /// 主题:'light' 或 'dark'(配色取自 `CODE_PALETTES`)。
81        #[wasm_bindgen(method, setter, js_name = theme)]
82        pub fn set_theme(this: &XtermOptions, v: &str);
83
84        /// 字号(默认 13)。
85        #[wasm_bindgen(method, setter, js_name = fontSize)]
86        pub fn set_font_size(this: &XtermOptions, v: u32);
87
88        /// 终端就绪回调(构造末尾同步触发一次)。
89        #[wasm_bindgen(method, setter, js_name = onReady)]
90        pub fn set_on_ready(this: &XtermOptions, cb: &Closure<dyn FnMut()>);
91    }
92
93    impl Default for XtermOptions {
94        /// 空 JS 对象;字段只经上面的 setter 写入,JS 端按同名 interface 读取。
95        fn default() -> Self {
96            js_sys::Object::new().unchecked_into()
97        }
98    }
99
100    /// 终端实例句柄:持有 instance + onReady 闭包,Drop 时销毁实例并释放闭包。
101    ///
102    /// 闭包字段 `_` 前缀表示仅用于保持生命周期——它被注入 JS 后,JS 侧持有
103    /// 函数引用;只要 [`TerminalHandle`] 存活,闭包就不会被回收。Drop 时随结构释放。
104    pub struct TerminalHandle {
105        instance: TerminalInstance,
106        _on_ready: Closure<dyn FnMut()>,
107    }
108
109    impl TerminalHandle {
110        /// 调用方须先把 on_ready set 进 XtermOptions,再 create,
111        /// 然后把返回的 instance + 同一 closure 一起传入 new。
112        pub fn new(instance: TerminalInstance, on_ready: Closure<dyn FnMut()>) -> Self {
113            Self {
114                instance,
115                _on_ready: on_ready,
116            }
117        }
118
119        /// 借用底层实例,供宿主调 writeStdout/writeStderr/setTheme 等。
120        pub fn instance(&self) -> &TerminalInstance {
121            &self.instance
122        }
123    }
124
125    impl Drop for TerminalHandle {
126        fn drop(&mut self) {
127            // 销毁 JS 侧终端;随后 _on_ready 字段释放,释放 wasm-bindgen 函数表槽位。
128            self.instance.destroy();
129        }
130    }
131}
132
133#[cfg(target_arch = "wasm32")]
134pub use wasm::*;