Skip to content

Instantly share code, notes, and snippets.

@sverrejoh
Last active May 20, 2026 20:31
Show Gist options
  • Select an option

  • Save sverrejoh/42742b2522c9ce07900ac7e5a7a5f1ac to your computer and use it in GitHub Desktop.

Select an option

Save sverrejoh/42742b2522c9ce07900ac7e5a7a5f1ac to your computer and use it in GitHub Desktop.
Hunting tmux sessions with fzf — Tokyo Night, top status bar, fzf session-tree + sessionizer popups

Hunting tmux sessions with fzf

fzf session-tree picker

I have 22 tmux sessions open right now. That's not unusual — each project, each side-quest, each "let me just poke at this for a minute" gets its own session and never quite leaves. The only way that scales is if jumping between them is faster than thinking about it.

So the centerpiece of this config is a pair of fzf-driven popups: C-s s to hunt an existing session or window, and C-s f to spin up a new one from any directory under ~. Everything else — the Tokyo Night theme, the rainbow window tabs, the system stats in the status bar — is decoration around those two bindings.

The config has no plugins for these. Just a small shell script each, ~50 and ~100 lines. The rest is plain tmux.

If you copy this verbatim: prefix is C-s (not C-b), you'll need fzf + fd + a Nerd Font, and the rainbow window tabs only light up if your terminal does true colour.


C-s s — the session/window tree

session-tree picker

Like choose-tree but with fzf underneath, so fuzzy filter works out of the box. Three things that are not obvious from the screenshot:

  • Tab expands or collapses a session to reveal its windows. The state is tracked in /tmp/tmux-tree-state for the duration of the picker, then thrown away.
  • Alt+0-9 and Alt+a-z jump to a row by position. With 20+ sessions the keyboard shortcut beats scrolling.
  • The current session is pinned at the top of the list on open (load:pos(N)+unbind(load)) and marked with a *. After that fzf sorts normally by match score.

Source: session-tree.sh — wired up in tmux.conf as:

bind-key s display-popup -w 90% -h 75% -E "~/.config/tmux/session-tree.sh"

C-s f — the directory sessionizer

sessionizer picker

Pick a directory under ~, get a session for it. If one already exists for that directory (matched by basename), switch to it instead of creating a duplicate. Tab drills into a subdirectory, Shift-Tab pops up to the parent — so this works as a lightweight file-tree-shaped session launcher.

Two opinions baked in:

  • Basenames as session names, with dots replaced by underscores (tmux dislikes dots in -t targets). That means ~/Projects/my.tool becomes session my_tool.
  • No "fuzzy across all directories". Started from ~ and drilled with Tab. Recursive fd across ~ finds too many build/.git directories and the results stop being useful.

Source: sessionizer.sh.

The status bar

status bar

Top-positioned, Tokyo Night palette, pill-shaped session badge on the left, CPU/RAM/disk/host/clock on the right, rainbow window tabs in the middle. The icons are Nerd Font symbols rendered with the colour of each window's index.

C-s B toggles between this full bar and a one-character minimal bar — useful when sharing the screen or recording.


Other things worth knowing

  • Prefix is C-s. I've used it for years; C-b collides with emacs backward-char and C-a with shell line-start. C-s is emacs isearch — which I do use constantly — but C-s C-s is bound to send-prefix, so the prefix forwards through to the inner program. Same trick works for C-s in the shell.
  • Vim-style pane navigation (h/j/k/l) under prefix, no repeat. I prefer the explicit re-prefix to fighting with timers.
  • ( / ) cycle sessions in creation order. Repeatable (-r), so C-s ) ) ) walks forward three sessions. Faster than the picker when you know the target is "one or two over".
  • renumber-windows on + base-index 0 — when I close a window, the others compact. The rainbow colours are keyed off index, so things stay visually stable.
  • mode-keys emacs — copy mode with C-space to mark, M-w/Enter to copy. Vim-mode users, sorry.

Dependencies

Thing Why
tmux 3.3+ display-popup and pos() in fzf bindings
fzf 0.44+ transform-header, pos(), --cycle, --no-separator
fd the sessionizer uses it to list directories
A Nerd Font status bar icons, expand/collapse glyphs
True-colour terminal the rainbow tabs need 24-bit colour
TPM plugin manager

Plugins (installed by TPM, see tmux.conf):

  • tmux-plugins/tmux-sensible — boring sane defaults
  • omerxx/tmux-sessionx — alternative session picker, kept around for comparison; the homegrown session-tree.sh is what's bound to C-s s
  • tmux-plugins/tmux-cpu — CPU percentage helper for the status bar

Install

# TPM
git clone https://github.com/tmux-plugins/tpm \
  ~/.config/tmux/plugins/tpm

# Drop these files in
mkdir -p ~/.config/tmux
cp tmux.conf ~/.config/tmux/tmux.conf
cp *.sh ~/.config/tmux/
chmod +x ~/.config/tmux/*.sh

# Start tmux, then install plugins with C-s I (capital I)
tmux

Keybindings reference

Key Action
C-s Prefix
C-s C-s Send literal C-s
C-s r Reload config
C-s s fzf session/window tree
C-s f fzf sessionizer (new session from dir)
C-s S Built-in choose-tree (fallback)
C-s h/j/k/l Move between panes
C-s | / C-s - Split horizontal / vertical
C-s n / C-s p Next / previous window (repeatable)
C-s ( / C-s ) Previous / next session (repeatable)
C-s N New session, prompts for name
C-s X Kill current session, switch away first
C-s B Toggle full / minimal status bar
C-s M Toggle mouse mode (for iPad selection)
C-s [ Copy mode (Emacs keys)
#!/bin/bash
# Get disk space for root: used/total in TB
df -BG / | awk 'NR==2 {gsub("G","",$2); gsub("G","",$3); printf "%.1fTB/%.1fTB", $3/1024, $2/1024}'
#!/bin/bash
# Kill a tmux session after switching away from it
session="$1"
tmux switch-client -l 2>/dev/null || tmux switch-client -n 2>/dev/null
tmux kill-session -t "$session"
#!/bin/sh
# Called by tmux status-left via #() - outputs mouse state icon
mouse=$(tmux show -gv mouse)
if [ "$mouse" = "on" ]; then
printf '\U000f037d'
else
printf '\U000f037e'
fi
#!/bin/bash
# Get memory in GB: used/total
read -r total used <<< $(free -g | awk '/^Mem:/ {print $2, $3}')
printf "%dGB/%dGB" "$used" "$total"
#!/bin/bash
sessions=($(tmux list-sessions -F '#{session_created}:#{session_name}' | sort -n | cut -d: -f2))
current=$(tmux display-message -p '#{session_name}')
count=${#sessions[@]}
for i in "${!sessions[@]}"; do
if [[ "${sessions[$i]}" == "$current" ]]; then
next=$(( (i + 1) % count ))
tmux switch-client -t "${sessions[$next]}"
exit
fi
done
#!/bin/bash
sessions=($(tmux list-sessions -F '#{session_created}:#{session_name}' | sort -n | cut -d: -f2))
current=$(tmux display-message -p '#{session_name}')
count=${#sessions[@]}
for i in "${!sessions[@]}"; do
if [[ "${sessions[$i]}" == "$current" ]]; then
prev=$(( (i - 1 + count) % count ))
tmux switch-client -t "${sessions[$prev]}"
exit
fi
done
#!/usr/bin/env bash
# fzf-based session/window tree with expand/collapse
# Tab: expand/collapse, Enter: switch, Alt+0-9/a-z: jump to item
STATE="/tmp/tmux-tree-state"
SELF="$HOME/.config/tmux/session-tree.sh"
IC=$'\uf07b'
IE=$'\uf07c'
LABELS=(0 1 2 3 4 5 6 7 8 9 a b c d e f g h i j k l m n o p q r s t u v w x y z)
JUMPS='alt-0:pos(1)+accept,alt-1:pos(2)+accept,alt-2:pos(3)+accept,alt-3:pos(4)+accept,alt-4:pos(5)+accept,alt-5:pos(6)+accept,alt-6:pos(7)+accept,alt-7:pos(8)+accept,alt-8:pos(9)+accept,alt-9:pos(10)+accept,alt-a:pos(11)+accept,alt-b:pos(12)+accept,alt-c:pos(13)+accept,alt-d:pos(14)+accept,alt-e:pos(15)+accept,alt-f:pos(16)+accept,alt-g:pos(17)+accept,alt-h:pos(18)+accept,alt-i:pos(19)+accept,alt-j:pos(20)+accept,alt-k:pos(21)+accept,alt-l:pos(22)+accept,alt-m:pos(23)+accept,alt-n:pos(24)+accept,alt-o:pos(25)+accept,alt-p:pos(26)+accept,alt-q:pos(27)+accept,alt-r:pos(28)+accept,alt-s:pos(29)+accept,alt-t:pos(30)+accept,alt-u:pos(31)+accept,alt-v:pos(32)+accept,alt-w:pos(33)+accept,alt-x:pos(34)+accept,alt-y:pos(35)+accept,alt-z:pos(36)+accept'
gen_list() {
local query="$1" cur="$2" idx=0
local exp=""
[[ -s "$STATE" ]] && exp=$'\n'$(<"$STATE")$'\n'
local prev="" show=false is_exp=false
while IFS='|' read -r _ sname wcount widx wname wflag; do
if [[ "$sname" != "$prev" ]]; then
prev="$sname"
show=true
if [[ -n "$query" ]]; then
local lower="${sname,,}"
for word in $query; do
[[ "$lower" == *"${word,,}"* ]] || { show=false; break; }
done
fi
is_exp=false
[[ "$exp" == *$'\n'"$sname"$'\n'* ]] && is_exp=true
if $show; then
local icon lbl="${LABELS[$idx]:- }" style='' rstyle='' star=''
$is_exp && icon="$IE" || icon="$IC"
if [[ "$sname" = "$cur" ]]; then
style=$'\033[1;34m' rstyle=$'\033[0m' star=' *'
fi
printf '%s\t\033[2m%s\033[0m %s%s %s (%d windows)%s%s\n' \
"$sname" "$lbl" "$style" "$icon" "$sname" "$wcount" "$star" "$rstyle"
idx=$((idx + 1))
fi
fi
if $show && $is_exp; then
printf '%s:%s\t\033[2m%s\033[0m %s: %s %s\n' \
"$sname" "$widx" "${LABELS[$idx]:- }" "$widx" "$wname" "$wflag"
idx=$((idx + 1))
fi
done < <(tmux list-windows -a \
-F '#{session_id}|#S|#{session_windows}|#{window_index}|#{window_name}|#{?window_active,*,}' \
| sort -t'$' -k2 -n)
}
case "${1:-}" in
toggle)
session="${2%%:*}"
if [[ -s "$STATE" ]]; then
found=false new=""
while IFS= read -r line; do
if [[ "$line" = "$session" ]]; then
found=true
else
new+="$line"$'\n'
fi
done < "$STATE"
if $found; then
printf '%s' "$new" > "$STATE"
else
echo "$session" >> "$STATE"
fi
else
echo "$session" > "$STATE"
fi
gen_list "$3" "$4"
;;
list)
gen_list "$2" "$3"
;;
*)
current=$(tmux display-message -p '#S')
: > "$STATE"
current_pos=$(tmux list-sessions -F '#{session_id}|#S' | \
sort -t'$' -k2 -n | \
awk -F'|' -v cur="$current" '{if ($2 == cur) {print NR; exit}}')
result=$(gen_list "" "$current" | fzf --reverse --ansi --disabled \
--cycle --no-separator --no-scrollbar \
--delimiter=$'\t' --with-nth=2 \
--header='tab: expand/collapse | alt+0-9/a-z: jump' \
--bind "load:pos(${current_pos:-1})+unbind(load)" \
--bind "change:reload($SELF list {q} $current)" \
--bind "tab:reload($SELF toggle {1} {q} $current)" \
--bind "$JUMPS")
[[ -n "$result" ]] && tmux switch-client -t "${result%%$'\t'*}"
rm -f "$STATE"
;;
esac
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
#!/usr/bin/env bash
# Tmux sessionizer — browse directories, create/switch to a session
# Tab: drill into dir | Shift-Tab: go to parent | Enter: create session here
SELF="$HOME/.config/tmux/sessionizer.sh"
STATE=/tmp/tmux-sessionizer-cwd
list_dirs() {
local dir="$1"
fd --type d --max-depth 1 . "$dir" 2>/dev/null | sort
}
case "${1:-}" in
--into)
selected="$2"
if [[ -d "$selected" ]]; then
printf '%s' "$selected" > "$STATE"
list_dirs "$selected"
fi
;;
--up)
dir=$(<"$STATE")
parent=$(dirname "$dir")
printf '%s' "$parent" > "$STATE"
list_dirs "$parent"
;;
--header)
cat "$STATE"
;;
*)
start="$HOME"
printf '%s' "$start" > "$STATE"
dir=$(list_dirs "$start" | fzf \
--reverse --no-separator --no-scrollbar \
--header "$start" \
--prompt 'filter: ' \
--bind "tab:reload(bash $SELF --into {})+clear-query+transform-header(bash $SELF --header)" \
--bind "shift-tab:reload(bash $SELF --up)+clear-query+transform-header(bash $SELF --header)" \
--preview 'ls -1p --color=always {}' \
--preview-window 'right:40%')
[[ -z "$dir" ]] && exit 0
dir=$(cd "$dir" && pwd)
# Session name from basename, dots → underscores (tmux dislikes dots)
name=$(basename "$dir" | tr '.' '_')
# Switch to existing session or create new one
if tmux has-session -t "=$name" 2>/dev/null; then
tmux switch-client -t "=$name"
else
tmux new-session -d -s "$name" -c "$dir"
tmux switch-client -t "=$name"
fi
;;
esac
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
unbind r
bind r source-file ~/.config/tmux/tmux.conf
set -g prefix C-s
set -g prefix2 None
unbind C-b
set -g mouse on
set -gq allow-passthrough on
set -g bell-action any
set -g monitor-bell on
set -g default-terminal "tmux-256color"
set -as terminal-features ',*:RGB'
set -g history-limit 100000
set -sg escape-time 0
set -g focus-events on
set -g set-clipboard on
set -g display-time 2000
bind-key h select-pane -L
bind-key j select-pane -D
bind-key k select-pane -U
bind-key l select-pane -R
# Easier splits (preserves working directory)
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
set-option -g status-position top
set -g base-index 0
setw -g pane-base-index 0
set -g renumber-windows on
# Emacs-style copy mode
set -g mode-keys emacs
bind -T copy-mode Enter send -X copy-selection-and-cancel
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-sensible'
set -g @plugin 'omerxx/tmux-sessionx'
set -g @plugin 'tmux-plugins/tmux-cpu'
# Minimal theme
set -g status-style bg='#1a1b26',fg='#565f89'
set -g status-interval 2
set -g status-left-length 50
set -g status-right-length 400
# Status left/right
set -g status-left '#[fg=#1a1b26,bg=#7aa2f7,bold]  #S #[fg=#7aa2f7,bg=default,nobold] #(~/.config/tmux/mouse-indicator.sh) '
set -g status-right '#[fg=#9ece6a]󰻠 #{cpu_percentage} #[fg=#bb9af7]󰘚 #(~/.config/tmux/ram-usage.sh) #[fg=#ff9e64]󰋊 #(~/.config/tmux/disk-usage.sh) #[fg=#c0caf5]󰌢 #H #[fg=#7aa2f7]#[fg=#1a1b26,bg=#7aa2f7,bold] 󰥔 %H:%M '
# Window list
set -g window-status-format '#[fg=#3b4261]#[bg=default]#[fg=#565f89]#[bg=#3b4261]#{?#{==:#I,0},󰼎,}#{?#{==:#I,1},󰼏,}#{?#{==:#I,2},󰼐,}#{?#{==:#I,3},󰼑,}#{?#{==:#I,4},󰼒,}#{?#{==:#I,5},󰼓,}#{?#{==:#I,6},󰼔,}#{?#{==:#I,7},󰼕,}#{?#{==:#I,8},󰼖,}#{?#{==:#I,9},󰼗,} #W#[fg=#3b4261]#[bg=default]'
set -g window-status-current-format '#[fg=#7aa2f7]#{?#{==:#I,0},#[fg=#7aa2f7],}#{?#{==:#I,1},#[fg=#9ece6a],}#{?#{==:#I,2},#[fg=#bb9af7],}#{?#{==:#I,3},#[fg=#ff9e64],}#{?#{==:#I,4},#[fg=#73daca],}#{?#{==:#I,5},#[fg=#f7768e],}#{?#{==:#I,6},#[fg=#2ac3de],}#{?#{==:#I,7},#[fg=#e0af68],}#{?#{==:#I,8},#[fg=#a9b1d6],}#{?#{==:#I,9},#[fg=#d4bfff],}#[bg=default]#[fg=#1a1b26]#[bg=#7aa2f7]#{?#{==:#I,0},#[bg=#7aa2f7],}#{?#{==:#I,1},#[bg=#9ece6a],}#{?#{==:#I,2},#[bg=#bb9af7],}#{?#{==:#I,3},#[bg=#ff9e64],}#{?#{==:#I,4},#[bg=#73daca],}#{?#{==:#I,5},#[bg=#f7768e],}#{?#{==:#I,6},#[bg=#2ac3de],}#{?#{==:#I,7},#[bg=#e0af68],}#{?#{==:#I,8},#[bg=#a9b1d6],}#{?#{==:#I,9},#[bg=#d4bfff],}#[bold]#{?#{==:#I,0},󰼎,}#{?#{==:#I,1},󰼏,}#{?#{==:#I,2},󰼐,}#{?#{==:#I,3},󰼑,}#{?#{==:#I,4},󰼒,}#{?#{==:#I,5},󰼓,}#{?#{==:#I,6},󰼔,}#{?#{==:#I,7},󰼕,}#{?#{==:#I,8},󰼖,}#{?#{==:#I,9},󰼗,} #W#[nobold]#[fg=#7aa2f7]#{?#{==:#I,0},#[fg=#7aa2f7],}#{?#{==:#I,1},#[fg=#9ece6a],}#{?#{==:#I,2},#[fg=#bb9af7],}#{?#{==:#I,3},#[fg=#ff9e64],}#{?#{==:#I,4},#[fg=#73daca],}#{?#{==:#I,5},#[fg=#f7768e],}#{?#{==:#I,6},#[fg=#2ac3de],}#{?#{==:#I,7},#[fg=#e0af68],}#{?#{==:#I,8},#[fg=#a9b1d6],}#{?#{==:#I,9},#[fg=#d4bfff],}#[bg=default]'
set -g window-status-separator ' '
# Pane borders
set -g pane-border-style fg='#3b4261'
set -g pane-active-border-style fg='#7aa2f7'
# Message style
set -g message-style bg='#7aa2f7',fg='#1a1b26'
set -g message-command-style bg='#292e42',fg='#7aa2f7'
# Tree/choose mode styling
run '~/.config/tmux/plugins/tpm/tpm'
bind-key -T root MouseDown1StatusLeft choose-tree -Zs
# Repeatable session cycling (C-s ( ( ( or C-s ) ) ))
# Longer repeat time for key sequences
set -g repeat-time 1000
# Repeatable window cycling (C-s n n n or C-s p p p)
bind-key -r n next-window
bind-key -r p previous-window
# Toggle status bar (C-s B for "bar")
bind-key B run-shell "~/.config/tmux/toggle-status.sh"
# Session/window tree picker popup (C-s s) — like choose-tree but with fzf
bind-key s display-popup -w 90% -h 75% -E "~/.config/tmux/session-tree.sh"
# Built-in choose-tree (C-s S)
bind-key S choose-tree -Zs
# Toggle mouse mode (C-s M) - turn off to select/copy text on iPad
bind M set -g mouse
# Drop-down split pane (C-s D) — pin anything you want at the top:
# htop, lazygit, a log tail, a chat agent…
# bind-key D split-window -vb -l 40% "your-command-here"
# New session with name prompt (C-s N)
bind-key N command-prompt -p "New session name:" "new-session -s '%%'"
# Kill session and switch away (C-s X)
bind-key X confirm-before -p "Kill session #S? (y/n)" \
"run-shell '~/.config/tmux/kill-session.sh #S'"
# Sessionizer — pick a directory, create/switch to session (C-s f)
bind-key f display-popup -w 80% -h 70% -E "bash ~/.config/tmux/sessionizer.sh"
# C-s C-s sends C-s to terminal (override plugin bindings)
bind C-s send-prefix
#!/bin/bash
# Toggle between full and minimal status bar
FULL='#[fg=#9ece6a]󰻠 #(~/.config/tmux/plugins/tmux-cpu/scripts/cpu_percentage.sh) #[fg=#bb9af7]󰘚 #(~/.config/tmux/ram-usage.sh) #[fg=#ff9e64]󰋊 #(~/.config/tmux/disk-usage.sh) #[fg=#c0caf5]󰌢 #H #[fg=#7aa2f7]󰥔 %H:%M '
MINI='#[fg=#565f89]󰁌 '
current=$(tmux show-option -gqv @status-mode)
if [ "$current" = "mini" ]; then
tmux set -g status-right "$FULL"
tmux set -g @status-mode "full"
else
tmux set -g status-right "$MINI"
tmux set -g @status-mode "mini"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment