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:
- Zellij: A terminal workspace with panes, tabs, and session management
- asdf: A universal version manager for multiple languages and tools
- 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
- 2. Setting Up asdf
- 3. Setting Up direnv
- 4. Integrating All Three Tools
- 5. Practical Projects
1. Setting Up Zellij
1.1 Installation
macOS
brew install zellijUbuntu/Debian
curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/zellij-org/zellij/master/assets/install.sh | bashArch Linux
pacman -S zellijVerify Installation
zellij --version1.2 Basic Navigation
Zellij uses a modal system for keybindings, similar to Vim. Here are the essential keybindings:
Ctrl+p: Enter pane modeCtrl+t: Enter tab modeCtrl+h: Enter resize modeCtrl+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 paned: Close focused panef: Toggle floating panez: Toggle pane zoom
Tab Mode (Ctrl+t)
n: Create new tabx: Close current tab1-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/zellijCreate 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.yaml1.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-projectTo attach to an existing session:
zellij attach my-projectList all active sessions:
zellij list-sessions2. Setting Up asdf
2.1 Installation
macOS
brew install asdfAdd 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" >> ~/.bashrcFor zsh:
echo -e "\n. $(brew --prefix asdf)/libexec/asdf.sh" >> ~/.zshrcLinux
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.10.2Add to your shell (for bash):
echo -e "\n. $HOME/.asdf/asdf.sh" >> ~/.bashrc
echo -e "\n. $HOME/.asdf/completions/asdf.bash" >> ~/.bashrcFor zsh:
echo -e "\n. $HOME/.asdf/asdf.sh" >> ~/.zshrc2.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.gitList installed plugins:
asdf plugin list2.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.0List installed versions:
asdf list nodejs2.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.0Set a local version (for a specific project):
cd ~/projects/my-project
asdf local nodejs 14.19.0
asdf local python 3.9.10This 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.03. Setting Up direnv
3.1 Installation
macOS
brew install direnvUbuntu/Debian
sudo apt-get update
sudo apt-get install direnvShell Integration
For bash:
echo 'eval "$(direnv hook bash)"' >> ~/.bashrcFor zsh:
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc3.2 Basic Configuration
Create a global configuration:
mkdir -p ~/.config/direnv
touch ~/.config/direnv/direnvrcExample 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 binAllow the .envrc file:
cd ~/projects/my-project
direnv allowNow, 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.localIntegrating with asdf:
# ~/projects/my-project/.envrc
use asdfThis 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:
- Create a new project directory:
mkdir ~/projects/new-project
cd ~/projects/new-project- Initialize version control:
git init- Set up asdf local versions:
asdf local nodejs 16.14.0
asdf local python 3.10.0- 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- Allow direnv:
direnv allow- 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"- Start a new Zellij session with this layout:
zellij --session new-project --layout ~/.config/zellij/layouts/new-project.yaml4.2 Development Workflow
With this setup, your daily workflow becomes:
- Start or attach to your Zellij session:
zellij attach new-project- Navigate to your project directory (direnv automatically loads environments):
cd ~/projects/new-project-
Work across multiple panes in Zellij:
- Code editor
- Running server
- Git operations
- Tests
-
Switch projects seamlessly:
# In a new pane:
cd ~/projects/another-project
# direnv automatically switches environment variables and tool versions4.3 Team Collaboration
For team collaboration:
-
Share asdf configuration:
- Commit
.tool-versionsto version control
- Commit
-
Share direnv template:
- Create a
.envrc.examplefile with template values - Add to version control, but keep the actual
.envrcin.gitignore
- Create a
-
Share Zellij layout:
- Add project layout to version control in a
zellij-layoutsdirectory
- Add project layout to version control in a
Example onboarding instructions for team members:
# Project Setup
1. Clone the repository:
```bash
git clone git@github.com:organization/new-project.git
cd new-project-
Install required tool versions (requires asdf):
# asdf will read the .tool-versions file asdf install -
Set up environment:
cp .envrc.example .envrc # Edit .envrc with your local values direnv allow -
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
- Set up asdf:
asdf local nodejs 18.12.0- 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- Allow direnv:
direnv allow- 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"- Initialize the Next.js project:
npx create-next-app@latest .- Start the Zellij session:
zellij --session nextjs-app --layout ~/.config/zellij/layouts/nextjs.yaml5.2 Project 2: Data Science Environment
- Create a project directory:
mkdir -p ~/projects/data-science-project
cd ~/projects/data-science-project- Set up asdf:
asdf local python 3.10.4- 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- Allow direnv:
direnv allow- 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"- Start the Zellij session:
zellij --session data-science --layout ~/.config/zellij/layouts/data-science.yaml5.3 Project 3: Multi-language Microservices
For a project with multiple services in different languages:
- Create project directories:
mkdir -p ~/projects/microservices/{api-gateway,auth-service,user-service,notification-service}
cd ~/projects/microservices- 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- 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- 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- 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- 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" }- Start the Zellij session:
zellij --session microservices --layout ~/.config/zellij/layouts/microservices.yamlConclusion
Congratulations! You’ve now learned how to set up and integrate:
- Zellij for terminal multiplexing and workspace management
- asdf for managing multiple language versions
- 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!