Terminals are crucial to my development workflow, locally and remotely. zellij has replaced tmux for my tiling and session management needs.

Terminals and tiles

Why do terminals need tiles? A zellij is a type of mosaic tile common in Islamic architecture. You might be wondering how this is useful in the context of a terminal. And no, I’m unfortunately not going to reveal how Mosques are powered by Linux. Terminal multiplexers such as zellij, tmux or screen manage multiple instances of your shell in one terminal. Although modern GUI terminals tend to have tabs and window management, having multiple tabs, tiles and even floating windows within your terminal means that it can work remotely and separately from a graphical session. You can even use one within an IDE.

How do I get a hold of those mosaics?

You can easily install zellij via your distro’s package manager and it’s available on Tumbleweed, Arch, Homebrew and Termux just to also give you an idea of how portable it is. Of course if you want to be even more portable you can install it straight from GitHub:

sys="unknown-linux-musl"; arch=$(uname -m); [ "$arch" == "arm64" ] && arch="aarch64"; curl --location https://github.com/zellij-org/zellij/releases/latest/download/zellij-$arch-$sys.tar.gz | tar -C ~/.local/bin -xz

Note: You can also install zellij via cargo install zellij --locked if you have a Rust development setup anyway 🦀️. Of course there’s always a chance your toolchain is out of sync or you run into bugs, so keep that in mind.

To start using zellij all you really need is one command from a shell of your choice:

zellij -s $HOSTNAME attach

If you use the hostname as the session name like me it doubles as an indicator for what host you’re on. Generally any name will do, and if you didn’t specify one a session name will be generated. With a name provided zellij will always create the session if it’s not running yet meaning you can use the same command at any point in time.

Let’s create a new tab. Guess how we will do that? Read the hotkey from the guide on-screen. Wanna rename that tab? Follow the hotkey. And spoiler alert, we’ll be looking into the configuration later and the guide knows what keys you have configured. It would even work if you needed someone else to use your session.

Note: You can incidentally attach from separate terminals and look at different tabs independently. Try it by attaching again from any other terminal, a tty or connecting from another machine.

Whatever floats your panes

You can toggle between regular panes and floating ones via Ctrl + p + w. For your convenience zellij will create a new floating pane if you didn’t already have one open. If your shell sets a window title e.g. PS1=$PS1'\[\033]0;\W\a\]' in bash or set titlestring=%t neovim you’ll see the title in the pane as well.

Moving existing panes is as easy as Ctrl + p + e which is short for embed or escape. If your pane is floating it will attach itself within the tab. Conversely a fixed pane will start floating with the same shortcut so you don’t need to settle on using it one way or the other.

You can also search all panes. And the statusbar will guide you by showing the keybindings while you use it. By default Ctrl + s enters search mode, Esc leaves it. Give it a try. It’s rather handy at times.

What was that about customized key bindings?

So far we’ve looked at what you can do out of the box, and that’s already quite a lot. Let’s see what more we can do by creating a configuration file:

zellij setup --dump-config > ~/.config/zellij/config.kdl

If you’re not familiar with it, the language used here is KDL or what’s called the cuddly document language, hence why it’s pronounced “cuddle”. It is similar to YAML (and that’s actually what used to be the format in earlier versions) but it uses curly braces and semicolons. Most likely you’ll be able to pick it up quickly following the examples.

You’re free to use the default config as a starting point or start from scratch, and add options on an as-needed basis as you follow along. Both is just fine.

Obviously the first thing you want to check out is theming. Nobody should be forced to get used to a new tool while being uncomfortable because of the color choices the developers made 😎️. That’s done by adding at least one theme to your config:

themes {
  gruvbox-dark {
        fg 213 196 161
        bg 40 40 40
        black 60 56 54
        red 204 36 29
        green 152 151 26
        yellow 215 153 33
        blue 69 133 136
        magenta 177 98 134
        cyan 104 157 106
        white 251 241 199
        orange 214 93 14
    }
}
theme "gruvbox-dark"

As you can see colors are defined in a themes section under the name of the theme, and defined in terms of RGB values. If you don’t feel like picking your own colors from scratch you can simply pick your favorite theme from the theme gallery. Finally the name of the theme is set by its name.

Lock, lock. Who’s there?

When you’re working with non-trivial apps within your multiplexer you’ll quickly notice that key bindings can overlap. Fortunately there’s a straightforward measure for this. Lock all key bindings with Ctrl + g! Should you actually need that shortcut you can also change it by adding something like this to your config file:

keybinds {
    unbind "Ctrl g" // optionally remove the default hotkey
    shared_except "normal" {
        bind "F12" { SwitchToMode "normal"; }
    }
    shared_except "locked" {
        bind "F12" { SwitchToMode "locked"; }
    }
}

From now on F12 is how you lock. You’ll probably guess that the mode switch actions change between normal and locked and only respond while you’re in the respective other mode. You can find a list of modes and a list of actions in the respective official docs. Like with YAML it’s all declarative even if it’s written in a functional style.

Simply detach and go back in to see the change reflected. The statusbar will indeed reflect the new hotkey if you also remove the default, although you can keep both if that’s what you prefer. If you later realize there’s a scenario you weren’t thinking of you don’t need to discard your entire session.

Can I haz actions on the command-line?

If by some chance you paint yourself into a corner e.g. by locking the session and not having a working key binding to unlock it again, it might be time to try out the great command-line interface of zellij:

zellij --session $HOSTNAME action switch-mode normal

Actions are named a little differently than in the config. You can check the handy reference for CLI actions in the official docs, though, when something isn’t obvious or simply consult zellij action --help on the command-line.

More keys than a key lime pie

Note: I will be repeating the keybinds section in my examples for clarity but you’ll only want one in your config.

To accommodate my muscle memory from having used tmux for a long time I’m taking advantage of the built-in compatibility layer. If you’ve used it before you will find that you can use the same key bindings to the same effect, which is amazing when your hands are moving faster than the time it would take for you to even look at a keyboard. However I’m also used to a few tweaks that aren’t so easy to unlearn:

keybinds {
    tmux {
        bind "Ctrl a" { Write 2; SwitchToMode "Normal"; }
    }
    shared_except "locked" {
        bind "Alt t" { NewTab; }
    }
    shared_except "normal" "locked" {
        bind "Ctrl c" { SwitchToMode "Normal"; }
    }
    shared_except "tmux" "locked" {
        bind "Ctrl a" { SwitchToMode "Tmux"; }
        bind "Ctrl b" { SwitchToMode "Tmux"; }
    }
}

What we’re doing here is the following:

  • Ctrl + a becomes an alias to Ctrl + b. So something like Ctrl + a + c opens a new tab, analoguous to Ctrl + t + n with the default zellij key bindings.
  • Alt + t also opens a new tab. Don’t ask.
  • Ctrl + c acts like Escape with the default bindings, going back to normal mode. And this is arguably easier to reach and also a binding I use with helix.

You can go beyond just muscle memory of course and freely customize and even completely replace default key bindings. In fact you can run entire commands as key bindings:

keybinds {
    bind "a" {
        Run "cat" "/etc/os-release" {
            cwd "/tmp"
            direction "Down"
        }
    }
}

The sky is the limit as they say. Well, that and you can’t bind Ctrl to special keys. So something like Ctrl Left is not supported and you need to rely on Alt instead.

Tweaking the GUI

I’ll explain the plugins later, for now you might just like to define a few:

plugins {
    tab-bar { path "tab-bar"; }
    status-bar { path "status-bar"; }
    strider { path "strider"; }
    compact-bar { path "compact-bar"; }
}

on_force_close "quit"

simplified_ui true

pane_frames false

scroll_buffer_size 100000

mirror_session true

To be sure the config is sensible, you can also run a handy command:

zellij setup --check

You can find all of the available options in the official docs.

Layouts… what are those anyway?

Let’s create a file ~/.config/zellij/layouts/default.kdl.

I said before you can start from scratch just fine. If you were to take me on my word by creating a layout without any default components you’d find that the GUI seemingly disappears. That’s because the plugins need to be part of the layout! So let’s see what I mean by that:

layout {
    tab name="btm" {
        pane command="btm" close_on_exit=true
    }
    tab name="distrobox" {
        pane {
            command "distrobox"
            args "enter"
            close_on_exit true
        }
    }
    tab {
        pane
    }
}

What I’m adding here is three tabs, one for btm, one for zellij and one empty tab. You’ll however be missing out on hotkey display, mode indicators, the session name and knowing what tabs are open. So here’s how to get them back:

    default_tab_template {
        pane size=2 borderless=true {
            plugin location="zellij:status-bar"
        }
        pane split_direction="Vertical" {
            pane size="15%" { plugin location="zellij:strider" }
            children
        }
        pane size=1 borderless=true {
            plugin location="zellij:tab-bar"
            // plugin location="zellij:compact-bar"
        }
    }

I’m choosing to have the statusbar at the top and the tabbar at the bottom. I’ll leave reversing those as an exercise to you, or keeping with my preference 😬️

Oh, and what is this? Did I or did I sneak in a strider plugin? This is a file browser within a pane. Note that it’s 15% wide and vertically adjacent to other children, meaning other panes.

And if that was too straight-forward, try replacing zellij-tab-bar with zellij-compact-bar or adding extra configuration options and keybindings to layouts.

Can I has plugins?

There’s also a plugin interface in case you want to go above and beyond what’s already there. I’ve not really reached a stage where I need that badly enough, but it’s tempting alright and there’s a few plugins out there already. In case you’re wondering, the examples are in Rust and zig but since it’s implemented via WASM you can use pretty much any language that you can compile to that.

One more thing

Also, and I decided to save this for last, you can toggle sync input to all panes in a tab with Ctrl + <t> + <s>. Then everything you type will be sent to all panes in the tab. Mind. Blown.

Enjoy