2026-06-29 Tmux Width-Responsive Statusline
What I set out to do
Make the tmux statusline collapse gracefully in “restricted space territory” (client narrower than 100 columns): drop the network up/download widget, battery, and clock; show only ram + cpu% + 1m load; and stop showing the active window’s process name in the title. Crucially, separators had to disappear with their widgets so the bar wouldn’t render empty gaps.
What I actually did
Edited nix/home-manager/modules/tmux.nix. Each droppable widget is wrapped in a width conditional that also swallows its leading separator. The CPU widget switches content rather than hiding: tmux-mem-cpu-load -v (mem, graph, cpu%, 1m/5m/15m) when wide, -g 0 -a 1 (ram + cpu% + 1m only) when restricted.
Mid-task, on request, refactored the separator handling so separators are no longer hand-paired with each widget. Introduced seg / segWide segment constructors and a joinWidgets sep widgets helper that interleaves separators automatically, binding each separator to its widget’s visibility. Adding or removing a widget is now a one-line list edit.
What was striking
Two tmux format gotchas, both now in code comments:
<:/>=:are string comparisons. The numeric width test needs#{e|>=:#{client_width},100}—#{<:5,100}is false because “5” > “100” lexically.- A literal comma inside a
#{?...}branch must be escaped as#,, which matters because every#[fg=...,bg=...]token has commas.joinWidgetsruns branch content throughescapeCommas; the condition’s own,100stays unescaped since it’s a protected#{...}token. #(...)is invisible to#{?...}conditions. Follow-up: wanted a separator between the AI-agents widget and ram, but only when agents are present. Can’t do it at the format layer —#{?#(echo nonempty),HAS,EMPTY}returnsEMPTYbecause tmux runs#()as an async, cached background job that reads empty during conditional evaluation. So the conditional had to move intoclaude-status.sh, which already branches ontotal>0; appending thesep_barpast that guard renders the separator only when an agent shows (passedsep_barin as a newseparatorsubstitution, updatedtest-claude-status.sh).- tmux reads
\1in a status string as an invalid octal escape. Trimming the spacetmux-mem-cpu-loadleaves between the cpu graph and the percent: a capturingsed 's/ \(…%\)/\1/'madesource-filefail at the status-right line, and because thereloadTmuxactivation runssource-file … || true, the error was swallowed and the old config stayed live — so the change looked like it did nothing. Any sed backreference/escaped-paren breaks inside tmux’s own string parser. Fix:tr -s ' ' | sed 's/ //'(no backslashes; the graph→percent gap is the first space since mem and graph aren’t separated). Lesson: a swallowedsource-fileerror is invisible; when a tmux change “does nothing,” re-source manually and watch for a parse error. - Clickable widgets via
#[range=user|NAME]…#[norange]+ aMouseDown1StatusRightbinding dispatching on#{mouse_status_range}. Wired claude→claude-pane-picker.sh, cpu→display-popup -E -w 80% -h 80% btop, session→fzf-session-picker.sh(matchesprefix g). Added aclickable name contenthelper alongsidejoinWidgets. Gotcha:range=user|Xfires theStatusmouse key, notStatusRight(man tmux: StatusLeft/StatusRight are only for the predefinedrange=left/range=right). So the binding must be onMouseDown1Status, with a finalselect-window -t=else-branch to preserve the window list’s default click-to-select.
Built with hm switch (slow, ~5min first eval, ran in background), sourced live via the reloadTmux activation. Verified branch selection at width 50 (restricted slots render empty, no dangling separator; cpu swaps to -g 0 -a 1; window title shows index only) and 120 (full bar). Staged but not committed.