Zellij Plugin Snapshot
A command-line host that loads your Zellij plugin .wasm, feeds it the same events a real session would, and writes two files:
| File | What it is |
|---|---|
{name}.ansi.txt |
Exact bytes from render(rows, cols) (SGR + Zellij UI DCS) |
{name}.svg |
That pane painted as self-contained glyph outlines (no font needed to view it) |
Pinned to Zellij / zellij-tile / zellij-utils 0.45.1. A plugin built against another version will not speak this protobuf.
This crate is a command. You run it against a wasm you already built.
Do not list it in [dependencies]: that graph is compiled into the plugin you ship.
Do not list it in [dev-dependencies] either, on stable Cargo. That field still means “link this library into cargo test.” This package has no library target, and even if it did, Cargo would not put the snapshot binary on your PATH or into your tests. Unstable artifact dependencies (artifact = "bin") are the Cargo-native way to depend on someone else’s binary; this project does not require that.
Install one published version. crates.io stores the source. cargo install compiles that source on your machine and puts the binary on PATH:
--locked builds the dependency set in the published Cargo.lock. Cargo includes that file because this package has a binary. The command leaves an existing install in place when that version is already installed, and rebuilds when the version you name is different. cargo update does not touch an installed binary. A newer release on crates.io does not replace yours until you run cargo install again and name the new version.
This repository’s rust-toolchain.toml is for CI and for developing this repository. It is excluded from the published package, so cargo install uses the cargo already on your PATH.
Use it on your plugin
1. Build your plugin wasm
From your plugin crate (the one with crate-type = ["cdylib"] and zellij-tile = "0.45.1"):
The artifact is typically:
target/wasm32-wasip1/release/<your_crate>.wasm
2. Get this tool
To work from a clone:
The Nerd Font is compiled into the binary. Font licenses are in THIRD_PARTY_NOTICES.md.
3. Write a drive script
A YAML file describes the pane size and the events to send before render. Put it in your repo. Paths in plugin: that are not absolute are relative to this YAML file.
shots/normal.yaml:
name: normal
plugin: ../target/wasm32-wasip1/release/your_plugin.wasm
geometry:
rows: 1
cols: 80
steps:
- action: grant_permissions
- action: mode_update
mode: normal
session: demo
- action: tab_update
tabs:
- name: Tab
active: true
tiled: 1
name is the output stem (normal.ansi.txt, normal.svg). If omitted, the YAML file stem is used.
4. Run the host
From your plugin directory:
# via cargo, without installing
# or if installed
--out is the directory for the two files (created if needed). Default is the current working directory.
Commit the YAML and the .ansi.txt snapshot. The SVG is for looking at; do not diff it in tests.
5. Tests and CI
A test or CI job is: build wasm, run the host, diff the ANSI file.
GitHub Actions sketch:
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-wasip1
- run: cargo build --target wasm32-wasip1 --release
- run: cargo install zellij-plugin-snapshot --version 0.2.2 --locked
- run: zellij-plugin-snapshot shots/normal.yaml --out /tmp/shots
- run: diff -u shots/normal.ansi.txt /tmp/shots/normal.ansi.txt
Drive script reference
name: optional-output-stem
plugin: path/to/plugin.wasm # relative to this YAML file
config: # string map passed to load()
welcome_screen: "true"
ids: # optional; defaults are 1 / /tmp
plugin_id: 1
zellij_pid: 1
client_id: 1
initial_cwd: /tmp
world: # session list for GetSessionList
current: demo
sessions:
resurrectable:
geometry:
rows: 1 # passed to render(rows, cols)
cols: 80
steps: # events, in order, then render
- action: grant_permissions
- action: initial_keybinds
- action: mode_update
mode: normal # default normal
session: demo
- action: tab_update
tabs:
- name: Tab
active: true
tiled: 1
floating: 0
floating_visible: false
- action: session_update
current: demo
sessions:
resurrectable:
- action: event_json
json: '{"ModeUpdate": ...}' # raw zellij_utils::data::Event
mode_update always applies a default-session theme (Styling::from(default_palette())) and arrow_fonts: false (Nerd Font separators), matching a typical local Zellij 0.45 session.
Which steps your plugin needs depends on what it reads in update:
| Plugin kind | Typical steps |
|---|---|
| Ignores host (hello-world) | steps: [] |
| Status / tab bar | grant_permissions, mode_update, tab_update |
| Stock status-bar | also initial_keybinds (it paints the keymap) |
| Session / welcome UI | world: plus session_update |
If render is empty or the wasm traps, add the query the plugin makes (GetSessionList, permissions, …) via the fields above. Unknown host commands are stubbed so the module can continue.
Outputs
- ANSI (
.ansi.txt) — source of truth for snapshots.diffthis file. Open it in a terminal, orcatit. Includes private DCS (ESC Pz … ESC \) when the plugin uses Zellij ribbons/tables/text. - SVG — the same pane as outlines, for humans. Cell grid is 9.60×21.12 px per column/row at 16px JetBrains Mono Nerd Font. Viewers do not need the font installed. Do not snapshot-
diffSVG.
Theme for DCS expansion is the same default palette as mode_update. Unstyled cells sit on a black pane.
This tool captures one plugin per run (render(rows, cols) for that wasm). It does not load a Zellij layout or compose tab bar, panes, and status bar into one image.
This repository’s examples
These check the host against known plugins. They are not required to snapshot yours.
That writes examples/out/{template,status-bar-nano,status-bar-stock,welcome,tab-bar-ribbons}.{ansi.txt,svg}.
fetch-plugins.sh downloads the fulldecent template wasm, builds sibling zellij-status-bar-ng and zellij-tab-bar-ribbons if those trees exist, and builds Zellij v0.45.1 status-bar and session-manager.
CLI
zellij-plugin-snapshot script.yaml [--out DIR]
| Argument | Meaning |
|---|---|
script.yaml |
Drive script; relative to the current working directory |
--out DIR |
Directory for {name}.ansi.txt and {name}.svg (default .) |
Maintenance and dependency updates
Do this every month or so and please send a PR here if you see updates available:
- Identify external Actions in .github/workflows scripts and look for available new versions. Review and then update to the new version if it is safe. GitHub-supported Actions (i.e. under the
actions/organization) may require only cursory review. - Review crates in Cargo.toml and refresh Cargo.lock.
anyhow,clap,serde,serde_json,ttf-parser, andunicode-widthmay take current compatible releases. - Keep
zellij-utils0.45.1,wasmi/wasmi_wasi1.1.0, andprost0.12 until Zellij itself ships a newer ABI. Wasmi 2.x is a different interpreter than Zellij 0.45 uses. This snapshot tool will only ever support the latest version of Zellij andzellij-utils. serde_yaml0.9 is deprecated. A later swap should be a maintained 0.9-compatible crate such asserde_yaml_ng, notserde_yml.- Review the Zellij tag and sibling plugin paths in examples/fetch-plugins.sh when example WASMs should track a new host.
- A release is one commit that contains the version bump in Cargo.toml and the same number in every
cargo install --versionline in this README, and a git tag of that version on that commit, with novprefix. Pushing that tag runs release.yml. That workflow publishes the crate to crates.io, then creates a GitHub Release with the same name as the tag. The Release body is thecargo install --versioncommand for that version. The Release has no attached binary. Older versions stay on crates.io so an existing pin keeps installing.
References
- This crate is a command-line host. Plugin authors run the binary against their wasm. They do not add it to
[dependencies]or to[dev-dependencies]on stable Cargo (that field links a library, and this package is not one). - Installation follows
cargo install. The registry holds source. Name the version. An install stays on that version until a latercargo installnames a different one. - Publishing from GitHub Actions follows crates.io Trusted Publishing. The crate's trusted publisher names the workflow file
release.ymland leaves the environment empty. GitHub's OIDC token is exchanged for a token that lasts about 30 minutes. No crates.io token is stored in this repository. - A git tag and a GitHub Release are different objects. GitHub documents that in About releases. This repository creates the Release from the tag workflow with
gh release create, using the job'sGITHUB_TOKEN. The job needscontents: writefor that call. - We use title case for titles and proper nouns; not for headings and things. This includes this README as well as workflow rules and other configuration files.
- We use an MIT license for this project’s source, Copyright (c) 2026 William Entriken. See LICENSE. The bundled JetBrains Mono Nerd Font is OFL 1.1 plus Nerd Fonts’ terms; see THIRD_PARTY_NOTICES.md. The published package license is
MIT AND OFL-1.1because the crate contains both works. In an SPDX expression,ANDmeans a recipient complies with both. Cargo documents that field as an SPDX expression: Thelicenseandlicense-filefields. - Zellij can load a plugin from an HTTPS URL. That is simpler and insecure. We treat that as wrong and do not document it.
- This project is built based on best practices documented in zellij-plugin-template, release 1.0.0.
- This project is built based on best practices documented in project-template, release 1.0.0.