Zellij, asdf, and direnv: A Complete Tutorial Course

This tutorial will guide you through setting up and using three powerful developer tools that can drastically improve your workflow:

  1. Zellij: A terminal workspace with panes, tabs, and session management
  2. asdf: A universal version manager for multiple languages and tools
  3. direnv: An environment switcher for the shell

By the end of this course, you’ll have a fully configured development environment that automatically loads project-specific tool versions and environment variables, all within an efficient terminal workspace.

Table of Contents


1. Setting Up Zellij

1.1 Installation

macOS

brew install zellij

Ubuntu/Debian

curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/zellij-org/zellij/master/assets/install.sh | bash

Arch Linux

pacman -S zellij

Verify Installation

zellij --version

1.2 Basic Navigation

Zellij uses a modal system for keybindings, similar to Vim. Here are the essential keybindings:

  • Ctrl+p: Enter pane mode
  • Ctrl+t: Enter tab mode
  • Ctrl+h: Enter resize mode
  • Ctrl+g: Enter locked mode (passes all keys to the application)
  • Ctrl+q: Quit Zellij

Pane Mode (Ctrl+p)

  • h/j/k/l: Move focus (left/down/up/right)
  • n: Create new pane
  • d: Close focused pane
  • f: Toggle floating pane
  • z: Toggle pane zoom

Tab Mode (Ctrl+t)

  • n: Create new tab
  • x: Close current tab
  • 1-9: Switch to tab by number
  • [/]: Navigate tabs

1.3 Layouts and Configurations

Zellij layouts are defined in YAML files that specify the arrangement of panes and tabs.

Create a default config directory:

mkdir -p ~/.config/zellij

Create a basic layout file:

# ~/.config/zellij/layouts/dev.yaml
---
template:
  direction: Horizontal
  parts:
    - direction: Vertical
      borderless: true
      split_size:
        Fixed: 1
      run:
        plugin:
          location: "zellij:tab-bar"
    - direction: Horizontal
      body: true
      parts:
        - direction: Vertical
          parts:
            - direction: Vertical
              ratio: 2
            - direction: Vertical
              ratio: 1
        - direction: Vertical
          ratio: 1
    - direction: Vertical
      borderless: true
      split_size:
        Fixed: 2
      run:
        plugin:
          location: "zellij:status-bar"

Launch Zellij with a custom layout:

zellij --layout ~/.config/zellij/layouts/dev.yaml

1.4 Custom Keybindings

Create a custom config file:

# ~/.config/zellij/config.yaml
---
keybinds:
  normal:
    - action: [SwitchToMode: Pane]
      key: [Ctrl: 'a']
  pane:
    - action: [SwitchToMode: Normal]
      key: [Escape]
    - action: [MoveFocus: Left]
      key: [Char: 'h']
    - action: [MoveFocus: Right]
      key: [Char: 'l']
    - action: [MoveFocus: Down]
      key: [Char: 'j']
    - action: [MoveFocus: Up]
      key: [Char: 'k']

1.5 Creating Persistent Sessions

To create a named session:

zellij --session my-project

To attach to an existing session:

zellij attach my-project

List all active sessions:

zellij list-sessions

2. Setting Up asdf

2.1 Installation

macOS

brew install asdf

Add to your shell (for bash):

echo -e "\n. $(brew --prefix asdf)/libexec/asdf.sh" >> ~/.bashrc
echo -e "\n. $(brew --prefix asdf)/etc/bash_completion.d/asdf.bash" >> ~/.bashrc

For zsh:

echo -e "\n. $(brew --prefix asdf)/libexec/asdf.sh" >> ~/.zshrc

Linux

git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.10.2

Add to your shell (for bash):

echo -e "\n. $HOME/.asdf/asdf.sh" >> ~/.bashrc
echo -e "\n. $HOME/.asdf/completions/asdf.bash" >> ~/.bashrc

For zsh:

echo -e "\n. $HOME/.asdf/asdf.sh" >> ~/.zshrc

2.2 Adding Plugins

asdf manages different programming languages and tools through plugins. Let’s add some common ones:

# Node.js
asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git
 
# Python
asdf plugin add python https://github.com/danhper/asdf-python.git
 
# Ruby
asdf plugin add ruby https://github.com/asdf-vm/asdf-ruby.git
 
# Golang
asdf plugin add golang https://github.com/kennyp/asdf-golang.git
 
# Rust
asdf plugin add rust https://github.com/code-lever/asdf-rust.git

List installed plugins:

asdf plugin list

2.3 Installing Versions

Install specific versions of your tools:

# Node.js
asdf install nodejs 16.14.0
 
# Python
asdf install python 3.10.0
 
# Ruby
asdf install ruby 3.1.0
 
# Golang
asdf install golang 1.17.8
 
# Rust
asdf install rust 1.59.0

List installed versions:

asdf list nodejs

2.4 Setting Global and Local Versions

Set a global version (default for all projects):

asdf global nodejs 16.14.0
asdf global python 3.10.0

Set a local version (for a specific project):

cd ~/projects/my-project
asdf local nodejs 14.19.0
asdf local python 3.9.10

This creates a .tool-versions file in your project directory.

2.5 Switching Between Versions

asdf automatically uses the appropriate version based on the directory you’re in:

# Global version
cd ~
node --version  # 16.14.0
 
# Project-specific version
cd ~/projects/my-project
node --version  # 14.19.0

3. Setting Up direnv

3.1 Installation

macOS

brew install direnv

Ubuntu/Debian

sudo apt-get update
sudo apt-get install direnv

Shell Integration

For bash:

echo 'eval "$(direnv hook bash)"' >> ~/.bashrc

For zsh:

echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc

3.2 Basic Configuration

Create a global configuration:

mkdir -p ~/.config/direnv
touch ~/.config/direnv/direnvrc

Example configuration:

# ~/.config/direnv/direnvrc
# Usage: use_node [version]
use_node() {
  local node_version=$1
  local node_path="$HOME/.asdf/installs/nodejs/${node_version}"
  
  if [[ ! -d "$node_path" ]]; then
    echo "Node.js version $node_version not found. Installing..."
    asdf install nodejs $node_version
  fi
  
  load_prefix "$node_path"
  layout node
}
 
# Usage: use_python [version]
use_python() {
  local python_version=$1
  local python_path="$HOME/.asdf/installs/python/${python_version}"
  
  if [[ ! -d "$python_path" ]]; then
    echo "Python version $python_version not found. Installing..."
    asdf install python $python_version
  fi
  
  load_prefix "$python_path"
  layout python $python_version
}

3.3 Creating Project-Specific Environments

Create a .envrc file in your project directory:

# ~/projects/my-project/.envrc
export PROJECT_ROOT=$(pwd)
export ENVIRONMENT="development"
 
# Set environment variables
export API_KEY="dev_key_123"
export DATABASE_URL="postgres://user:pass@localhost/dev_db"
 
# Use specific tool versions
use node 14.19.0
use python 3.9.10
 
# Add project bin directory to PATH
PATH_add bin

Allow the .envrc file:

cd ~/projects/my-project
direnv allow

Now, whenever you enter the project directory, direnv will automatically set these environment variables.

3.4 Advanced Use Cases

Loading Secrets from a .env File

# ~/projects/my-project/.envrc
dotenv .env.local

Integrating with asdf:

# ~/projects/my-project/.envrc
use asdf

This will automatically use the versions specified in .tool-versions.

Project-specific Shell Aliases

# ~/projects/my-project/.envrc
alias start="npm start"
alias test="npm test"
alias deploy="./scripts/deploy.sh"

4. Integrating All Three Tools

4.1 Project Setup Workflow

Here’s a workflow to set up a new project with all three tools:

  1. Create a new project directory:
mkdir ~/projects/new-project
cd ~/projects/new-project
  1. Initialize version control:
git init
  1. Set up asdf local versions:
asdf local nodejs 16.14.0
asdf local python 3.10.0
  1. Create direnv configuration:
# ~/projects/new-project/.envrc
export PROJECT_ROOT=$(pwd)
export NODE_ENV="development"
 
# Load project-specific environment variables
dotenv_if_exists .env.local
 
# Use asdf versions
use asdf
 
# Add local node_modules bin to PATH
PATH_add node_modules/.bin
  1. Allow direnv:
direnv allow
  1. Create a Zellij layout for the project:
# ~/.config/zellij/layouts/new-project.yaml
---
template:
  direction: Horizontal
  parts:
    - direction: Vertical
      borderless: true
      split_size:
        Fixed: 1
      run:
        plugin:
          location: "zellij:tab-bar"
    - direction: Horizontal
      body: true
      parts:
        - direction: Vertical
          parts:
            - direction: Horizontal
              parts:
                - command: { text: "cd ~/projects/new-project && vim ." }
                  focus: true
            - direction: Horizontal
              parts:
                - command: { text: "cd ~/projects/new-project && npm run dev" }
        - direction: Vertical
          ratio: 1
          parts:
            - command: { text: "cd ~/projects/new-project && git status" }
    - direction: Vertical
      borderless: true
      split_size:
        Fixed: 2
      run:
        plugin:
          location: "zellij:status-bar"
  1. Start a new Zellij session with this layout:
zellij --session new-project --layout ~/.config/zellij/layouts/new-project.yaml

4.2 Development Workflow

With this setup, your daily workflow becomes:

  1. Start or attach to your Zellij session:
zellij attach new-project
  1. Navigate to your project directory (direnv automatically loads environments):
cd ~/projects/new-project
  1. Work across multiple panes in Zellij:

    • Code editor
    • Running server
    • Git operations
    • Tests
  2. Switch projects seamlessly:

# In a new pane:
cd ~/projects/another-project
# direnv automatically switches environment variables and tool versions

4.3 Team Collaboration

For team collaboration:

  1. Share asdf configuration:

    • Commit .tool-versions to version control
  2. Share direnv template:

    • Create a .envrc.example file with template values
    • Add to version control, but keep the actual .envrc in .gitignore
  3. Share Zellij layout:

    • Add project layout to version control in a zellij-layouts directory

Example onboarding instructions for team members:

# Project Setup
 
1. Clone the repository:
   ```bash
   git clone [email protected]:organization/new-project.git
   cd new-project
  1. Install required tool versions (requires asdf):

    # asdf will read the .tool-versions file
    asdf install
  2. Set up environment:

    cp .envrc.example .envrc
    # Edit .envrc with your local values
    direnv allow
  3. Start Zellij workspace:

    zellij --session new-project --layout ./zellij-layouts/default.yaml

## 5. Practical Projects

### 5.1 Project 1: Web Development Setup

Let's set up a Next.js project with the three tools:

1. Create a project directory:

```bash
mkdir -p ~/projects/nextjs-app
cd ~/projects/nextjs-app
  1. Set up asdf:
asdf local nodejs 18.12.0
  1. Create a direnv configuration:
# ~/projects/nextjs-app/.envrc
export NEXT_PUBLIC_API_URL="http://localhost:3001/api"
export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/nextjs_dev"
 
# Use node version from .tool-versions
use asdf
 
# Add local bins to PATH
PATH_add node_modules/.bin
  1. Allow direnv:
direnv allow
  1. Create a Zellij layout:
# ~/.config/zellij/layouts/nextjs.yaml
---
template:
  direction: Horizontal
  parts:
    - direction: Vertical
      borderless: true
      split_size:
        Fixed: 1
      run:
        plugin:
          location: "zellij:tab-bar"
    - direction: Horizontal
      body: true
      parts:
        - direction: Vertical
          parts:
            - direction: Horizontal
              parts:
                - command: { text: "cd ~/projects/nextjs-app && vim ." }
                  focus: true
            - direction: Horizontal
              parts:
                - command: { text: "cd ~/projects/nextjs-app && npm run dev" }
        - direction: Vertical
          ratio: 1
          parts:
            - command: { text: "cd ~/projects/nextjs-app && npm run test:watch" }
            - command: { text: "cd ~/projects/nextjs-app && git status" }
    - direction: Vertical
      borderless: true
      split_size:
        Fixed: 2
      run:
        plugin:
          location: "zellij:status-bar"
  1. Initialize the Next.js project:
npx create-next-app@latest .
  1. Start the Zellij session:
zellij --session nextjs-app --layout ~/.config/zellij/layouts/nextjs.yaml

5.2 Project 2: Data Science Environment

  1. Create a project directory:
mkdir -p ~/projects/data-science-project
cd ~/projects/data-science-project
  1. Set up asdf:
asdf local python 3.10.4
  1. Create a direnv configuration:
# ~/projects/data-science-project/.envrc
export PYTHONPATH=$PWD:$PYTHONPATH
export DATA_DIR=$PWD/data
export MODEL_DIR=$PWD/models
 
# Use python version from .tool-versions
use asdf
 
# Set up a Python virtual environment if not already present
layout python python3.10
 
# Install dependencies if not already installed
if ! pip list | grep -q "pandas"; then
  pip install pandas numpy matplotlib scikit-learn jupyter
fi
  1. Allow direnv:
direnv allow
  1. Create a Zellij layout:
# ~/.config/zellij/layouts/data-science.yaml
---
template:
  direction: Horizontal
  parts:
    - direction: Vertical
      borderless: true
      split_size:
        Fixed: 1
      run:
        plugin:
          location: "zellij:tab-bar"
    - direction: Horizontal
      body: true
      parts:
        - direction: Vertical
          parts:
            - direction: Horizontal
              parts:
                - command: { text: "cd ~/projects/data-science-project && vim ." }
                  focus: true
            - direction: Horizontal
              parts:
                - command: { text: "cd ~/projects/data-science-project && jupyter notebook" }
        - direction: Vertical
          ratio: 1
          parts:
            - command: { text: "cd ~/projects/data-science-project && python -i" }
            - command: { text: "cd ~/projects/data-science-project && ls -la data/" }
    - direction: Vertical
      borderless: true
      split_size:
        Fixed: 2
      run:
        plugin:
          location: "zellij:status-bar"
  1. Start the Zellij session:
zellij --session data-science --layout ~/.config/zellij/layouts/data-science.yaml

5.3 Project 3: Multi-language Microservices

For a project with multiple services in different languages:

  1. Create project directories:
mkdir -p ~/projects/microservices/{api-gateway,auth-service,user-service,notification-service}
cd ~/projects/microservices
  1. Set up asdf for each service:
# API Gateway (Node.js)
cd api-gateway
asdf local nodejs 16.14.0
touch .tool-versions
 
# Auth Service (Rust)
cd ../auth-service
asdf local rust 1.59.0
touch .tool-versions
 
# User Service (Go)
cd ../user-service
asdf local golang 1.18.0
touch .tool-versions
 
# Notification Service (Python)
cd ../notification-service
asdf local python 3.10.0
touch .tool-versions
  1. Create a main direnv configuration:
# ~/projects/microservices/.envrc
export PROJECT_ROOT=$(pwd)
export ENVIRONMENT="development"
 
# Shared environment variables
export LOG_LEVEL="debug"
export TRACING_ENABLED=true
 
# Service-specific environment variables
export API_GATEWAY_PORT=3000
export AUTH_SERVICE_PORT=3001
export USER_SERVICE_PORT=3002
export NOTIFICATION_SERVICE_PORT=3003
 
# Service URLs
export AUTH_SERVICE_URL="http://localhost:3001"
export USER_SERVICE_URL="http://localhost:3002"
export NOTIFICATION_SERVICE_URL="http://localhost:3003"
 
# Database connections
export AUTH_DB_URL="postgres://user:pass@localhost/auth_dev"
export USER_DB_URL="postgres://user:pass@localhost/user_dev"
export NOTIFICATION_DB_URL="postgres://user:pass@localhost/notification_dev"
 
# Include service-specific .envrc files if they exist
source_env_if_exists api-gateway/.envrc
source_env_if_exists auth-service/.envrc
source_env_if_exists user-service/.envrc
source_env_if_exists notification-service/.envrc
 
# Add project-level bin directory to PATH
PATH_add bin
  1. Create service-specific direnv configurations:
# ~/projects/microservices/api-gateway/.envrc
use asdf
PATH_add node_modules/.bin
 
# ~/projects/microservices/auth-service/.envrc
use asdf
PATH_add target/release
 
# ~/projects/microservices/user-service/.envrc
use asdf
PATH_add bin
 
# ~/projects/microservices/notification-service/.envrc
use asdf
layout python
  1. Allow direnv:
cd ~/projects/microservices
direnv allow
cd api-gateway
direnv allow
cd ../auth-service
direnv allow
cd ../user-service
direnv allow
cd ../notification-service
direnv allow
  1. Create a Zellij layout:
# ~/.config/zellij/layouts/microservices.yaml
---
template:
  direction: Horizontal
  parts:
    - direction: Vertical
      borderless: true
      split_size:
        Fixed: 1
      run:
        plugin:
          location: "zellij:tab-bar"
    - direction: Horizontal
      body: true
      parts:
        - direction: Vertical
          parts:
            - direction: Horizontal
              parts:
                - command: { text: "cd ~/projects/microservices && vim ." }
                  focus: true
        - direction: Vertical
          ratio: 1
          parts:
            - command: { text: "cd ~/projects/microservices/api-gateway && npm run dev" }
            - command: { text: "cd ~/projects/microservices/auth-service && cargo run" }
            - command: { text: "cd ~/projects/microservices/user-service && go run cmd/main.go" }
            - command: { text: "cd ~/projects/microservices/notification-service && python app.py" }
    - direction: Vertical
      borderless: true
      split_size:
        Fixed: 2
      run:
        plugin:
          location: "zellij:status-bar"
tabs:
  - name: "Editor"
  - name: "API Gateway"
    direction: Horizontal
    parts:
      - command: { text: "cd ~/projects/microservices/api-gateway && vim ." }
  - name: "Auth Service"
    direction: Horizontal
    parts:
      - command: { text: "cd ~/projects/microservices/auth-service && vim ." }
  - name: "User Service"
    direction: Horizontal
    parts:
      - command: { text: "cd ~/projects/microservices/user-service && vim ." }
  - name: "Notification Service"
    direction: Horizontal
    parts:
      - command: { text: "cd ~/projects/microservices/notification-service && vim ." }
  - name: "Logs"
    direction: Vertical
    parts:
      - command: { text: "cd ~/projects/microservices && docker-compose logs -f" }
  1. Start the Zellij session:
zellij --session microservices --layout ~/.config/zellij/layouts/microservices.yaml

Conclusion

Congratulations! You’ve now learned how to set up and integrate:

  1. Zellij for terminal multiplexing and workspace management
  2. asdf for managing multiple language versions
  3. direnv for environment variable management

This powerful combination allows you to:

  • Switch between projects seamlessly
  • Ensure consistent development environments
  • Automatically load project-specific configurations
  • Organize your terminal workspace effectively

Remember to explore each tool’s documentation for more advanced features:

Happy coding!