Zellij
Zellij is a terminal multiplexer: it enables a number of terminals (or windows), each running a separate program, to be created, accessed, and controlled from a single screen. Zellij may be detached from a screen and continue running in the background, then later reattached.
Installation
Shell
To temporarily use zellij in a shell environment without modifying your system configuration, you can run:
$ nix-shell -p zellij
This makes the zellij available in your current shell. You can then launch zellij by typing zellij.
Configuration
zellij can be configured using Home Manager:
As an example:
programs.zellij = {
enable = true;
# automatically start zellij when opening a new terminal
enableBashIntegration = true;
enableFishIntegration = true;
enableZshIntegration = true;
# zellij uses KDL as its configuration language,
# see also https://zellij.dev/documentation/configuration.html
settings = {
theme = "custom";
themes.custom.fg = "#ffffff";
keybinds._props.clear-defaults = true;
keybinds.pane._children = [
};
# extra settings as a raw string
# they are appended to the end of the file
extraConfig = ''
keybinds {
pane {
bind "h" "Left" { MoveFocus "Left"; }
bind "l" "Right" { MoveFocus "Right"; }
bind "j" "Down" { MoveFocus "Down"; }
bind "k" "Up" { MoveFocus "Up"; }
}
}
'';
};
Using Plugins
Zellij offers a WebAssembly / WASI plugin system, allowing plugin developers to develop plugins in many different languages.
Plugins can be configured using programs.zellij.plugins. In nixpkgs, they can be found in the zellijPlugins namespace.
programs.zellij = {
enable = true;
plugins = with zellijPlugins; [ vim-zellij-navigator ];
settings ={
plugins = {
vim-zellij-navigator = {
vim_commands = "v|nvim|vim|testNvim";
};
};
};
extraConfig = ''
keybinds {
shared_among "normal" "locked" "scroll" {
bind "Ctrl j" {
MessagePlugin "vim-zellij-navigator" {
name "move_focus";
payload "down";
};
}
}
}
'';
};
This will generate ~/.config/zellij/config.kdl, with something like this:
load_plugins {
vim-zellij-navigator
}
plugins {
vim-zellij-navigator location="file:/home/perchun/.config/zellij/plugins/vim-zellij-navigator.wasm" {
vim_commands "v|nvim|vim|testNvim"
}
}
// extraConfig
keybinds {
shared_among "normal" "locked" "scroll" {
bind "Ctrl j" {
MessagePlugin "vim-zellij-navigator" {
name "move_focus";
payload "down";
};
}
}
}Packaging Plugins
All plugins are packaged in the pkgs/by-name/ze/zellij/plugins/ folder. Currently, only Rust plugins are packaged, but if you figure out how to package other languages, contributions are welcome.
To add a new plugin, you can just use nix-init like this:
$ nix-init pkgs/by-name/ze/zellij/plugins/rust/my-plugin-name.nix
Enter url
❯ https://github.com/hello/my-plugin-name
Enter tag or revision (defaults to v0.1.0)
❯ v0.1.0
Enter version
❯ 0.1.0
Enter pname
❯ my-plugin-name
How should this package be built?
❯ buildRustPackage - cargoHashIf the repository name starts with zellij, you should omit it in the actual package name. zellijPlugins.autolock looks better than zellijPlugins.zellij-autolock.
After you have generated the initial definition file, remove all dependencies that were automatically set by nix-init. They are not needed when compiling a WASM binary. If the plugin depends on something at runtime, read the next section.
Specifying runtime dependencies
Runtime dependencies are packages that will be used by the plugin inside a Zellij session. Those are specified in passthru.runtimeDeps attribute from pkgsBuildBuild attrset.
Since we compile all plugins for WASI, everything that the plugin gets as derivation arguments is also compiled for WASI. Some packages (for example coreutils) might not be available on WASI, so we need to use pkgsBuildBuild attrs set (which points to the user's system).
# assume we build the plugin on an x86_64-linux machine
{
lib,
fetchFromGitHub,
rustPlatform,
# these will be compiled for WASI, not x86_64-linux!
just,
bacon,
# but pkgsBuildBuild points to x86_64-linux
pkgsBuildBuild,
}:
rustPlatform.buildRustPackage (finalAttrs: {
pname = "jbz";
version = "0.39.0";
src = fetchFromGitHub {
owner = "nim65s";
repo = "jbz";
tag = "v${finalAttrs.version}";
hash = "sha256-3n3Bv3YDb1+MYJTTAmMkIgGY7kX9IVUoDNV4c/n0Ydo=";
};
cargoHash = "sha256-U+P2LlhmXwaZy2a2eigrg545HTuV1T01jZfUOEUQ5+w=";
# specify runtime dependencies here
passthru.runtimeDeps = with pkgsBuildBuild; [
bacon
just
];
meta = {
description = "Display your Just commands wrapped in Bacon";
homepage = "https://github.com/nim65s/jbz";
changelog = "https://github.com/nim65s/jbz/releases/tag/${finalAttrs.src.tag}";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ you ];
};
})
If you are wondering why pkgsBuildBuild is named like that, refer to the docs on cross-compilation.
Overriding plugins
Plugins use a wrapper for additional post-processing, like stripping unnecessary information from pluginDrv.name. So naїve override won't work, as it overrides the wrapper and not the plugin itself. Instead, you should override the unwrapped version and then pass that to the wrapper:
final: prev: {
zellijPlugins = prev.zellijPlugins // {
# wrap our overrode package
zjstatus = prev.zellijPlugins.wrapper "zjstatus" (
# override unwrapped package
prev.zellijPlugins.zjstatus.unwrapped.overrideAttrs (
oldAttrs:
let
src = final.fetchFromGitHub {
owner = "dj95";
repo = "zjstatus";
rev = "17609e498f88e674b846b844b48ad1dc545fddcf";
hash = "sha256-/LsFGF0BAYdiVVQZ/8vjo6IXuSsYS+ueIWHN47NHEAc=";
};
in
{
version = "0-unstable-2026-06-03";
inherit src;
# override cargoHash if it is needed
cargoDeps = final.rustPlatform.fetchCargoVendor {
inherit src;
hash = "sha256-+UYNIPrRYUYwxHk227PDo5vdAqho8SMm80o7UqSdZ3g=";
};
}
)
);
};
}