This is the documentation for the Zellij terminal workspace.


Installation options for Zellij are currently limited as the app is undergoing very heavy development.

Currently Binaries are produced for both Linux and MacOS.

However it is available in some public repositories.

Rust - Cargo

For instructions on how to install Cargo see here:

Once installed run:

cargo install zellij

Binary Download

Binaries are made available each release for the Linux and MacOS operating systems.

It is possible to download the binaries for these here:

Once downloaded, untar the file:

tar -xvf zellij*.tar.gz

check for the execution bit:

chmod +x zellij

and then execute Zellij:


Include the directory Zellij is in, in your PATH Variable if you wish to be able to execute it anywhere.


move Zellij to a directory already in your PATH Variable.

Compiling Zellij From Source

"Under Construction"


By default Zellij will look for config.yaml in the config directory.

The default config directory order is as follows:

  • --config-dir flag

  • ZELLIJ_CONFIG_DIR env variable

  • $HOME/.config/zellij

  • default location

    • Linux: /home/alice/.config/zellij
    • Mac: /Users/Alice/Library/Application Support/org.Zellij-Contributors.Zellij
  • system location (/etc/zellij)

In order to pass a config file directly to zellij:

zellij --config [FILE]

or use the ZELLIJ_CONFIG_FILE environment variable.

To start without loading configuration from default directories:

zellij options --clean

To show the current default configuration:

zellij setup --dump-config


You can make use of these options either by invoking zellij with zellij options [OPTION] or binding them in the configuration file.

Eg. zellij options --simplified-ui is equivalent to simplified_ui: true in the config file.

default-modedefault_modenormalThe first mode on startup.
simplified-uisimplified_uifalseRequest the Plugins to use a more compatible ui.
themethemedefaultSwitch to a theme configured under the themes section.

Configuring Keybindings

Zellij comes with a default set of keybindings that try to fit as many different users and use cases while trying to maximize comfort for everyone.

It is possible to add to these defaults or even override them with an external configuration. For more information about the file, see Configuration.

The structure of the keybinds section of the file is as follows:

        - action: []
          key: []

Under the main keybinds section one can list the new bindings they'd like to add grouped under the different Modes (normal in this example). The action is a sequence of one or more instructions sent to Zellij through this keybinding. The key is a list of one or more keys, any one of them alone would trigger the sequence of actions.

For example:

        - action: [ NewTab, GoToTab: 1,]
          key: [ Char: 'c',]

Will create a new tab and then switch to tab number 1 on pressing the c key. Whereas:

        - action: [ NewTab,]
          key: [ Char: 'c', Char: 'd',]

Will create a new tab on pressing either the c or the d key.

To unbind the default Keybindings

The default keybinds can be unbound either for a specific mode, or for every mode. It supports either a list of keybinds, or a bool indicating that every keybind should be unbound:

    unbind: true

Will unbind every default binding.

    unbind: [ Ctrl: 'p']

Will unbind every default ^P binding for each mode.

        - unbind: true

Will unbind every default keybind for the normal mode.

        - unbind: [ Alt: 'n', Ctrl: 'g']

Will unbind every default keybind for n and ^g for the normal mode.


This configuration can be used to configure Zellij's default keybindings: default.yaml


The Zellij keybindings are grouped into different modes, which are a logical separation meant to reduce the mental overhead and allow to duplicate shortcuts in different situations.


This is the default mode Zellij starts with. By default it provides the ability to switch to different modes, as well as some quick navigation shortcuts.


This mode "locks" the interface, disabling all keybindings except one that would switch to "normal" mode (ctrl-g by default). It is intended to give users a workaround in case one of the default keybindings overrides something they use in their terminal. (eg. ctrl-r for reverse history search in bash).


This mode includes instructions that manipulate the different panes. Eg. adding new panes, closing panes and moving the focused pane.


This mode includes instructions that manipulate the different tabs. Eg. adding new tabs, closing tabs and moving the active tab.


This mode allows the resizing of the focused pane.


This mode allows scrolling up/down within the focused pane.


This mode allows detaching from a session.


These are the possible keys and key combinations one can set in the Keybindings configuration. For more information, please see:

Char: <character>

A single character with no modifier, eg. Char: f

Alt: <character>

A single character preceded by the Alt modifier, eg. Alt: f.

Ctrl: <character>

A single character preceded by the Ctrl modifier, eg. Ctrl: f.

F: <1-12>

One of the F characters (usually at the top of the keyboard). eg. F: 11


The Backspace key.

Left / Right / Up / Down

The arrow keys on the keyboard.


The home key.


The End key.

PageUp / PageDown

The PageUp or PageDown keys.


The backward Tab key.


The delete key.


The insert key.


The Esc key.


These are the actions that can be assigned to key sequences when configuring keybindings.


Quit Zellij.

SwitchToMode: <InputMode>

Switch to the specified input mode. The mode should be capitalized, eg. SwitchToMode: Normal.

Note that there's a "hidden" mode called RenameTab which can be used in order to trigger the renaming of a tab.

Resize: <Direction>

Resize focused pane in specified direction. Direction should be one of Left, Right, Up or Down.

eg. Resize: Down


Switch focus to next pane to the right or below if on screen edge.


Switch focus to next pane to the left or above if on screen edge.


Switch focus to pane with the next ID (this is mostly left around for legacy support, FocusNextPane or FocusPreviousPane should be preferred).

MoveFocus: <Direction>

Move focus to the pane with the greatest overlap with the current pane in the specified direction. Direction should be one of Left, Right, Up or Down.

eg. MoveFocus: Left

Scroll up 1 line inside the focused pane.

Scroll down 1 line inside the focused pane.


Toggle between fullscreen focus pane and normal layout.

NewPane: <Direction>

Open a new pane in the specified direction (relative to focus). If no direction is specified, will try to use the biggest available space. Direction should be one of Left, Right, Up or Down. Specifying no direction should be done by introducing a space character (this is a bug and should be fixed).

eg. NewPane: Left or NewPane:


Close the focused pane.


Create a new tab.


Go to the next tab.


Go to the previous tab.


Close the current tab.

GoToTab: <index>

Go to the tab of the specified index.


You can specify a color theme, that will be picked up by zellij in the following way:

    fg: [0,0,0]
    bg: [0,0,0]
    black: [0,0,0]
    red: [0,0,0]
    green: [0,0,0]
    yellow: [0,0,0]
    blue: [0,0,0]
    magenta: [0,0,0]
    cyan: [0,0,0]
    white: [0,0,0]
    orange: [0,0,0]

for truecolor, or:

    fg: 0
    bg: 0
    black: 0
    red: 0
    green: 0
    yellow: 0
    blue: 0
    magenta: 0
    cyan: 0
    white: 0
    orange: 0

for 256 color.

If the theme is called default, then zellij will pick it on startup. To specify a different theme, run zellij with:

zellij options --theme [NAME]

or put the name in the configuration file with theme: [NAME].


Layouts are yaml configuration files which Zellij can load on startup. These files can describe a layout of terminal panes and plugins that Zellij will create when it loads. To load a layout with Zellij:

zellij --layout /path/to/your/layout_file.yaml


This file:

direction: Vertical
  - direction: Horizontal
      Percent: 50
      - direction: Vertical
          Percent: 50
      - direction: Vertical
          Percent: 50
  - direction: Horizontal
      Percent: 50

Will instruct Zellij to create this layout:

│     │     │
├─────┤     │
│     │     │

Creating a layout file

A layout file is a nested tree structure. Each node describes either a pane, or a space in which its parts (children) will be created.

Each node has the following fields:

direction: Horizontal / Vertical

If the node has children, they will be created as splits in this direction.

split_size: Percent: <1-100> / Fixed: <lines/columns>

This indicates either a percentage of the node's parent's space or a fixed size of columns/rows from its parent's space.

plugin: /path/to/plugin.wasm

This is an optional path to a compiled Zellij plugin. If indicated, instead of loading a terminal, this plugin will be loaded. For more information, please see the plugin documentation of this guide.

Further examples

Please see the default layouts that come with Zellij: layouts


One feature that makes Zellij unique is its WebAssembly plugin system. This allows plugin developers to write their plugin in any language that can run on WASI! Rust offers first-class support for WASI, but other languages like C/C++, AssemblyScript, even Python should be supported.

Disclaimer: The API for plugins is very much a work in progress. Don't be shy to request new features on our tracking issue!

Developing a Plugin

Currently we have a complete guide for developing plugins in Rust and plan to add more guides for other languages in the future. If you are feeling particularly brave, you can try to write plugins in another language today! The Other Languages section will get you started.

Writing a Plugin in Rust

Writing a Zellij plugin in Rust is incredibly easy thanks to Rust's first-class support for WebAssembly and the simple zellij-tile scaffolding library. This guide will walk through implementing the rust-plugin-example, a simple event logger that records mode-changes within Zellij.

Getting Started

Installing Rust & Zellij

First things first, to develop a plugin in Rust, you'll need Rust installed! The easiest way to do this is by using rustup.

Once you have Rust and Cargo installed, getting the latest version of Zellij is as easy as running:

cargo install zellij

You'll also want to add the installed binary to your path!

Cloning The Template Repository

To streamline the development experience, we provide a template repository that contains everything you need to get started quickly!

You can use a tool called cargo-generate to fill in a couple of the gaps automatically:

# First install `cargo-generate`
cargo install cargo-generate
# Then clone the rust-plugin template
cargo generate --git --name mode-logger
cd mode-logger

The Basic Structure of a Rust Plugin

After cloning the template repository, you should have a directory that looks a little bit like this:

├── .cargo
│   └── config.toml
├── Cargo.toml
├── plugin.yaml
└── src


target = "wasm32-wasi"

This file specifies a default target for our project. In this case, the correct WASI target is wasm32-wasi.


name = "mode-logger"
version = "0.1.0"
authors = ["Brooks J Rady <>"]
edition = "2018"

zellij-tile = "1.0.0"

This is a quite standard package file that cargo-generate has partially filled in for us. Note the dependency on zellij-tile which provides some helpful functionality for avoiding boilerplate and writing unsafe code.


direction: Horizontal
  - direction: Vertical
      Fixed: 1
    plugin: tab-bar
  - direction: Vertical
    plugin: target/wasm32-wasi/debug/mode-logger.wasm
  - direction: Vertical
      Fixed: 2
    plugin: status-bar

This is a Zellij Layout that loads a mostly default instance of Zellij, but with the middle terminal pane replaced by the plugin being developed. The plugin: target/wasm32-wasi/debug/mode-logger.wasm line should point Zellij to the development version of our plugin.

There will likely be a better way of loading plugins in the future, but custom Layouts are currently the only way to do so.


use zellij_tile::prelude::*;

struct State;


impl ZellijPlugin for State {
    fn load(&mut self) {}

    fn update(&mut self, event: Event) {}

    fn render(&mut self, rows: usize, cols: usize) {}

When using the zellij-tile library, plugins are written as Structs that implement the ZellijPlugin trait. The magic line here is register_plugin!(State), which wraps up the State struct in a way that neatly exposes its ZellijPlugin implementation for Zellij to find.

Note that load(), update(), and render() have default implementations, so you only need to define the callbacks used by your plugin.

Hello, Zellij!

Let's tweak our a little to say hello!

use zellij_tile::prelude::*;

struct State;


impl ZellijPlugin for State {
    fn load(&mut self) {}

    fn update(&mut self, event: Event) {}

    fn render(&mut self, rows: usize, cols: usize) {
        println!("Hello, Zellij!");

It really is as simple as that! Anything printed to stdout by the render() method will be automatically drawn to the screen in the pane where the plugin is active.

Let's build our plugin and test things out:

cargo build
zellij --layout-path plugin.yaml

Our Plugin

Implementing the Event Logger

That was pretty easy, so let's try to do something a bit more interesting! Let's subscribe to some Events by adding the following code to load():

fn load(&mut self) {

Code in load() is called once the first time your plugin is loaded. Aside from that, it's nothing special. Anything that you can do in the load() method should be possible from within the update() and render() methods as well.

The subscribe() function is part of zellij-tile::prelude and sends a message to Zellij asking to be notified when certain Events occur. In this case, we're subscribing to ModeUpdate events. The documentation for Event tells us that a ModeUpdate contains the ModeInfo struct, which stores the current mode as well as some additional information.

To actually handle these events, we'll need to add some code to our update() method:

fn update(&mut self, event: Event) {
    if let Event::ModeUpdate(mode_info) = event {

Here we are checking for ModeUpdates and destructuring them to get the current mode. Currently, the dbg!() macro is being used to dump this information to stderr. If we want to actually see this debug info, we'll need to run our plugin slightly differently:

cargo build
# The 2> redirects stderr to dbg.log
zellij -l plugin.yaml 2> dbg.log

Do some faffing about in Zellij, changing modes a couple of times, then take a look at dbg.log:

[src/] mode_info.mode = Normal
[src/] mode_info.mode = Pane
[src/] mode_info.mode = Tab
[src/] mode_info.mode = Resize
[src/] mode_info.mode = Scroll
[src/] mode_info.mode = Locked
[src/] mode_info.mode = Normal

Excellent! It looks like our plugin is receiving mode updates! If you'd like to see these sorts of logs live, try opening a second terminal and running tail -f dbg.log.

The next thing to do is properly store a log of events and print them to the screen. Let's start by tweaking our State struct:

use std::collections::VecDeque;

struct State {
    log: VecDeque<String>,

We've gone with a VecDeque so that we can efficiently push Strings to the front of log and so we can show off how easy it is to use standard library components from within a plugin.

Let's change the update() method again to grow our log:

fn update(&mut self, event: Event) {
    if let Event::ModeUpdate(mode_info) = event {
        let mode = format!("{:?}", mode_info.mode);

Recall that the {:?} format specifier simply debug-prints a value and that log.push_front(mode) adds a mode String to the front of our log messages.

Finally, let's update render() to print out all of our log messages:

fn render(&mut self, rows: usize, cols: usize) {
    for mode in &self.log {
        println!("Mode: {}", mode);

Let's give things a run with cargo build && zellij -l plugin.yaml and test it out!

Our Plugin

Excellent! You should notice that, as you cycle through different modes in Zellij, that those updates are being logged on-screen.

This is a good start, but no logger is complete without storing timestamps! Let's import the chrono crate for working with time. First we'll need to add it to our Cargo.toml:

name = "mode-logger"
version = "0.1.0"
authors = ["Brooks J Rady <>"]
edition = "2018"

zellij-tile = "1.0.0"
chrono = "0.4"

Quite a few Rust libraries can compile to WebAssembly without any issue – this is one of them! Let's import it and update State to store timestamps:

use chrono::{DateTime,Local};

struct State {
    log: VecDeque<(String, DateTime<Local>)>,

Next we'll need to actually store these timestamps in update():

fn update(&mut self, event: Event) {
    if let Event::ModeUpdate(mode_info) = event {
        let mode = format!("{:?}", mode_info.mode);
        // Local::now() gets the current time and date
        self.log.push_front((mode, Local::now()));

Finally, we can render() the timestamps to the screen:

fn render(&mut self, rows: usize, cols: usize) {
    for (mode, time) in &self.log {
        println!("Mode: {} ({})", mode, time.format("%T"));

The different options for time.format() can be found in the chrono::format::strftime module; %T shows the time in HH:MM:SS format.

Finally, let's test this out!

Our Plugin

It looks like getting the time is working perfectly! Unfortunately, because our different mode names are different lengths, it looks a bit messy at the moment. Luckily, every time that render() is called, it passes the size of plugin pane. Let's use the cols value to right-align the timestamp:

fn render(&mut self, _rows: usize, cols: usize) {
    for (mode, time) in &self.log {
        let mode = format!("Mode: {}", mode);
        let time = time.format("%T").to_string();
        let padding = " ".repeat(cols - mode.len() - time.len());
        println!("{}{}{}", mode, padding, time);

By separately storing the left-aligned mode String and the right-aligned timestamp, we can calculate (using cols) exactly how much padding we need to fill the screen. Once we've done that, it's just a matter of printing the left half, the padding, then the right half.

If we run this code, we now get the much nicer:

Our Plugin

Try resizing your terminal window or the plugin pane and watch how things stay properly justified!

As a final step, let's add a couple of commands that allow the user to clear their history or save their log to a file. To do this, we'll need our plugin to receive KeyPress events:

fn load(&mut self) {
    subscribe(&[EventType::ModeUpdate, EventType::KeyPress]);

Now we can expand update() to handle ModeUpdate and KeyPress events. Let's make Ctrl-L the command for clearing the logs:

fn update(&mut self, event: Event) {
    match event {
        Event::ModeUpdate(mode_info) => {
            let mode = format!("{:?}", mode_info.mode);
            self.log.push_front((mode, Local::now()));
        Event::KeyPress(Key::Ctrl('l')) => self.log.clear(),
        _ => (),

When matching against KeyPress events, you might find it helpful to explore the zellij_tile::Key enum.

Also note that we need the catch-all case _ => () because, even though we've only subscribed to the ModeUpdate and KeyPress events, the Rust compiler on its own can't guarantee that only those events will be passed to update().

Let's try running this, changing modes a couple of times, then pressing Ctrl-L:

Our Plugin

Incredibly blank! Nicely done!

As one final feature, we'll dump a log of the captured events to mode-log.txt when Ctrl-W is pressed:

use std::{fs::File, io::Write};

// ... snip ...

fn update(&mut self, event: Event) {
    match event {
        Event::ModeUpdate(mode_info) => {
            let mode = format!("{:?}", mode_info.mode);
            self.log.push_front((mode, Local::now()));
        Event::KeyPress(Key::Ctrl('l')) => self.log.clear(),
        Event::KeyPress(Key::Ctrl('w')) => {
            if let Ok(mut f) = File::create("mode-log.txt") {
                for (mode, time) in self.log.iter().rev() {
                    writeln!(f, "{}: Entered {} Mode", time.format("%c"), mode).unwrap();
        _ => (),

A couple of new things here, but all of them are vanilla Rust – nothing changes when writing a plugin! File::create("mode-log.txt") is just normal code for creating a file named mode-log.txt in the current directory. After the file is created, we're again looping through all of the log events and writing them out (but this time to a file!). The format of the log file is also a bit different from the log we show on-screen. First of all, we're writing things to the log file in chronological order (that's why we're reversing our log with self.log.iter().rev()), and we've also changed to a more verbose timestamp. The %c should look something like this: Tue Apr 20 10:21:02 2021.

Let's run Zellij again, change some modes, then press Ctrl-W:

Our Plugin

We can then look at mode-log.txt, which should look something like this:

Tue Apr 20 10:20:40 2021: Entered Normal Mode
Tue Apr 20 10:20:46 2021: Entered Pane Mode
Tue Apr 20 10:20:50 2021: Entered Tab Mode
Tue Apr 20 10:20:53 2021: Entered Resize Mode
Tue Apr 20 10:20:57 2021: Entered Scroll Mode
Tue Apr 20 10:21:00 2021: Entered Locked Mode
Tue Apr 20 10:21:02 2021: Entered Normal Mode

And that's all, folks! The full code for this example can be found in the rust-plugin-example repository.

Further Steps

There are a lot of ways that this plugin could be improved! Here are a couple of the things that you might want to try implementing if you're looking for the extra practice:

  • Support for scrolling :: you'll need to use rows and save a cursor position in State
  • Make it pretty :: our default plugins use the colored crate for fancy formatting
  • Make it more stable :: if you make the logger pane too small, the plugin will crash!
  • Add a help bar :: there is currently no UI telling the user about the clear and save features

Have fun, and don't hesitate to get in touch if you find any bugs or would like some guidance :)

Writing a Plugin in Another Language

This page is very much a work in progress! If you're familiar with WASM and Rust, then understanding the zellij-tile library is a great place to start.

In short, Zellij expects your WASI module to export three functions:

  • main() :: called once on plugin load
  • update() :: called after event subscriptions are triggered
  • render(i32, i32) :: called when the plugin needs to be rendered

The render(i32, i32) function is passed the size of the plugin pane, first the rows, then the columns – e.g. render(rows, cols).

Complex types are sent over stdin as JSON objects. Before update() is called, Zellij writes an event to the plugin's stdin that can be read back in the update() function.

Host functions, for communicating with Zellij, are best documented by the extern section of zellij-tile::shim and are found in a WASM module named zellij.

Help Add More Language Guides! If you'd like to contribute, either drop us a line on Discord or open a PR improving these docs!


Operating System Compatibility:

Currently tested on these systems, though most recent Linux kernels, and MacOS versions should run it with minimal issue. Please report issues on different Operating Systems here.


Ubuntu 21.04


10.15 Catalina

Known Issues

The status bar fonts don't render correctly:

This most likely is caused by a missing character in the font.

Fonts from nerdfonts can fix this problem.

Some Options:

Package ManagerName