Tmux go brr - buffered tmux sessions
2024-09-23
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.
- Ephemeral sessions should be destroyed as soon as the terminal is closed. They are named either "<number>" (for fallback) or "tmux-<number>".
- 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:
- What happens if more than one client is attached?
- 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
- A global file
/tmp/next_tmux_session
stores the next session number. (Since I cannot access tmux's internal numbering) - The script increments the session number and creates a new session.
- The current session (
buffer
) is renamed totmux-<session_number>
- 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:
- Try to attach to the
buffer
session. - If it does not succeed (tmux was not running) create a new
buffer
session detached. - Attach to the new
buffer
session. And trigger theclient-attached
hook.
Yay 🎉. Now my prompt is basically instant.