上一篇文章介绍了 Zellij 插件系统的架构和 API。本文将从实践角度出发,带你从零开始构建一个 Zellij 插件,包括开发环境搭建、理解插件生命周期、编写完整的最小插件,以及探索多语言开发的可能性。
一、开发环境搭建
创建项目
使用 Cargo 创建一个新的库项目:
cargo new --lib my-zellij-plugin
cd my-zellij-plugin
配置 Cargo.toml
Zellij 插件编译为 WASM 模块,需要配置 crate-type 为 cdylib,并添加 zellij-tile 依赖:
[package]
name = "my-zellij-plugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
zellij-tile = "0.41.0"
注意:zellij-tile 的版本号必须与你安装的 Zellij 版本匹配。可以使用 zellij --version 查看当前版本,然后在 crates.io 查找对应的 zellij-tile 版本。
安装 WASM 目标
Zellij 使用 WASI(WebAssembly System Interface)运行插件,需要安装对应的编译目标:
rustup target add wasm32-wasip1
然后编译项目:
cargo build --target wasm32-wasip1
编译产物位于 target/wasm32-wasip1/debug/my_zellij_plugin.wasm。
编译和开发循环
Zellij 提供了便捷的开发命令 start-or-reload-plugin,它会加载插件并在 WASM 文件变化时自动重新加载:
# 启动或重新加载插件
zellij start-or-reload-plugin target/wasm32-wasip1/debug/my_zellij_plugin.wasm
典型的开发循环如下:
- 修改 Rust 代码
- 运行
cargo build --target wasm32-wasip1 - Zellij 自动检测文件变化并重新加载插件
- 在 Zellij 中查看效果
二、插件生命周期
每个 Zellij 插件都遵循相同的生命周期:load → subscribe → update → render 循环。理解这个循环是开发插件的关键。
load(初始化)
load 方法在插件首次加载时调用一次。这是执行初始化逻辑的地方:
- 初始化插件状态
- 解析配置参数
- 订阅感兴趣的事件
fn load(&mut self, configuration: Vec<(String, String)>) {
// 解析配置
for (key, value) in configuration {
match key.as_str() {
"greeting" => self.greeting = value,
_ => {},
}
}
// 订阅事件
subscribe(&[EventType::Key, EventType::Mouse]);
}
update(处理事件)
update 方法在每次有订阅的事件发生时调用。这是处理用户输入、状态变化和定时器事件的核心位置:
fn update(&mut self, event: Event) -> bool {
match event {
Event::Key(key_event) => {
// 处理按键
self.handle_key(key_event);
true // 返回 true 表示需要重新渲染
},
Event::Mouse(mouse_event) => {
// 处理鼠标
self.handle_mouse(mouse_event);
true
},
_ => false,
}
}
pipe(接收管道消息)
pipe 方法在收到管道消息时调用,用于处理来自 CLI、键位绑定或其他插件的消息:
fn pipe(&mut self, message: PipeMessage) -> bool {
// message.name 包含消息名称
// message.payload 包含消息内容
// message.source 表示消息来源
match message.name.as_str() {
"refresh" => {
self.refresh_data();
true
},
_ => false,
}
}
render(渲染UI)
render 方法在需要绘制插件界面时调用。插件通过向终端输出 ANSI 文本和转义序列来渲染 UI:
fn render(&mut self, rows: usize, cols: usize) {
// rows 和 cols 是插件面板的可用尺寸
println!("Hello from my plugin!");
println!("Panel size: {}x{}", cols, rows);
}
完整的生命周期流程:
插件加载
│
▼
load() ─── 初始化状态 + 订阅事件
│
▼
render() ─── 首次渲染
│
▼
┌──────────────────────────────┐
│ 等待事件 │
│ │ │
│ ├─ 事件到达 ──→ update() ──→│
│ │ │ │
│ │ 需要渲染? │
│ │ │ │
│ │ render() ←──────┘
│ │ │
│ ├─ 管道消息 ──→ pipe() ────→│
│ │ │ │
│ │ 需要渲染? │
│ │ │ │
│ │ render() ←──────┘
│ │ │
│ └─→ 返回等待事件 ──────────→│
└──────────────────────────────┘
三、完整最小插件示例
以下是一个完整的 HelloWorld 插件,它显示问候语并响应按键切换消息:
// src/lib.rs
use zellij_tile::prelude::*;
struct HelloWorldPlugin {
message: String,
counter: u32,
}
impl Default for HelloWorldPlugin {
fn default() -> Self {
Self {
message: "Hello, Zellij!".to_string(),
counter: 0,
}
}
}
impl ZellijPlugin for HelloWorldPlugin {
fn load(&mut self, _configuration: Vec<(String, String)>) {
// 订阅键盘事件
subscribe(&[EventType::Key]);
}
fn update(&mut self, event: Event) -> bool {
if let Event::Key(key) = event {
match key.bare_key {
Key::Char(' ') => {
self.counter += 1;
self.message = format!("Pressed {} times!", self.counter);
return true; // 需要重新渲染
}
Key::Char('r') => {
self.counter = 0;
self.message = "Reset!".to_string();
return true;
}
Key::Char('q') => {
close_focus();
return false;
}
_ => {}
}
}
false
}
fn render(&mut self, rows: usize, cols: usize) {
// 使用 ANSI 转义序列美化输出
println!("\x1b[1;36m{}\x1b[0m", self.message);
println!("");
println!("Panel: {}x{} | Press SPACE to count | R to reset | Q to quit", cols, rows);
}
}
// 注册插件
register_plugin!(HelloWorldPlugin);
使用布局加载插件
编译后,创建一个布局文件来加载这个插件:
// layout.kdl
layout {
pane {
plugin location="file:///path/to/target/wasm32-wasip1/debug/my_zellij_plugin.wasm"
}
}
或者直接用命令加载:
cargo build --target wasm32-wasip1
zellij start-or-reload-plugin target/wasm32-wasip1/debug/my_zellij_plugin.wasm
四、示例插件参考
内置插件源码
学习插件开发最好的方式之一是阅读 Zellij 内置插件的源码。这些插件在 Zellij 的 GitHub 仓库中:
- tab-bar:标签栏的实现,展示了如何响应标签变化事件和渲染自定义 UI
- status-bar:状态栏的实现,展示了模式切换、快捷键提示和样式化输出
- compact-bar:紧凑型底部栏,展示了如何将多个功能整合到一个小型插件中
- strider:文件浏览器,展示了文件系统访问、目录导航和用户交互
- session-manager:会话管理器,展示了会话列表、切换和管理的完整实现
所有内置插件源码位于 zellij-utils/src/plugins/ 目录下。
外部示例仓库
社区维护了一些示例插件项目,可以作为开发参考:
- Zellij 官方示例插件仓库
- 社区贡献的各类实用插件(在 GitHub 上搜索
zellij-plugin)
五、使用其他语言开发
虽然 Rust 是 Zellij 插件的官方支持语言,但由于插件基于 WASM,理论上任何能编译到 WASM 的语言都可以使用。
社区语言支持
| 语言 | 状态 | 说明 |
|---|---|---|
| Python | 社区探索中 | 通过 Pyodide 或 wasm32 编译,目前限制较多 |
| Go | 社区探索中 | TinyGo 支持 WASM 目标,但 WASI 支持有限 |
| C/C++ | 可行 | 通过 Emscripten 或 wasi-sdk 编译为 WASM |
| AssemblyScript | 可行 | TypeScript 风格语法,天然支持 WASM |
限制说明
使用非 Rust 语言开发插件存在以下限制:
- 没有官方 SDK:
zellij-tile只提供 Rust 版本,其他语言需要手动实现 WASM 导出接口 - ABI 兼容性:需要正确实现 Zellij 期望的 WASM 导出函数签名(
load、update、render等) - 序列化格式:事件和命令数据使用特定的二进制格式,需要正确处理
- 生态支持:缺少其他语言的辅助库和工具链
如果你有兴趣尝试非 Rust 语言开发,建议先从 AssemblyScript 入手,它的语法更接近 TypeScript,且对 WASM 有原生支持。
六、版本兼容性
Zellij 插件的版本兼容性是一个重要的注意事项。
核心规则:zellij-tile crate 的版本必须与 Zellij 的版本匹配。
| Zellij 版本 | zellij-tile 版本 | 说明 |
|---|---|---|
| 0.41.0 | 0.41.0 | 版本号完全一致 |
| 0.40.0 | 0.40.0 | 不同版本之间不兼容 |
如果版本不匹配,可能会遇到以下问题:
- 插件无法加载(ABI 不兼容)
- 缺少新版本的 API 函数
- 事件类型或数据结构不匹配
检查和解决版本问题:
# 检查 Zellij 版本
zellij --version
# 在 Cargo.toml 中指定匹配的 zellij-tile 版本
[dependencies]
zellij-tile = "0.41.0" # 替换为你的 Zellij 版本
# 如果使用最新开发版 Zellij
# 可以直接使用 git 依赖
[dependencies]
zellij-tile = { git = "https://github.com/zellij-org/zellij", branch = "main" }
开发插件时,建议始终使用最新稳定版 Zellij 和对应的 zellij-tile,并在发布插件时注明兼容的 Zellij 版本范围。