Tmux go brr - buffered tmux sessions

2024-09-23

druskus

I have recently re-visited my zsh and tmux configs, after I finally decided to try powerlevel10k (which is amazing, but that's another story). The point is, now my shell is super fast. And tmux has become the bottleneck. You see, when you start a tmux session, there is a ~1s of delay before you see your prompt. I have a solution for this now, but first, let me sell you on tmux.

# Tmux

tmux is both great and horrible. It is awesome because it allows me to have long-running shell sessions, tabs, etc. It is especially amazing if you have to SSH into a workstation server or any remote machine constantly. It is however dated, and I should probably start thinking about replacing it with zellij. Tmux's config is generally a bunch of shell script code put together, flexible enough to do most things you want, but annoying as hell to write and maintain. There are a lot of bad defaults and a lot of vestigial features. Could be worse, could be yaml. I digress.

My general workflow (at least for my local machine) usually involves a couple of "named sessions" and an "ephemeral session" per terminal I open.

  1. Ephemeral sessions should be destroyed as soon as the terminal is closed. They are named either "<number>" (for fallback) or "tmux-<number>".
  2. Named sessions should persist - even across reboots (see this and this).

# A solution

One way to achieve #1 is with a client-detached hook.

set-option -g destroy-unattached on

# or also
# set-hook -g client-detached kill-session

This will kill any session whenever a client detaches. There are two obvious problems:

  1. What happens if more than one client is attached?
  2. It does not solve #2.

My solution is instead, the following.

set-hook -g client-detached "run-shell $HOME/.config/tmux/kill-dangling-sessions.sh"

Where kill-dangling-sessions.sh is the following script:

#!/bin/bash

# List all unattached tmux sessions
unattached_sessions="$(tmux ls | awk '!/\(attached\)/ {gsub(/:$/, "", $1); print $1}')"

# Get all active tmux sessions
for session_name in $unattached_sessions; do
  if [[ "$session_name" =~ ^[0-9]+$ ]] || [[ "$session_name" =~ ^tmux-[0-9]+$ ]]; then
      tmux kill-session -t "$session_name"
  fi
done

Another common way to solve this is with a cronjob - I do not like it as much, I prefer to keep everything in tmux's config.

# The OG problem

As I mentioned, tmux takes around 1s to initialize. This is greatly unacceptable (it's 2024!).

The solution I went with is to have a "buffered" (actually called buffer) tmux session. In the past I have done similar stuff with zsh hooks - but I tried to keep everything as tmux as possible, independent of shell.

# Note that this time we only add the hook to "buffer"
set-hook -g -t buffer client-attached "run-shell $HOME/.config/tmux/create-buffer-shell.sh"

My create-buffer-shell.sh script is here:

#!/bin/bash

SESSION_FILE="/tmp/next_tmux_session"
if [ ! -f "$SESSION_FILE" ]; then
    echo "0" > "$SESSION_FILE" 
fi

SESSION_NUMBER=$(cat "$SESSION_FILE")
if [ -z "$SESSION_NUMBER" ]; then
    echo "Session number is empty in $SESSION_FILE."
    exit 1
fi

tmux rename-session "tmux-$SESSION_NUMBER"

NEXT_SESSION_NUMBER=$((SESSION_NUMBER + 1))
echo "$NEXT_SESSION_NUMBER" > "$SESSION_FILE"

BUFFER_SESSION_COUNT=$(tmux list-sessions | grep -c "^buffer$")
if [ "$BUFFER_SESSION_COUNT" -eq 0 ]; then
    tmux new-session -d -s "buffer"
fi
  1. A global file /tmp/next_tmux_session stores the next session number. (Since I cannot access tmux's internal numbering)
  2. The script increments the session number and creates a new session.
  3. The current session (buffer) is renamed to tmux-<session_number>
  4. Create a new buffer session detached (-d)

Finally, I modified my .zshrc.

# Before p10k's instant prompt!
# Start tmux (only if not already in a tmux session)
function safe-start-tmux() {
  export TERM=xterm-256color
  case $- in *i*)
    if [ -z "$TMUX" ]; then  
      # Custom tmux stuff - buffer one tmux session ahead to make startup instant
      tmux attach -t buffer || (tmux new -d -s buffer && tmux new -t buffer -A)
    fi
  esac
}

Why tmux attach -t buffer || (tmux new -d -s buffer && tmux new -t buffer -A) This is a bit ugly - but it works for now.

See the problem is, attaching directly on tmux new does not trigger the client-attached hook. So I do this in 3 steps:

  1. Try to attach to the buffer session.
  2. If it does not succeed (tmux was not running) create a new buffer session detached.
  3. Attach to the new buffer session. And trigger the client-attached hook.
Look at how quickly my prompt shows up!
Look at how quickly my prompt shows up!

Yay 🎉. Now my prompt is basically instant.