zellij Documentation

A terminal workspace with batteries included

35,803 stars Rust Rust 12 files ยท ~11,907 tokens #multiplexer#terminal#workspace
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy zellij on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name zellij -p 2222:2222 --restart always ghcr.io/linuxserver/openssh-server
Configured Services & Ports
โš™๏ธ zellij-e2e
image: ghcr.io/linuxserver/openssh-server
๐Ÿ”Œ Ports: 127.0.0.1:2222:2222/tcp
docker-compose.yml
yaml (docker-compose.yml)
---
version: "2.1"
services:
  zellij-e2e:
    image: ghcr.io/linuxserver/openssh-server
    container_name: zellij-e2e
    hostname: zellij-e2e
    environment:
      PUID: 1000
      PGID: 1000
      TZ: Europe/Vienna
      PASSWORD_ACCESS: true
      USER_PASSWORD: test
      USER_NAME: test
    volumes:
      - type: bind
        source: ./target
        target: /usr/src/zellij
      - type: bind
        source: ./src/tests/fixtures
        target: /usr/src/zellij/fixtures
    ports:
      - "127.0.0.1:2222:2222/tcp"
    restart: unless-stopped

Technical Documentation: zellij-org/zellij

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (`README.md`)

Zellij

[[Installation](https://zellij.dev/documentation/installation)] [[Screencasts & Tutorials](https://zellij.dev/screencasts/)] [[Configuration](https://zellij.dev/documentation/configuration)] [[Layouts](https://zellij.dev/documentation/layouts)] [[FAQ](https://zellij.dev/documentation/faq)]

What is this?

Zellij is a workspace aimed at developers, ops-oriented people and anyone who loves the terminal. Similar programs are sometimes called "Terminal Multiplexers".

Zellij is designed around the philosophy that one must not sacrifice simplicity for power, taking pride in its great experience out of the box as well as the advanced features it places at its users' fingertips.

Zellij is geared toward beginner and power users alike - allowing deep customizability, personal automation through layouts, true multiplayer collaboration, unique UX features such as floating and stacked panes, and a plugin system allowing one to create plugins in any language that compiles to WebAssembly.

Zellij includes a built-in web-client, making a terminal optional.

You can get started by installing Zellij and checking out the Screencasts & Tutorials.

For more details about our future plans, read about upcoming features in our roadmap.

How do I install it?

The easiest way to install Zellij is through a package for your OS.

If one is not available for your OS, you could download a prebuilt binary from the latest release and place it in your $PATH. If you'd like, we could automatically choose one for you.

You can also install (compile) with cargo:

text
cargo install --locked zellij

Try Zellij without installing

bash/zsh:

bash
bash <(curl -L https://zellij.dev/launch)

fish/xonsh:

bash
bash -c 'bash <(curl -L https://zellij.dev/launch)'

Installing from `main`

Installing Zellij from the main branch is not recommended. This branch represents pre-release code, is constantly being worked on and may contain broken or unusable features. In addition, using it may corrupt the cache for future versions, forcing users to clear it before they can use the officially released version.

That being said - no-one will stop you from using it (and bug reports involving new features are greatly appreciated), but please consider using the latest release instead as detailed at the top of this section.

How do I start a development environment?

  • Clone the project
  • In the project folder, for debug builds run: cargo xtask run
  • To run all tests: cargo xtask test

For more build commands, see CONTRIBUTING.md.

Configuration

For configuring Zellij, please see the Configuration Documentation.

About issues in this repository

Issues in this repository, whether open or closed, do not necessarily indicate a problem or a bug in the software. They only indicate that the reporter wanted to communicate their experiences or thoughts to the maintainers. The Zellij maintainers do their best to go over and reply to all issue reports, but unfortunately cannot promise these will always be dealt with or even read. Your understanding is appreciated.

Roadmap

Presented here is the project roadmap, divided into three main sections.

These are issues that are either being actively worked on or are planned for the near future.

If you'll click on the image, you'll be led to an SVG version of it on the website where you can directly click on every issue

Origin of the Name

From Wikipedia, the free encyclopedia

Zellij (Arabic: ุงู„ุฒู„ูŠุฌ, romanized: zillฤซj; also spelled zillij or zellige) is a style of mosaic tilework made from individually hand-chiseled tile pieces. The pieces were typically of different colours and fitted together to form various patterns on the basis of tessellations, most notably elaborate Islamic geometric motifs such as radiating star patterns composed of various polygons. This form of Islamic art is one of the main characteristics of architecture in the western Islamic world. It is found in the architecture of Morocco, the architecture of Algeria, early Islamic sites in Tunisia, and in the historic monuments of al-Andalus (in the Iberian Peninsula).

License

MIT



Overview (`example/README.md`)

Special considerations when using the configuration:

While trying to bind the newline character in the Config, use double quotes:

Ctrl: "\n" instead of Ctrl: '\n'


Overview (`default-plugins/layout-manager/README.md`)

About

This is an example Zellij plugin in Rust. It can be used as a template to start developing your own plugins.

More about Zellij plugins: Zellij Documentation

Development

Note: you will need to have wasm32-wasip1 added to rust as a target to build the plugin. This can be done with rustup target add wasm32-wasip1.

With the Provided Layout

Run zellij -l zellij.kdl at the root of this repository. This will open a development environment that will help you develop the plugin inside Zellij.

It can also be used if you prefer developing outside of the terminal - in this case you should ignore the $EDITOR pane and use your IDE instead.

Otherwise

  1. Build the project: cargo build
  2. Load it inside a running Zellij session: zellij action start-or-reload-plugin file:target/wasm32-wasip1/debug/rust-plugin-example.wasm
  3. Repeat on changes (perhaps with a watchexec or similar command to run on fs changes).

Overview (`example/themes/README.md`)

Themes

It contains examples showing how to write a theme.

If you would like to add a theme to zellij, please refer zellij-utils/assets/themes.


Overview (`zellij-utils/assets/README.md`)

Assets for the Utils Crate

Here reside assets, that are especially needed for
setup eg. a default configuration file.


Overview (`zellij-utils/assets/completions/README.md`)

Extra completion files that get appended to the
clap output, in order to support dynamic commands.


ARCHITECTURE (`docs/ARCHITECTURE.md`)

This document details the different components of the code base in plain English. The intention is to provide a high level description rather than a more detailed drill-down of each internal mechanism.

Screen (zellij-server/src/screen.rs)

The Screen is responsible for managing the relationship between the panes currently displayed on screen.
It is in charge of actions such as:

  • Coordinating the resizing of panes
  • Creating new panes
  • Closing existing panes (and filling up their space by other panes)

Terminal Pane (zellij-server/src/panes/terminal_pane.rs)

The TerminalPane represents a pane on screen that connects to a single pty (pseudo terminal) and (presumably) has one shell or other program running inside it.
The TerminalPane has two main roles:

  • Keeping track of the Scroll, which represents the buffer of lines in this terminal
  • Interpreting the ANSI/VT instructions coming from the pty in order to adjust the styling of characters, change the cursor position, etc.

Scroll (zellij-server/src/panes/terminal_pane.rs and zellij-server/src/panes/grid.rs)

The Scroll holds the terminal buffer and is in charge of:

  • Keeping track of the viewport (which part of it we see) this can change when we scroll up/down
  • Keeping track of the cursor position
  • Controlling line-wrapping

Terminal Character (zellij-server/src/panes/terminal_character.rs)

The TerminalCharacter represents a single character in the pane. It holds the char itself on the one hand, and an internal CharacterStyles struct representing the styling of this character.
This struct derives the Copy trait for performance reasons, because it is moved around quite a bit (eg. when line wrapping).

How a terminal emulator draws characters?

The ANSI/VT instructions received from the pty include style instructions (eg. change foreground colour to red) as well as instructions to reposition the cursor (eg. go to line x/y) and to perform certain actions with the current buffer on-screen (eg. delete all lines after cursor).

When an instruction is received to change the character style, we know that all characters printed from now on should use that style. This is true until we move to a new line (with a newline instruction or a go to instruction) or until we receive a style reset code for this specific style. There are several kinds of reset codes that apply to different styles.

It's important to understand that these styles are ongoing relative to the current cursor position. It might be useful to imagine them as brushes: "pick up the red brush", "put down the bold brush", etc.

This is important to note because the styles themselves are saved only on characters that have already been printed. If we receive an instruction to change the style to blue, then print a (blue) character, then receive another instruction to move to a new line, print another (plain) character and then come back, the style would be reset. We would have to receive a new instruction to change the style to blue in order for the next character to be blue.

Boundaries (zellij-server/src/ui/boundaries.rs)

The Boundaries refer to those lines that are drawn between terminal panes. A few things that happen here:

  • The Rect trait is here so that different panes can implement it, giving boundaries a generic way to calculate the size of the pane and draw boundaries around it.
  • Here we use the unicode box drawing characters in order to draw the borders. There's some logic here about combining them together for all possible combinations of pane locations.

PTY Bus (zellij-server/src/pty.rs)

The PtyBus keeps track of several asynchronous streams that read from pty sockets (eg. /dev/pts/999), parse those bytes into ANSI/VT events and send them on to the Screen so that they can be received in the relevant TerminalPane.


ERROR HANDLING (`docs/ERROR_HANDLING.md`)

Help wanted

As the zellij code-base changed, a lot of places where a call to unwrap()
previously made sense can now potentially cause errors which we'd like to
handle. While we don't consider unwrap to be a bad thing in general, it hides
the underlying error and leaves the user only with a stack trace to go on.
Worse than this, it will crash the application without giving us a chance to
potentially recover. This is particularly bad when the user is using
long-running sessions to perform tasks.

Hence, we would like to eliminate unwrap() statements from the code where
possible, and apply better error handling instead. This way, functions higher
up in the call stack can react to errors from underlying functions and either
try to recover, or give some meaningful error messages if recovery isn't
possible.

Since the zellij codebase is pretty big and growing rapidly, this endeavor
will continue to be pursued over time, as zellij develops. The idea is that
modules or single files are converted bit by bit, preferably in small PRs that
each target a specific module or file. If you are looking to contribute to
zellij, this may be an ideal start for you!
This way you get to know the
codebase and get an idea which modules are used at which other places in the
code.

If you have an interest in this, don't hesitate to get in touch with us and
refer to the tracking issue to see what has already been
done.

Error handling facilities

You get access to all the relevant functions and traits mentioned in the
remainder of this document by including/adding this in the code you're working
on:

rust
use zellij_utils::errors::prelude::*;

Displaying panic messages

Panics are generally handled via the Panic error type and the
handle_panic panic handler function. The fancy formatting
is performed by the miette crate.

Propagating errors

We use the anyhow crate to propagate errors up the call stack. At
the moment, zellij doesn't have custom error types, so we wrap whatever errors
the underlying libraries give us, if any. anyhow serves the purpose
of providing context about where (i.e. under which circumstances)
an error happened.

A critical requirement for propagating errors is that all functions involved
must return the Result type. This allows convenient error handling
with the ? operator.

At some point you will likely stop propagating errors and decide what to do
with the error. Generally you can:

  1. Try to recover from the error, or
  2. Report the error to the user and either
    1. Terminate program execution (See fatal), or
    2. Continue program execution (See non_fatal)

Handling errors

Ideally, when the program encounters an error it will try to recover as good as
it can. This can mean falling back to some sane default if a specific value
(e.g. an environment variable) cannot be found. Note that this isn't always
applicable. If in doubt, don't hesitate to ask.

Recovery usually isn't an option if an operation has changed the internal state
(i.e. the value or content of specific variables) of objects in the code. In
this case, if an error is encountered, it is best to declare the program state
corrupted and terminate the whole application. This can be done by unwraping
on the Result type. Always try to propagate the error as good as
you can and attach meaningful context before unwraping. This gives the user
an idea what went wrong and can also help developers in quickly identifying
which parts of the code to debug if necessary.

When you encounter such a fatal error and cannot propagate it further up (e.g.
because the current function cannot be changed to return a Result,
or because it is the "root" function of a program thread), use the
fatal function to panic the application. It will attach some small
context to the error and finally unwrap it. Using this function over the
regular unwrap has the added benefit that other developers seeing this in the
code know that someone has previously spent some thought about error handling
at this location.

If you encounter a non-fatal error, use the non_fatal function
to handle it. Instead of panicing the application, the error is written to
the application log and execution continues. Please use this sparingly, as an
error usually calls for actions to be taken rather than ignoring it.

Examples of applied error handling

You can have a look at the commit that introduced error handling to the
zellij_server::screen module right here (look at the changes in
zellij-server/src/screen.rs). We'll use this to demonstrate a few things in
the following text. You can find countless other examples in the tracking
issue for error handling

Converting a function to return a `Result` type

TL;DR

  • Add use zellij_utils::errors::prelude::*; to the file
  • Make the function return Result<T>, with an appropriate T (Often ())
  • Append .context() to any Result you get with a sensible error description (see below)
  • Generate ad-hoc errors with anyhow!(<SOME MESSAGE>)

Here's an example of the Screen::render function as it looked before:

rust
pub fn render(&mut self) {
        // ...
        let serialized_output = output.serialize();
        self.bus
            .senders
            .send_to_server(ServerInstruction::Render(Some(serialized_output)))
            .unwrap();
    }

It performs a few actions (not shown here for brevity) and then sends an IPC
message to the server. As you can see it calls unwrap() on the result from
sending a message to the server. This means: If sending the message to the
server fails, execution is terminated and the program crashes. Let's assume
that crashing the application in this case is a reasonable course of action.

In total (as of writing this), the render() function is called 80 times from
various places in the code of the Screen struct. Hence, if sending the
message fails, we only see that the application crashed trying to send an IPC
message to the server. We won't know which of the 80 different code paths lead
to the execution of this function.

So what can we do? Instead of unwraping on the Result type here, we can
pass it up to the calling functions. Each of the callers can then decide for
themselves what to do: Continue regardless, propagate the error further up or
terminate execution.

Here's what the function looked like after the change linked above:

rust
pub fn render(&mut self) -> Result<()> {
        let err_context = || "failed to render screen".to_string();
        // ...

        let serialized_output = output.serialize();
        self.bus
            .senders
            .send_to_server(ServerInstruction::Render(Some(serialized_output)))
            .with_context(err_context)
    }

We leverage the Context trait from anyhow to attach a
context message to the error and make the function return a Result type
instead. As you can see, the Result here contains a (), which is the empty
type. It's primary purpose here is allowing us to propagate errors to callers
of this function.

Hence, for example the resize_to_screen function changed from this:

rust
pub fn resize_to_screen(&mut self, new_screen_size: Size) {
        // ...
        self.render();
    }

to this:

rust
pub fn resize_to_screen(&mut self, new_screen_size: Size) -> Result<()> {
        // ...
        self.render()
            .with_context(|| format!("failed to resize to screen size: {new_screen_size:#?}"))
    }

Note how it returns a Result type now, too. This way we can pass the error up
to callers of resize_to_screen and keep going like this until we decide it's
time to do something about the error.

In general, any function calling unwrap or expect is a good candidate to be
rewritten to return a Result type instead.

Attaching context

Anyhows Context trait gives us two methods to attach
context to an error: context and with_context. You should use context
if the message contains only a static text and with_context if you need
additional formatting:

rust
fn move_clients_between_tabs(
        &mut self,
        source_tab_index: usize,
        destination_tab_index: usize,
        clients_to_move: Option<Vec<ClientId>>,
    ) -> Result<()> {
        // ...
        if let Some(client_mode_info_in_source_tab) = drained_clients {
            let destination_tab = self.get_indexed_tab_mut(destination_tab_index)
                .context("failed to get destination tab by index")
                .with_context(|| format!("failed to move clients from tab {source_tab_index} to tab {destination_tab_index}"))?;
            // ...
        }
        Ok(())
    }

Feel free to move context string/closure to a variable to avoid copy-pasting:

rust
pub fn render(&mut self) -> Result<()> {
        let err_context = "failed to render screen";
        // ...

        for tab_index in tabs_to_close {
            // ...
            self.close_tab_at_index(tab_index)
                .context(err_context)?;
        }
        // ...
        self.bus
            .senders
            .send_to_server(ServerInstruction::Render(Some(serialized_output)))
            .context(err_context)
    }
    // ...
    pub fn close_tab(&mut self, client_id: ClientId) -> Result<()> {
        let err_context = || format!("failed to close tab for client {client_id:?}");

        let active_tab_index = *self
            .active_tab_indices
            .get(&client_id)
            .with_context(err_context)?;
        self.close_tab_at_index(active_tab_index)
            .with_context(err_context)
    }

When there is only a single Result to be returned from your function, use
context as shown above for resize_to_screen.

Choosing helpful context messages

TL;DR

  • Don't repeat what the error message in the Result says
  • Describe what you were doing, ideally include the current functions name

When attaching context to an error, usually you want to express what you were
doing when the error occurred, i.e. in what context the error occurred. In the
render method, we could have done something like this instead:

rust
pub fn render(&mut self) -> Result<()> {
        // ...

        for tab_index in tabs_to_close {
            // ...
            self.close_tab_at_index(tab_index)
                .context("Failed to close tab at index: {tab_index}")?;
        }
        // ...
        self.bus
            .senders
            .send_to_server(ServerInstruction::Render(Some(serialized_output)))
            .context("Failed to send message to server")
    }

Why do we add the message "failed to render screen" instead? Because that is
what we were trying to do when we received the error from the underlying
functions (close_tab_at_index and send_to_server in this case). Functions
from libraries usually already return an error that describes what went wrong
(Example: When we try to open a file that doesn't exist, the std library will
give us a NotFound error), so we don't have to repeat that.

In case of doubt, look at the name of the function you're currently working in
and write a context message somehow mentioning this.

Terminating execution

TL;DR

  • Terminate execution on errors by adding .fatal() to it
  • First try to pass the error as far up as you can or deem reasonable

We want to propagate errors as far up as we can. This way, every function along
the way can at least attach a context message giving us an idea what chain of
events lead to the error. Where do we terminate execution in Screen? If you
study the code in screen.rs, you'll notice all the components of zellij
interact with the Screen instance by means of IPC messages. These messages
are handled in the screen_thread_main function. Here's an excerpt:

rust
ScreenInstruction::Render => {
        screen.render()?;
    },
    ScreenInstruction::NewPane(pid, client_or_tab_index) => {
        // ...
        screen.update_tabs()?;

        screen.render()?;
    },
    ScreenInstruction::OpenInPlaceEditor(pid, client_id) => {
        // ...
        screen.update_tabs()?;

        screen.render()?;
    },

The code goes on like this for quite a while, so there are many places where an
error may occur. In this case, since all the functions are called from this
central location, we forego attaching a context message to every error.
Instead, we propagate the errors to the caller of this function, which happens
to be the function init_session in zellij-server/src/lib.rs. We see that
screen_thread_main is spawned to run in a separate thread. Hence, we cannot
propagate the error further up and terminate execution at this point:

rust
// ...
    screen_thread_main(
        screen_bus,
        max_panes,
        client_attributes_clone,
        config_options,
    )
    .fatal();

Remember the call to fatal will log the error and afterwards panic
the application (i.e. crash zellij). Since we made sure to attach context
messages to the errors on their way up, we will see these messages in the
resulting output!

Error handling for `Option` types

TL;DR

  • Attach a .context with a message saying why a None here is an error
  • Attach a regular context message like you would for a Result type, too!

Beyond what's described in "Choosing helpful context messages" above, Option
types benefit from extra handling. That is because a Option containing a
None where a value is expected doesn't carry an error message: It doesn't
tell us why the None is bad (i.e. equivalent to an Error) in this case.

In situations where a call to unwrap() or similar on a Option type is to be
converted for error handling, it is a good idea to attach an additional short
context. An example from the zellij codebase is shown below:

rust
let destination_tab = self.get_indexed_tab_mut(destination_tab_index)
        .context("failed to get destination tab by index")
        .with_context(|| format!("failed to move clients from tab {source_tab_index} to tab {destination_tab_index}"))?;

Here the call to self.get_indexed_tab_mut(destination_tab_index) will return
a Option. The surrounding code, however, doesn't know what to do with a
None value, so it is considered an error.

Here you see that we attach two contexts:

rust
.context("failed to get destination tab by index")

Because the None type itself doesn't tell us what the "error" means, we
attach a description manually. The second context:

rust
.with_context(|| format!("failed to move clients from tab {source_tab_index} to tab {destination_tab_index}"))?;

then describes what the surrounding function was trying to achieve (See
descriptions above).

Logging errors

TL;DR

  • When there's a Result type around, use .non_fatal() on that instead of log::error!
  • When there's a Err type around, use Err::<(), _>(err).non_fatal()
  • Also attach context before logging!
  • For further examples, refer to PR #1881

You may encounter situations where you have an error and decide it's safe to
ignore. Depending on the circumstances, this is a perfectly fine thing to do.
However, oftentimes it proves to be useful to at least log the error, so in
case things do go wrong we at least see the logged error message. Also, the
logged message may hint towards an underlying problem which may require further
action.

An obvious thing to do is something like the following:

rust
log::error!("failed to find tab with index {tab_index}");

While an ad-hoc log message is better than silently ignoring the error, we can
usually do better than that. That is because in large parts of the codebase we
have a Result available in one form or another.

If the Result has been treated as suggested above and context messages have
been attached to it, it already contains a lot of valuable information. This is
lost when instead we log a custom error. Hence, the better solution is to log
the Result type including all the context information.

This is easily achieved like so:

rust
fs::create_dir_all(&plugin_global_data_dir)
    .context("failed to create plugin asset directory")
    .non_fatal();

Here, we try to create a directory, and if we fail to do this we simply log the
error (with non_fatal()) and continue. It's important to note that the
non_fatal() function always returns (): It cannot be used on any Result
type whose Ok value is different from (). This is on purpose: If your
Result carries a type, it is probably required for further
calculations/actions. Hence, you mustn't ignore it.

Also note that, even though we log the error and continue, we still attach
context information to it. Just like with fatal errors, the context information
allows us to tell what we tried to do before we got the error and logged it.

This is a simple example, and oftentimes the Result you're trying to handle
doesn't carry a (). Your code may look more like this:

rust
if let Ok(active_tab) = self.get_active_tab(client_id) {
    let active_tab_pos = active_tab.position;
    let new_tab_pos = (active_tab_pos + 1) % self.tabs.len();
    return self.switch_active_tab(new_tab_pos, client_id);
} else {
    log::error!("Active tab not found for client_id: {:?}", client_id);
}

When we get an Ok, we do something with it. When we get an Err, we log a
message. Here you'll notice that the usage of an if let statement hides the
error from us: In the else branch, we have no means to access the error we
got from get_active_tab() above. In such a situation, it is helpful to
rewrite the if let to a match statement instead:

rust
match self.get_active_tab(client_id) {
    Ok(active_tab) => {
        let active_tab_pos = active_tab.position;
        let new_tab_pos = (active_tab_pos + 1) % self.tabs.len();
        return self.switch_active_tab(new_tab_pos, client_id);
    },
    Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(),
}

The interesting part here is what happens after the Err(err). Let's break it down:

  1. Err(err) =>: We're making the Err value that was inside the Result
    accessible in the variable err
  2. Err::<(), _>(err): We're wrapping the err back into a Result with the
    Err() function
  3. .with_context(err_context): We're attaching error context (the actual
    context isn't shown here)
  4. .non_fatal(): We're logging the error with non_fatal

Notice that in step 2 we must qualify the final Result type, which is the job
of the ::<(), _> attached to Err. This is necessary because at this point
Rust cannot determine on its own what the Ok value of the Result we create
may be. This isn't relevant for our case, because we know the Result will
never be an Ok variant, but Rust still requires this.

If you're getting errors about "type annotations needed" or "cannot infer type
for type parameter T" in one of your calls to Err, this is most likely
fixed by adding ::<(), _>.

Adding Concrete Errors, Handling Specific Errors

TL;DR

  • Add a new variant to zellij_utils::errors::ZellijError
  • Use anyhow::Error::downcast_ref::<ZellijError>() to recover underlying errors

Sometimes you'll find yourself in a situation where you want to react to very
specific errors. For example, the "command panes" feature in zellij has a
special handling for "command not found" errors. If all the anyhow::Errors
are the same, how can we distinguish between underlying error types?

This is possible because while anyhow can unify a vast amount of errors into
the anyhow::Error type, it also gives us the possibility to recover
underlying error types. To do this, however, we must first know what error type
to expect.

External libraries, such as other crates or even std will likely define their
own error types. These error types have distinct error variants that one can
distinguish and react upon. But what happens, for example, if we create the
error we want to react to ourselves?

For this purpose, there is the ZellijError, which is contained in
zellij_utils::errors. It is built with the thiserror crate and
hence easily extensible. If you need a specific error type to act upon, just
define a new variant in ZellijError. It is automatically available in any
source file that has the use zellij_utils::errors::prelude::*; statement in
it.

Once you have created your error instance, as soon as you wrap a context
around it, it is turned into an anyhow::Error. This makes it compatible with
all the other functions in the code that return anyhow::Result.

Recovering the error can look, for example, like this:

rust
match pty
    .spawn_terminal(terminal_action, client_or_tab_index)
    .with_context(err_context)  // <-- Note how we attach a context, but can
                                //     still recover the error below!
{
    Ok(_) => {
        // ... Whatever
    },
    Err(err) => match err.downcast_ref::<ZellijError>() {
        Some(ZellijError::CommandNotFound { terminal_id, .. }) => {
            // Do something now that this error occurred.
            // We can even access the values stored inside it, "terminal_id" in
            // this case
        },
        // You can check for other error variants here
        _ => {
            // Some other error, which we haven't checked for, occurred here.
            // Now we can, for example, log it!
            Err::<(), _>(err).non_fatal(),
        },
    },
}

PACKAGING DESKTOP (`docs/PACKAGING_DESKTOP.md`)

Packaging Zellij for the desktop (Linux)

Each release has a separate zellij-desktop-files.tar.gz (with a
zellij-desktop-files.sha256sum) holding a share/ folder laid out like
/usr/share. The binary archives hold only the binary. Install the folder's
contents alongside the zellij binary so the desktop app shows up in
application menus and software centres:

File in zellij-desktop-files.tar.gz Install to
share/applications/dev.zellij.Zellij.desktop /usr/share/applications/
share/icons/hicolor/128x128/apps/dev.zellij.Zellij.png /usr/share/icons/hicolor/128x128/apps/
share/icons/hicolor/256x256/apps/dev.zellij.Zellij.png /usr/share/icons/hicolor/256x256/apps/
share/icons/hicolor/512x512/apps/dev.zellij.Zellij.png /usr/share/icons/hicolor/512x512/apps/
share/metainfo/dev.zellij.Zellij.metainfo.xml /usr/share/metainfo/

The same files are in the source tree: assets/dev.zellij.Zellij.desktop,
zellij-utils/assets/icons/logo128.png, logo256.png and logo512.png (each installed as
dev.zellij.Zellij.png in its size folder) and assets/dev.zellij.Zellij.metainfo.xml.

Notes for packagers:

  • The launcher replaces the old zellij.desktop. Do not ship both, or the app
    menu shows two "Zellij" entries.
  • Exec=zellij window and TryExec=zellij find the binary through PATH, so
    zellij must be on PATH (for example /usr/bin/zellij).
  • The app ID is dev.zellij.Zellij. The window's Wayland app ID and X11 class,
    the icon name and the notification sender all use it, so keep the file names.
  • The launcher carries the keys xdg-terminal-exec reads (X-TerminalArgExec,
    X-TerminalArgDir, X-TerminalArgHold), so Zellij can be chosen as the default
    terminal once it is installed.
  • When Zellij finds its launcher in $XDG_DATA_DIRS/applications, it shows it as
    "installed by your package manager" and never writes its own copy.
  • A desktop entry or default-terminal setting that Zellij writes itself only ever
    runs a binary in a permanent place: a package-manager path (Nix, Homebrew,
    cargo install, /usr outside /usr/local), a system folder (/usr/local
    or /opt), or Zellij's own copy in ~/.local/bin. Run from anywhere else, Zellij
    first copies itself to ~/.local/bin (or updates an older Zellij there) and
    points the entry at that copy. A binary in one of the permanent places is never
    offered a copy.
  • Check the listing with appstreamcli validate --no-net dev.zellij.Zellij.metainfo.xml and the launcher with desktop-file-validate.
  • Zellij decides whether it runs on a desktop from the server's environment: no
    DISPLAY or WAYLAND_DISPLAY, an SSH_CONNECTION, SSH_CLIENT or SSH_TTY,
    a container or WSL all count as "no desktop". Set
    ZELLIJ_DESKTOP_INTEGRATION=on to treat the environment as a desktop anyway,
    or off to never offer desktop items.
  • Everything the setup screen does is also available from the command line:
    zellij setup --copy-to-permanent-folder [--replace-existing],
    --install-desktop-entry, --uninstall-desktop-entry,
    --make-default-terminal and --keybinding-preset NAME. They use the same
    checks as the setup screen and can be combined.

RELEASE (`docs/RELEASE.md`)

How to release a zellij version

This document is primarily targeted at zellij maintainers in need to (prepare to)
release a new zellij version.

Simulating a release

This section explains how to do a "dry-run" of the release process. This is
useful to check if a release is successful beforehand, i.e. before publishing
it to the world. Because there is no "undo"-button for a real release as
described below, it is recommended to perform a simulated release first.

Requirements

You only need a publicly accessible Git repository to provide a cargo registry.

High-level concept

The setup explained below will host a third-party cargo registry software
(ktra) locally on your PC. In order for
cargo to pick this up and be able to work with it, we must perform a few
modifications to the zellij repository and other components. Once setup, we
release a zellij version to this private registry and install zellij from there
to make sure it works as expected.

Step-by-step guide

  1. Create a cargo index repository
    1. Create a new repo on some git forge (GitHub/GitLab/...)
    2. Clone the repo with HTTPS (not SSH), we'll refer to the https://
      clone-url as $INDEX_REPO for the remainder of this text
    3. Add a file named config.json with the following content in the root:
      json
      {"dl":"http://localhost:8000/dl","api":"http://localhost:8000"}
    4. Generate an access token for full repo access, we'll refer to this as
      $TOKEN for the remainder of this text
    5. Create and push a commit with these changes. Provide the following HTTPS
      credentials:
      1. Username: Your git-forge username
      2. Password: $TOKEN
  2. Prepare the zellij repo
    1. cd into your local copy of the zellij repository
    2. Add a new cargo registry to .cargo/config.toml like this:
      toml
      [registries]
      ktra = { index = "https://$INDEX_REPO" }
    3. Modify all Cargo.toml in the zellij repo to retrieve the individual
      zellij subcrates from the private registry:
      1. Find all dependencies that look like this:
        toml
        zellij-utils = { path = "../zellij-utils/", version = "XXX" }
      2. Change them to look like this
        toml
        zellij-utils = { path = "../zellij-utils/", version = "XXX", registry = "ktra" }
      3. This applies to all zellij subcrates, e.g. zellij-client,
        zellij-server, ... You can ignore the plugins, because these aren't
        released as sources.
  3. Launch your private registry
    1. Create the file ~/.cargo/config.toml with the following content:
      text
      [registries.ktra]
      index = "https://$INDEX_REPO"
    2. Install ktra, the registry server: cargo install ktra
    3. In a separate shell/pane/whatever, navigate to some folder where you
      want to store all data for the registry
    4. Create a config file for ktra named ktra.toml there with the
      following content:
      toml
      [index_config]
      remote_url = "https://$INDEX_REPO"
      https_username = "your-git-username"
      https_password = "$TOKEN" 
      branch = "main"  # Or whatever branch name you used
    5. Launch ktra (with logging to see what happens): RUST_LOG=debug ktra
    6. Get a registry token for ktra (The details don't really matter, unless
      you want to reuse this registry):
      bash
      curl -X POST -H 'Content-Type: application/json' -d '{"password":"PASSWORD"}' http://localhost:8000/ktra/api/v1/new_user/ALICE
    7. Login to the registry with the token you received as reply to the
      previous command:
      bash
      cargo login --registry ktra "KTRA_TOKEN"
  4. Install safety measures to prevent accidentally performing a real release:
    1. In your zellij repo, remove all configured remotes that allow you to
      push/publish directly to the zellij main GitHub repo. Setup a fork of
      the main zellij repo instead and configure a remote that allows you to
      push/publish to that. Please, this is very important.
    2. Comment out the entire [registry] section in ~/.cargo/credentials to
      prevent accidentally pushing a new release to crates.io.
  5. Simulate a release
    1. Go back to the zellij repo, type:
      bash
      cargo x publish --git-remote <YOUR_ZELLIJ_FORK> --cargo-registry ktra
    2. A prompt will open with the commit message for the release commit. Just
      save and close your editor to continue
    3. If all goes well, the release will be done in a few minutes and all the
      crates are published to the private ktra registry!
  6. Testing the release binary
    1. Install zellij from the registry to some local directory like this:
      bash
      $ cargo install --registry ktra --root /tmp zellij
    2. Execute the binary to see if all went well:
      bash
      $ /tmp/bin/zellij
  7. Cleaning up
    1. Uncomment the [registry] section in ~/.cargo/config.toml
    2. Restore your original git remotes for the zellij repo
    3. Undo your last commit:
      bash
      $ git reset --hard HEAD~1
    4. Undo your last commit in the remote zellij repo:
      bash
      $ git push --force <YOUR_ZELLIJ_FORK>
    5. Delete the release tag:
      bash
      $ git tag -d "vX.Y.Z"
    6. Delete the release tag in the remote zellij repo
      bash
      $ git push <YOUR_ZELLIJ_FORK> --force --delete "vX.Y.Z"

You're done! :tada:

Releasing a new version


TERMINOLOGY (`docs/TERMINOLOGY.md`)

ANSI/VT

This refers to the stream of text one receives when listening to the primary side of the pty.
For example [31mHi [5mthere! would print the word "Hi" in red and then the word "there!" in a blinking red. You can try this in your terminal with the echo command:

echo -e "\033[31mHi \033[5mthere!" (\033 is an escape character).

CSI (Control Sequence Identifier)

Part of the ANSI/VT specification that includes instructions for the terminal emulator. These instructions can be a style change (eg. change color to red) or cursor position change (eg. go to line x/y).

OSC (Operating System Command)

Part of the ANSI/VT specification that includes instructions for the underlying operating system (eg. change window title).

pty

A pty (pseudoterminal) is a character device that emulates a traditional terminal. It is a pair of processes (traditionally given culturally insensitive names, here they will be referred to as primary/secondary).

The primary part is the part connected to the terminal emulator. The emulator listens to this part of the channel, reads instructions from it that it uses in order to draw characters on the screen.
The secondary part is used by the program running inside the terminal (eg. the shell) in order to send those instructions.

In Zellij, there is one pty pair for each terminal pane.


THIRD PARTY INSTALL (`docs/THIRD_PARTY_INSTALL.md`)

Third Party Install

Packages

:warning: These packages are not affiliated with the Zellij maintainers and are provided here for convenience.

Arch Linux

You can install the zellij package from the official extra repository:

text
pacman -S zellij

Or install from AUR repository with AUR Helper:

text
paru -S zellij-git

Fedora Linux

You can install the zellij package from the COPR

text
sudo dnf copr enable varlad/zellij 
sudo dnf install zellij

MacOS

You can install zellij with Homebrew on MacOS:

text
brew install zellij

Or install with MacPorts:

text
sudo port install zellij

Void Linux

text
sudo xbps-install zellij

Packaging the desktop app

Packagers adding the launcher, icons and software-centre listing will find the
details in PACKAGING_DESKTOP.md.


--- METRICS ---