I switched from Konsole to Wezterm recently. The selling point: tabs that work like a browser. Ctrl+T for new tab, Ctrl+W to close, Ctrl+Tab to cycle. After years of muscle memory from Chrome and Firefox, having the same shortcuts in my terminal just makes sense.
Other things I like:
- GPU-accelerated rendering (fast, smooth)
- Config hot-reloads on save (no restart needed)
- Lua config file (scriptable if you want, ignorable if you don’t)
- Pane splits built in (like tmux, but native)
- Cross-platform (same config works on Linux, Mac, and Windows)
Installation
Linux (Debian/Ubuntu)
The distro packages are often outdated. Use the official repo:
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://apt.fury.io/wez/gpg.key | sudo gpg --yes --dearmor -o /etc/apt/keyrings/wezterm-fury.gpg
echo 'deb [signed-by=/etc/apt/keyrings/wezterm-fury.gpg] https://apt.fury.io/wez/ * *' | sudo tee /etc/apt/sources.list.d/wezterm.list
sudo apt update && sudo apt install -y wezterm
Also grab JetBrains Mono (excellent programming font) and Noto fonts (comprehensive Unicode coverage - prevents garbled characters when displaying web content, emojis, or international text):
sudo apt install fonts-jetbrains-mono fonts-noto fonts-noto-color-emoji
To set as default terminal:
sudo update-alternatives --install /usr/bin/x-terminal-emulator x-terminal-emulator /usr/bin/wezterm 50
sudo update-alternatives --set x-terminal-emulator /usr/bin/wezterm
To set Ctrl+Alt+T as the launch shortcut (KDE Plasma):
- Open System Settings → Keyboard → Shortcuts
- Search for “Konsole” → clear its shortcut
- Search for “WezTerm” → set its shortcut to Ctrl+Alt+T
On GNOME, use Settings → Keyboard → View and Customise Shortcuts → Custom Shortcuts, and add a new shortcut with command wezterm.
Mac
brew install --cask wezterm
JetBrains Mono:
brew tap homebrew/cask-fonts
brew install --cask font-jetbrains-mono
Windows
Download the installer from wezfurlong.org/wezterm or use winget:
winget install wez.wezterm
JetBrains Mono: download from jetbrains.com/lp/mono and install, or use Scoop:
scoop bucket add nerd-fonts
scoop install JetBrains-Mono
Config
Config location:
- Linux/Mac:
~/.config/wezterm/wezterm.lua - Windows:
C:\Users\<username>\.config\wezterm\wezterm.lua
Create the directory if it doesn’t exist.
Linux/Windows Config
local wezterm = require 'wezterm'
local config = wezterm.config_builder()
-- Appearance
config.color_scheme = 'Catppuccin Mocha'
config.window_background_opacity = 1.0
config.hide_tab_bar_if_only_one_tab = false
config.use_fancy_tab_bar = true
config.window_decorations = "RESIZE"
-- Font
config.font = wezterm.font_with_fallback {
'JetBrains Mono',
'Fira Code',
'monospace',
}
config.font_size = 12.0
-- Scrollback
config.scrollback_lines = 50000
-- Browser-like keybindings
config.keys = {
{ key = 't', mods = 'CTRL', action = wezterm.action.SpawnTab 'CurrentPaneDomain' },
{ key = 'w', mods = 'CTRL', action = wezterm.action.CloseCurrentPane { confirm = false } },
{ key = 'Tab', mods = 'CTRL', action = wezterm.action.ActivateTabRelative(1) },
{ key = 'Tab', mods = 'CTRL|SHIFT', action = wezterm.action.ActivateTabRelative(-1) },
{ key = '1', mods = 'CTRL', action = wezterm.action.ActivateTab(0) },
{ key = '2', mods = 'CTRL', action = wezterm.action.ActivateTab(1) },
{ key = '3', mods = 'CTRL', action = wezterm.action.ActivateTab(2) },
{ key = '4', mods = 'CTRL', action = wezterm.action.ActivateTab(3) },
{ key = '5', mods = 'CTRL', action = wezterm.action.ActivateTab(4) },
{ key = '6', mods = 'CTRL', action = wezterm.action.ActivateTab(5) },
{ key = '7', mods = 'CTRL', action = wezterm.action.ActivateTab(6) },
{ key = '8', mods = 'CTRL', action = wezterm.action.ActivateTab(7) },
{ key = '9', mods = 'CTRL', action = wezterm.action.ActivateTab(-1) },
{ key = 'T', mods = 'CTRL|SHIFT', action = wezterm.action.SpawnTab 'CurrentPaneDomain' },
{ key = 'e', mods = 'CTRL|SHIFT', action = wezterm.action.SplitHorizontal { domain = 'CurrentPaneDomain' } },
{ key = 'h', mods = 'CTRL|SHIFT', action = wezterm.action.SplitVertical { domain = 'CurrentPaneDomain' } },
{ key = 'w', mods = 'CTRL|SHIFT', action = wezterm.action.CloseCurrentTab { confirm = false } },
}
-- URL detection: explicit rules instead of patching default_hyperlink_rules().
-- Checked in order; first match wins.
config.hyperlink_rules = {
-- URLs wrapped in delimiters: extract the URL from inside
{ regex = [[\((\w+://\S+)\)]], format = '$1' },
{ regex = [=[\[(\w+://\S+)\]]=], format = '$1' },
{ regex = [[\{(\w+://\S+)\}]], format = '$1' },
{ regex = [[<(\w+://\S+)>]], format = '$1' },
-- Bare URLs: balanced-paren matching keeps Wikipedia-style URLs intact
-- but strips unbalanced trailing ) from (https://example.com)
{ regex = [=[\b\w+://(?:[^\s()]+|\([^\s()]*\))+(?<![),;:!?'">\]])]=], format = '$0' },
-- Email addresses
{ regex = [[\b\w+@[\w-]+(\.[\w-]+)+\b]], format = 'mailto:$0' },
}
-- Copy on select + click URLs without modifier + smaller scroll increment
config.mouse_bindings = {
{ event = { Up = { streak = 1, button = 'Left' } }, mods = 'NONE', action = wezterm.action.CompleteSelection 'ClipboardAndPrimarySelection' },
{ event = { Up = { streak = 1, button = 'Left' } }, mods = 'NONE', action = wezterm.action.OpenLinkAtMouseCursor, mouse_reporting = false },
{ event = { Down = { streak = 1, button = { WheelUp = 1 } } }, mods = 'NONE', action = wezterm.action.ScrollByLine(-2) },
{ event = { Down = { streak = 1, button = { WheelDown = 1 } } }, mods = 'NONE', action = wezterm.action.ScrollByLine(2) },
}
return config
Mac Config
Same thing, but with Cmd instead of Ctrl (and slightly larger font for Retina):
local wezterm = require 'wezterm'
local config = wezterm.config_builder()
-- Appearance
config.color_scheme = 'Catppuccin Mocha'
config.window_background_opacity = 1.0
config.hide_tab_bar_if_only_one_tab = false
config.use_fancy_tab_bar = true
config.window_decorations = "RESIZE"
-- Font
config.font = wezterm.font_with_fallback {
'JetBrains Mono',
'Fira Code',
'monospace',
}
config.font_size = 13.0
-- Scrollback
config.scrollback_lines = 10000
-- Browser-like keybindings (Cmd on Mac)
config.keys = {
{ key = 't', mods = 'CMD', action = wezterm.action.SpawnTab 'CurrentPaneDomain' },
{ key = 'w', mods = 'CMD', action = wezterm.action.CloseCurrentPane { confirm = false } },
{ key = 'Tab', mods = 'CTRL', action = wezterm.action.ActivateTabRelative(1) },
{ key = 'Tab', mods = 'CTRL|SHIFT', action = wezterm.action.ActivateTabRelative(-1) },
{ key = '1', mods = 'CMD', action = wezterm.action.ActivateTab(0) },
{ key = '2', mods = 'CMD', action = wezterm.action.ActivateTab(1) },
{ key = '3', mods = 'CMD', action = wezterm.action.ActivateTab(2) },
{ key = '4', mods = 'CMD', action = wezterm.action.ActivateTab(3) },
{ key = '5', mods = 'CMD', action = wezterm.action.ActivateTab(4) },
{ key = '6', mods = 'CMD', action = wezterm.action.ActivateTab(5) },
{ key = '7', mods = 'CMD', action = wezterm.action.ActivateTab(6) },
{ key = '8', mods = 'CMD', action = wezterm.action.ActivateTab(7) },
{ key = '9', mods = 'CMD', action = wezterm.action.ActivateTab(-1) },
{ key = 'T', mods = 'CMD|SHIFT', action = wezterm.action.SpawnTab 'CurrentPaneDomain' },
{ key = 'e', mods = 'CMD|SHIFT', action = wezterm.action.SplitHorizontal { domain = 'CurrentPaneDomain' } },
{ key = 'h', mods = 'CMD|SHIFT', action = wezterm.action.SplitVertical { domain = 'CurrentPaneDomain' } },
{ key = 'w', mods = 'CMD|SHIFT', action = wezterm.action.CloseCurrentTab { confirm = false } },
}
-- URL detection: explicit rules (see Linux config for detailed comments)
config.hyperlink_rules = {
{ regex = [[\((\w+://\S+)\)]], format = '$1' },
{ regex = [=[\[(\w+://\S+)\]]=], format = '$1' },
{ regex = [[\{(\w+://\S+)\}]], format = '$1' },
{ regex = [[<(\w+://\S+)>]], format = '$1' },
{ regex = [=[\b\w+://(?:[^\s()]+|\([^\s()]*\))+(?<![),;:!?'">\]])]=], format = '$0' },
{ regex = [[\b\w+@[\w-]+(\.[\w-]+)+\b]], format = 'mailto:$0' },
}
-- Mouse bindings (copy on select, click URLs, smaller scroll)
config.mouse_bindings = {
{ event = { Up = { streak = 1, button = 'Left' } }, mods = 'NONE', action = wezterm.action.CompleteSelection 'ClipboardAndPrimarySelection' },
{ event = { Up = { streak = 1, button = 'Left' } }, mods = 'NONE', action = wezterm.action.OpenLinkAtMouseCursor, mouse_reporting = false },
{ event = { Down = { streak = 1, button = { WheelUp = 1 } } }, mods = 'NONE', action = wezterm.action.ScrollByLine(-2) },
{ event = { Down = { streak = 1, button = { WheelDown = 1 } } }, mods = 'NONE', action = wezterm.action.ScrollByLine(2) },
}
return config
Keybindings Summary
| Action | Linux/Windows | Mac |
|---|---|---|
| New tab | Ctrl+T | Cmd+T |
| Close pane | Ctrl+W | Cmd+W |
| Close tab | Ctrl+Shift+W | Cmd+Shift+W |
| Next tab | Ctrl+Tab | Ctrl+Tab |
| Previous tab | Ctrl+Shift+Tab | Ctrl+Shift+Tab |
| Jump to tab N | Ctrl+1-9 | Cmd+1-9 |
| Split horizontal | Ctrl+Shift+E | Cmd+Shift+E |
| Split vertical | Ctrl+Shift+H | Cmd+Shift+H |
Notes
Tab bar as title bar: The config uses window_decorations = "RESIZE" which removes the traditional title bar. The tab bar is always visible and acts as a draggable surface for moving the window.
Panes vs tabs: Tabs are separate terminal sessions. Panes are splits within a single tab—useful for seeing two things at once (e.g., editor and logs) without switching tabs.
Copy on select: Selecting text with the mouse automatically copies it to the clipboard. Standard on Linux, less common on Mac, but I find it useful everywhere.
Clickable URLs: Single-click on any URL to open it in your browser. No modifier key needed.
URL detection fix: WezTerm’s default bare-URL rule includes ) as a valid trailing character. This means if a tool outputs a URL in parentheses—like (https://example.com/page)—clicking the link opens https://example.com/page) which 404s. The config above defines all hyperlink rules explicitly rather than patching default_hyperlink_rules() by index (which breaks if WezTerm changes the default rule order). Rules 1-4 extract URLs from delimiter pairs, rule 5 is a balanced-paren-aware bare-URL matcher, and rule 6 makes email addresses clickable as mailto: links.
Scroll speed: Config sets scroll to 2 lines per wheel tick (default is 3). Adjust the -2 and 2 values if you want faster/slower.
Hot reload: Save the config file and changes apply immediately. No restart required.
Claude Code Status Line
If you use Claude Code in WezTerm, you can add a custom status line that shows context window usage with traffic-light colouring—green when you have plenty of room, yellow when you should start wrapping up, red when you need to park the session.
The display looks like: Ctx: 50k (5%) | Model: Opus 4.6 (1M context) | Session: 16m
The colour of the context indicator changes based on usage:
- Green (<30%): safe working zone
- Yellow (30–50%): start wrapping up
- Red (>50%): park now, before the auto-compaction eats your context
Quick setup
Claude Code has a built-in /statusline command that generates a status line script and configures settings for you:
/statusline show model name and context percentage with traffic light colors
This creates the script, makes it executable, and updates your settings.json automatically. Works on Linux, Mac, and Windows.
Manual setup
If you want full control over the script, or want to understand what /statusline generates:
Claude Code has a built-in statusLine setting that pipes session data (model, context usage, duration) as JSON to any script you point it at. The script below parses that JSON and outputs a colour-coded status line.
Requires jq for JSON parsing:
# Linux (Debian/Ubuntu)
sudo apt install jq
# Mac
brew install jq
# Windows (via Scoop)
scoop install jq
# Windows (via Chocolatey)
choco install jq
Create the script at ~/.claude/statusline.sh:
#!/bin/bash
# Claude Code status line with traffic-light context display
# Green (<30%), Yellow (30-50%), Red (>50%)
input=$(cat)
MODEL=$(echo "$input" | jq -r '.model.display_name // .model // "Claude"')
PERCENT=$(echo "$input" | jq -r '.context_window.used_percentage // 0')
TOTAL=$(echo "$input" | jq -r '.context_window.context_window_size // 200000')
DURATION_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
# Calculate used tokens from percentage
USED=$((TOTAL * PERCENT / 100))
# Format used tokens (e.g., 50k, 150k)
if [ "$USED" -ge 1000 ]; then
USED_FMT=$(awk "BEGIN {printf \"%.0fk\", $USED/1000}")
else
USED_FMT="$USED"
fi
# Format session duration
SESSION_SECS=$((DURATION_MS / 1000))
if [ "$SESSION_SECS" -lt 60 ]; then
DURATION="<1m"
elif [ "$SESSION_SECS" -lt 3600 ]; then
DURATION="$((SESSION_SECS / 60))m"
else
DURATION="$((SESSION_SECS / 3600))h$((SESSION_SECS % 3600 / 60))m"
fi
# Traffic-light colour coding
if [ "$PERCENT" -ge 50 ]; then
CTX="\033[31m${USED_FMT} (${PERCENT}%)\033[0m" # Red
elif [ "$PERCENT" -ge 30 ]; then
CTX="\033[33m${USED_FMT} (${PERCENT}%)\033[0m" # Yellow
else
CTX="\033[32m${USED_FMT} (${PERCENT}%)\033[0m" # Green
fi
echo -e "Ctx: ${CTX} | Model: ${MODEL} | Session: ${DURATION}"
Make it executable (Linux/Mac only, not needed on Windows):
chmod +x ~/.claude/statusline.sh
Then add it to ~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "/absolute/path/to/.claude/statusline.sh"
}
}
Replace the path with your actual home directory (/home/username/... on Linux, /Users/username/... on Mac, C:/Users/username/... on Windows). JSON doesn’t expand ~ or $HOME, so use the full absolute path.
Windows note: Claude Code runs status line commands through Git Bash, so the .sh script works on Windows without modification. Just make sure jq is installed and available on your PATH. Use forward slashes in the path (e.g., C:/Users/billy/.claude/statusline.sh).
Why the thresholds?
Claude Code auto-compacts the conversation when the context window fills up. Compaction loses detail—tool outputs get summarised, earlier reasoning gets trimmed. The traffic light gives you a glanceable signal to wrap up and save state before compaction happens, rather than discovering mid-thought that half your context just disappeared.