A zellij plugin that replaces the thin one-row tab bar with a taller, multi-row tab bar in which every tab is drawn as a color-coded minimap of its own pane layout — a tiny pixel-grid thumbnail of how that tab's terminal is split. Panes are identified by color; where a tab is wide enough, a summarized title is overlaid; the ⌘N switch hint is shown per tab. The active tab stands out — vivid fills, a focus ring on its focused pane, and an optional perspective lift that floats it forward.
The renderer rendered standalone in a terminal — five tabs of varied layouts (a single pane, a 2-column split, a 2×2 grid, a main+stack, and a 2-row split) drawn as color-coded pane minimaps. The active tab (
⌘ 3) is lifted forward by the perspective depth cue while the inactive tabs recede a half-row at top and bottom; pane fills carry the gradient sheen, the focused pane wears its outline ring, and each tab shows its⌘ Nswitch hint. Reproduce it with therender_demoexample. This renderer is wired into the live zellij tab bar, including click-to-switch — see Status.
Box-drawing rules can only place a line on a cell boundary. The upper-half-block glyph ▀ paints its foreground color on the top half of a cell and its background color on the bottom half, so the color can change within a single cell. That doubles the vertical resolution (a 3-text-row block becomes a 6-pixel-tall grid) and lets even finely split layouts render as distinct color bands instead of collapsing into noise. It's the same half-block technique image-to-terminal tools (chafa, timg) use, applied to a pane map.
3 text rows left A (full height) right: top B / bottom C, split by ▀
row 1 │ █ A █ │ ▀▀▀ fg=B bg=B (top & bottom both B)
row 2 │ █ A █ │ ▀▀▀ fg=B bg=C (top half B / bottom half C — split mid-cell)
row 3 │ █ A █ │ ▀▀▀ fg=C bg=C (top & bottom both C)
A focused pane is marked with an outline ring and a bold label — its fill keeps the same identity hue as when unfocused, so a pane never changes color as focus moves. The ring is a luminance-shifted shade of the pane's own fill (a blue pane gets a slightly different blue outline), so the highlight stays in the pane's hue family. Titles degrade gracefully — labels that cannot fit are dropped rather than truncated into noise.
✨ Usable today, actively developed. Installable from a prebuilt wasm (no build step), with gradients, active-tab cues, and perspective depth all shipped.
- ✅ The minimap renderer (
src/minimap.rs) is feature-complete and unit-tested (HSL palette, half-block grid, gradient sheen, focus ring + active-tab emphasis, perspective depth, label degradation). It has no zellij dependency, so it runs and is tested on the native host. - ✅ The full render pipeline is wired: every tab is projected from zellij's live
PaneManifest, packed into column spans (src/line.rs), assembled into a per-tab block at its budgeted width (src/tab_block.rs), and composed into the multi-row bar (src/paint.rs). By default the active tab is centered, so the strip slides to follow focus; setalign "left"to anchor the row instead. Tabs that don't fit collapse into← +N/+N →end markers. - ✅ Mouse click-to-switch is wired: a left click anywhere inside a tab's column span focuses that tab. The hit-test (
src/line.rs) maps the clicked column to the tab drawn there and converts its 0-based position to the 1-based indexswitch_tab_toexpects, so it needs theChangeApplicationStatepermission (see the first-run note below). - ✅ The latest release ships a prebuilt
zellij-tabmap.wasmasset, so you can install the plugin without building it — see Use it in zellij.
The full design — architecture, rendering pipeline, degradation ladder, golden-repo mapping, risks, and test strategy — lives in docs/design.md.
rustup target add wasm32-wasip1 # one time
cargo build --release # .cargo/config.toml targets wasm32-wasip1
# artifact: target/wasm32-wasip1/release/zellij-tabmap.wasmThe robust way to run it is to install the prebuilt wasm to a local path and load it with file:: a local file loads instantly (no first-launch download wait), updates cleanly, and the permission grant persists across versions. (Prefer to try it before installing? A no-install option that loads straight from the release URL is in the collapsed note at the end of this section — handy for a first look, but it does not auto-update.)
1. Download the wasm to a local path. Any absolute path works; ~/.config/zellij/plugins/ matches zellij's own plugin convention:
mkdir -p ~/.config/zellij/plugins
curl -fL https://github.com/GeneralD/zellij-tabmap/releases/latest/download/zellij-tabmap.wasm -o ~/.config/zellij/plugins/zellij-tabmap.wasm-f makes curl fail on an HTTP error instead of silently saving the error page as your wasm. To pin a version, swap latest/download for download/vX.Y.Z.
2. Grant permissions once. The bar needs ReadApplicationState (pane/tab layout data) and ChangeApplicationState (click-to-switch), but a plugin loaded from default_tab_template gets no usable permission prompt (zellij#4982 tracks this dead-end for background plugins). Load it once in a regular pane, where the prompt can be focused and answered (the bar stays selectable until the permission flow resolves):
zellij plugin -- file:$HOME/.config/zellij/plugins/zellij-tabmap.wasmPress y to accept, then close the pane. The grant is keyed on the exact location string, and because the file path stays the same across versions this is a one-time step — a per-tag URL would need re-granting on every release. (As a fallback, add the entry by hand to permissions.kdl in zellij's cache directory — Linux: ~/.cache/zellij/permissions.kdl, macOS: ~/Library/Caches/org.Zellij-Contributors.Zellij/permissions.kdl — which is read once at server startup, so manual edits apply only in a fresh session.)
3. Wire it into your layout. In default_tab_template, give the tab-bar pane a height of 3 rows and point it at the file. KDL does not expand ~, so use the absolute path:
default_tab_template {
pane size=3 borderless=true { // 3 rows (the floor); raise to 4+ to enable perspective
plugin location="file:/Users/you/.config/zellij/plugins/zellij-tabmap.wasm" {
shortcut_prefix "⌘"
active_width "24"
align "center" // "center" slides to keep the active tab centered; "left" anchors the row (all-fit only)
reorder "false" // drag a tab to reorder; "true" also needs RunActionsAsUser
close_button "true" // stamps a clickable close button on each tab; "false" to hide
close_button_color "theme" // glyph color: "theme" (alert red) / "fg" (white) / "red" / "#rrggbb"
scroll "tab" // mouse wheel: "tab" (default) switch tabs / "pane" walk panes across tabs / "off"
scroll_cooldown_ms "40" // ms between wheel steps; higher = less sensitive, "0" = one step per event
tab_gap "2" // cleared columns between tab blocks; "0" packs them flush
gradient "sheen" // pane fill sweep: "sheen" (default) / "weave" (alternating rows) / "off" (flat)
gradient_shape "linear" // sweep geometry: "linear" (default) / "radial" (circular, from each block's center)
gradient_angle "0" // linear direction in degrees [0,360): 0 L→R (default), 90 top→bottom, 180 R→L, 270 bottom→top
gradient_radial "outward" // radial direction: "outward" (default, base at center) / "inward" (stop at center)
inactive_dim "true" // dim inactive tabs so the active one stands out; "false" to opt out
perspective "true" // lift the active tab with depth (needs pane size 4+); "false" to opt out
}
}
children
pane size=1 borderless=true { plugin location="status-bar" }
}Restart the session. Because the wasm is already local, the bar paints on the first tab immediately — there is no first-launch download wait (a remote-URL plugin is blank until its initial download lands, which can read as a broken bar).
4. Update later by re-running step 1 — the download overwrites the file in place. zellij reads a local file from disk at each session start, so a fresh session picks up the new build automatically; a remote URL, by contrast, is cached by zellij by URL and never re-fetched, so updates silently never arrive. An already-running session keeps serving the old build until you start a new one — there is no in-place reload for the tab bar (zellij action start-or-reload-plugin opens a stray plugin pane instead of refreshing a layout-loaded plugin, zellij#3927), so simply start a fresh session to pick up the update. (Updating a release-URL install instead? Re-warm the new URL once — see the blank bar after a bump note in the collapsed section below — or the bar can come up empty on that first post-upgrade session; file: never has this race.)
Contributors hacking on the plugin build from source and point file: at their own target/wasm32-wasip1/release/zellij-tabmap.wasm artifact instead of the downloaded wasm.
align— center vs left. When every tab fits,aligndecides how the row is anchored:center(default) re-centers the active block on each focus change, so the whole strip slides horizontally;leftpins the row's left edge at the start of the tab area (column 0, or just after any reserved prefix columns), removing that whole-strip slide. Noteleftdoes not freeze every tab's column — the active tab is still drawn wider than the inactives, so the tabs drawn after it shift right as focus crosses them; only the leftmost tab is truly fixed.aligngoverns the all-fit case only — when tabs overflow, the visible window always follows the active tab (with← +N/+N →markers) regardless ofalign, because the active tab must stay on screen. The default stayscenterso existing layouts render unchanged on update.
tab_gap— space between tabs. Leaves the given number of cleared columns between adjacent tab blocks so the boundary between screens reads clearly (default2). Set0to pack the blocks flush.
scroll— mouse wheel navigation. Selects what the wheel does over the bar (zellij scroll events carry no position, so the gesture is bar-wide, not tied to a specific tab).tab(default) switches tabs — scroll up = next, scroll down = previous, following zellij's stock tab-bar direction but wrapping at the ends (first ↔ last) instead of clamping.paneinstead walks the focused pane forward / backward in reading order (top→bottom, then left→right), crossing tab boundaries — stepping past a tab's last pane jumps to the next tab's first pane, and back — wrapping globally; the focus is absolute, so the bar's highlight follows correctly.offleaves the wheel inert. The wheel is rate-limited byscroll_cooldown_ms(below) so a stepless device does not race through tabs; the reported line count is otherwise ignored (each event is one notch). No extra permission is needed beyond the default set, so existing installs gain this on update without a re-grant.
scroll_cooldown_ms— wheel sensitivity. The cooldown window, in milliseconds, between wheel navigation steps (default40). A stepless pointing device — an Apple Magic Mouse, a trackpad — reports a single flick as a stream of scroll events, so the original one-event-per-step behavior raced through several tabs (or panes) at once. zellij hands the plugin onlyScrollUp/ScrollDownwith no device identity, so the two kinds of mouse can't be told apart by hardware — instead the wheel is rate-limited by timing: the first event navigates immediately and opens the window, and any event arriving withinscroll_cooldown_msof it is dropped. So a fast flick collapses to about one step per window, while a deliberate, well-spaced notch (its window long since elapsed) always steps at once — responsive on a notched wheel mouse, damped on a trackpad, with no added latency on that first event. Raise it to damp the wheel further; set0to disable the limiter and restore the original one-step-per-event feel. Ignored whenscroll "off". No extra permission needed.
gradient— per-pane fill sweep.sheen(default) sweeps each pane block's fill from its base color toward a luminance-shifted shade (lighter for dark themes, darker for light ones);weavealternates the sweep direction on each half-block pixel row for a woven texture. The focus ring, labels, and the⌘Nbadge stay solid on top, so readability is unchanged. Setofffor flat fills.
gradient_shape/gradient_angle/gradient_radial— sweep direction. These steer thesheen/weavesweep (they have no effect whengradient "off").gradient_shapeislinear(default, a straight sweep) orradial(a circular sweep from each pane block's center). Forlinear,gradient_anglesets the perceived on-screen direction in whole degrees over[0, 360):0left→right (the v0.5 look),90top→bottom,180right→left,270bottom→top, and any angle in between for a diagonal — out-of-range or non-integer values fall back to0. Forradial,gradient_radialchoosesoutward(default, base fill at the center easing to the stop at the edge) orinward(the reverse). Because each half-block pixel is already ≈ square, angles read as the true on-screen angle —45is a real 45°, not skewed by the terminal cell's 1:2 aspect.
inactive_dim— visual cue for the active tab. Whentrue(default), inactive tabs are dimmed toward the terminal background so the active tab stands out clearly: its pane fills stay vivid, its shortcut badge and focused pane label are drawn in white, and no focus ring appears on other tabs. Setfalseto disable the dimming and treat all tabs with equal intensity.
perspective— lift the active tab with depth. Whentrue(default) and the bar is at least 4 rows tall, every inactive tab recedes by one row — a half-row of terminal background inset at its top and bottom — while the active tab fills the full height, so the selected tab appears to float forward. The height comes from the layout'spane size=N, which the plugin can only read, not set: bump the tab-bar pane tosize=4(or more) to see the effect. Below 4 rows the option is a no-op (every tab fills the bar), andfalsealways renders every tab at full height. Pairs naturally withinactive_dim— color recede plus depth recede. The bar renders nothing if it is given fewer than 3 rows (the minimap needs that floor to stay legible).Enabling
reorderrequests a third permission,RunActionsAsUser(for theMoveTabByTabIdaction a tab drag performs). Granting is all-or-nothing for tab-template plugins, so when you setreorder "true"you must re-run step 2 (the grant prompt then lists all three permissions) and restart — otherwise the bar freezes with no prompt. Left at the default (false), the plugin requests only the two permissions above, so an existing install keeps working unchanged across updates.
close_button— click to close a tab. Whentrue(the default), a tab stamps a small close glyph (the Nerd Font close-circle, or a plain×where your terminal runs zellij's simplified UI without a Nerd Font) in its top-right corner; left-clicking exactly that cell closes the tab (viaclose_tab_with_index, which falls under the existingChangeApplicationStategrant — no re-grant needed). The glyph appears on the active tab — and, when theperspectivedepth cue is off, on every tab; under perspective the inactive tabs recede, where a corner glyph reads unbalanced, so they carry none. It only appears on blocks wide enough to draw a per-pane minimap, and never on the last remaining tab, so you can't close the bar out of existence. A click anywhere else on the block keeps its usual behavior (switch tab / focus the clicked pane), since the close target is that single cell, not the whole column. Setfalseto hide the glyph for keyboard-driven users.
close_button_color— close glyph color. The color of theclose_buttonglyph (defaulttheme).themeuses zellij's own alert red — your theme'sexit_code_errorcolor, falling back to a built-in red when the theme leaves it unset. Some themes derive that alert color from a near-black or near-whitered(e.g. sobrio'sred "#121212"), which renders thethemeglyph almost invisible; override it withfg(the active label's white — always legible on a colored tab),red(the plugin's built-in alert red, independent of the theme), or any#rrggbbhex (e.g."#ff5555"). Affects only the close glyph and is ignored whenclose_button "false".
Load straight from the release URL (quick try — does not auto-update)
For a first look without downloading anything, point the layout's plugin location directly at the release URL — zellij fetches and caches the wasm on first use:
plugin location="https://github.com/GeneralD/zellij-tabmap/releases/latest/download/zellij-tabmap.wasm"Grant this URL once before relying on the layout — the step-2 grant does not carry over, because zellij keys permissions on the exact location string and a default_tab_template plugin gets no usable prompt (zellij#4982). Load the URL in a regular pane (this also pre-warms the download):
zellij plugin -- https://github.com/GeneralD/zellij-tabmap/releases/latest/download/zellij-tabmap.wasmPress y and close the pane. Two caveats still make this unsuitable as a permanent install:
- Updates never arrive. zellij caches the downloaded wasm by URL and never re-fetches it, so the
latestURL keeps serving whatever version you first loaded; clearing zellij's cache is the only way to move forward. (A version-pinnedreleases/download/vX.Y.Z/URL avoids the stale-cache problem but then needs a fresh permission grant on every release.) - Blank bar on first launch — and once again after every version bump. An uncached URL downloads on its first session while the bar sits empty; worse, zellij broadcasts its one-shot initial tab/pane snapshot at server start, so if the download is still in flight the freshly-loaded plugin misses that snapshot and the bar stays blank until the next event (e.g. opening a second tab). A version-pinned
releases/download/vX.Y.Z/URL is uncached again after each bump, so this recurs once per upgrade. Two ways out: pre-warm the exact URL with thezellij plugin --grant step above — it downloads and caches the wasm as a side effect, so the later template-loaded bar starts from a warm cache and never blanks — or, after the download lands, just open a second tab (or restart the session) once and it is permanent. This cannot be fixed in the plugin: while the download is in flight the wasm is not running yet (zellij paints its own placeholder, not ours), and a plugin that loads late has no side-effect-free way to pull the full tab/pane geometry the minimap needs — zellij-tile 0.44 only delivers it through the next pushedTabUpdate/PaneUpdate. Thefile:install above sidesteps the whole race: a local path has no download window, so the bar paints on the first tab even right after an update.
If a fetch ever returns a non-wasm body (e.g. a 404 page when the release asset is not published yet), zellij caches that error text as the wasm, permanently — the log then shows magic header not detected. Recover by deleting both cache traces for that URL (a hashed blob directly under zellij's cache root, and the https:/github.com/GeneralD/zellij-tabmap/releases/… directory tree beneath it) and starting a fresh session.
cargo test --lib --target "$(rustc -vV | sed -n 's/host: //p')" # native unit tests
cargo clippy --target wasm32-wasip1 --all-features --lib # lint (CI denies warnings)
cargo build --release --target wasm32-wasip1 # the loadable wasmCI runs the same three on every push; tagging vX.Y.Z builds the wasm, generates a changelog with git-cliff, and attaches the artifact to a GitHub Release.
Structured after KiryuuLight/zellij-attention, used as a golden-repository reference for the Rust/WASM zellij-plugin layout (thin register_plugin! bin + native-testable lib + FFI-stubbed tests + CI/release workflows).

