TL;DR I’ve used tmux since 2016 because I can script it. Over ten years my config grew into 1,495 lines of zsh across 13 scripts, and in May 2026 the status bar alone was burning 15.4% of a core on battery. The fix was one
#()call instead of six, and the scripts moved into one Rust daemon, tmux-companion. The bar costs 1.8% of a core on a spike and 0.3% idle now.
I’ve been using tmux since somewhere in 2016 and I’ve never seriously tried to leave.
People usually sell it on the detach trick, and that part is real. An ssh session drops, the work carries on, I reattach and everything’s where I left it. Sessions on my laptop live for months across sleeps and reboots.
It’s flexible Link to heading
I stayed for a different reason, tmux is scriptable all the way down. Every binding can run a command, every command can read the state of the session, and all of that is out of the box without a single plugin. So I build my own developer experience on top of it, and I’ve customised it heavily.
Once a binding is in my fingers my attention stays on the problem in front of me instead of on the procedure for reaching it, so the difference between “I need to run this against staging” and running it is pulling one chord instead of four windows and a paste.
tmux is also cheap. With a dozen sessions open it uses less memory than most IDEs use for one project and sits at nothing when idle, so I can leave sessions running for months without once thinking about what they’re costing me.
The trail it left in my dotfiles Link to heading
I can date most of this, because my private dotfiles repo has been running the whole time and git kept the receipts.
For the first year I didn’t care about dotfiles, vim’s included, because they were small and I didn’t see the point of maintaining a repo for them. I don’t remember which YouTube video it was, but one was about tmux and neovim, and the person showed his dotfiles repo and explained how it grew, and that clicked. If you use a tool for years it grows on you and you want to make it yours, and a dotfiles repo is an easy way to keep track of that, which also shows you your own mistakes and likings.
First commit in it was the README, second commit was .tmux.conf, 51 lines.
That first file already had the things I still set: escape-time 0, windows and panes numbered from 1, renumber-windows on, and a comment about fixing the split path “for tmux 1.9”.
It also bound prefix a to send the prefix through, which means I was already sshing into something that was itself running tmux.
I was working on test databases on a VPS box that needed a lot of editing to make them work, the jobs ran for more than 30 minutes, and my connection sometimes dropped, which failed the job 3-4 times, and each time I had to restore the backup and start again.
There was also the headache of scp-ing the scripts across, as the box didn’t have access to VCS.
Powerline arrived the next day, 1 August 2017, and stayed for years. I configured and customised it, then hit its limits: powerline is a DSL over what tmux already gives you raw, and going raw is what let me build what I wanted. The limit I hit was working with JSON and making segments dynamic and runtime-aware, which at that time I couldn’t figure out. So I started playing with my own bash scripts for segments, and that’s when the customising got serious.
A year later, on 17 August 2018, I wired up tmux-resurrect on C-s and C-r, told it to bring back vim mvim "git log", turned on @resurrect-capture-pane-contents, and set default-command to reattach-to-user-namespace so that copying out of a pane put something in the macOS clipboard.
The same commit added seven lines of mouse wheel bindings, which is what scrolling in tmux cost you in 2018.
I rarely use resurrect nowadays, mostly because of session autosave and my laptop not dying on me randomly.
On 1 March 2019 I opened pull request #2 against my own dotfiles, “Feature/status-bar-improvements”.
I knew I’d be committing and reverting things dozens of times, and I wanted it to land as one change I could revert if I didn’t like it in a week.
I pushed 9 commits, most of them squashed on my machine first.
I moved from bash to zsh on 31 December 2019.
Then five years where the tmux config barely moves.
2021 fixes TERM and reconfigures powerline, 2022 and 2023 and 2024 are one “changes” commit each, and the config sits there working while I get on with the job.
The pile grew somewhere else during those years, one zsh file per thing the bar needed to know.
The pile Link to heading
Mine lived in ~/.config/tmux/comrades and came to 1,495 lines of zsh across 13 scripts, plus another 294 lines of generators and probes.
I know .config is meant for configs and not scripts, which I fixed later.
That directory held:
- A project switcher.
- A key-binding search.
- A history runner.
- Three scripts for theme picker, preview and generator.
- Something to open whatever’s under the cursor.
- Something to close a session without leaving nvim swap files everywhere.
- zoxide window picker
- a custom window toggler based on process running in window
I wrote each one on the day something annoyed me.
The history command runner was the most frustrating: while working on code I needed to run a script, which I could loop but had to wait until I’d saved the changes, so at first I tried for and read so it ran manually, but then I had to keep switching windows, and if I used a split pane then vim wrapped long lines or I had to scroll, and it wasn’t a good time.
So I wrote the history runner and added restart on a key press, so I can run it anytime or view its output to copy from.
The script that opens vim on a file path found in copy-mode-vi was obvious once I started doing a lot of testing with vim as my editor and my onchange script as an auto runner for anything.
Not all of it was zsh.
I also had a Go CLI that did various tasks for me on the command line, and one of them was drawing git status in tmux, which is where gst comes from.
I don’t remember where I got the inspiration to write it.
The first Go version was sloppy and had SQLite embedded in it, which I later replaced with an in-memory cache.
I never added any of it up till May 2026.
Then I measured it Link to heading
In May 2026 I noticed my laptop was always running hot, with the CPU high for no reason I could name.
Chrome and Docker were there, I was locked in on Reddit for an hour or so and the cooling fans slowly gained speed.
So I quit Chrome and killed Docker Desktop, but CPU utilisation was still at 20%, with nothing running in my sessions.
I went to Activity Monitor first, then top, then ps to understand the process hierarchy, and the open files in Activity Monitor made it clear it was my CLI and the other scripts, all started by tmux.
The rest of the 20% was menubar apps and macOS processes, which I googled and decided not to touch.
tmux refreshes the status bar on status-interval, once a second here.
My bar called six programs to draw one line of text.
On battery that was 15.4% of one core, all day, so tmux wasn’t sitting at nothing any more.
Computing every segment on my bar takes 2.6 ms, but a fork and exec costs 12.4 ms of CPU on my machine, 14.6 as tmux runs it through sh -c, so almost all of it went on starting programs.
Six of those a second came to 153.77 ms of CPU for every second of wall clock, 80.90 in the tmux server and 72.88 in the spawns.
The worst case was worse and had a different cause: the old net segment hit a netstat hang about one call in three, which took a single refresh past 1,100 ms.
The middle of the bar was the worst of it, since that segment ran once per window instead of once per bar, and tmux’s own window-status-format had been sitting there doing the same job the whole time.
On the left, a client count and a glyph that meant “nvim is suspended somewhere”, and the second one walked every process on the machine to decide whether to draw one character.
The pickers paid the same cost.
A popup that starts zsh, sources a config and pipes into fzf spends most of its latency before the first frame shows up, and none of that’s the search.
One process Link to heading
I ported the Go CLI to Rust.
I chose Rust because I found it fun, nothing in particular except that I liked its memory management and wanted to write something in it.
On 5 June 2026 I pointed the bar at the port, tmux-companion, and deleted 324 lines of zsh in one commit, battery-life.zsh, net-monitor.zsh, check-clients.zsh, tmux-session-name-format.zsh, and window-status.zsh at 154 lines on its own.
The bar was still slow, because I’d swapped what ran inside six #() calls without touching the six.
On 11 August I collapsed the six calls into one.
# one call, not six
set -g status-right "#(tmux-companion status-right #{pane_current_path})"The daemon holds its caches in memory and answers over a unix socket. Every binding that used to start a shell sends it one line of JSON and reads one back. The pickers draw in the client, since the daemon hasn’t got a terminal.
I also stopped passing #{pane_pid} to the git segment in that commit.
That one argument was what sent it hunting through the process table for a suspended nvim, 18.3 ms every second, for a marker I turned out not to miss.
The rest of the config followed it in Link to heading
The zsh was 1,495 lines and the Rust is 20,207, with another 1,375 in tests.
Most of the performance came from that single #() call, so the extra lines bought something else: I can dump a lot of zsh scripts into one codebase, and over time I plan to move the other parts of tmux I’ve customised into it.
Having multiple scripts isn’t a problem as such, but a single companion for tmux made more sense to me than a directory of scripts, and scripts are hard to configure, since you change the script itself instead of overriding a YAML or TOML file.
Every project I have starts with two windows, neovim in the first called “editor” and Claude or Gemini in the second called “ai”, and that’s standard for most projects, but some have “editor” and “ci” with 3-4 panes for linter, tests and dev server, and none of that is easy to write into a script and keep flexible.
Once a process is sitting there anyway, asking it something else costs close to nothing, so the things I’d shelved for being another spawn got cheap. The daemon holds the rows and the client draws them, which is also why a picker can be tested against a fake backend with no socket in sight.
- keyssearches every binding I’ve written. tmux lets you put a note on a binding with- -Nand gives you no way to search the notes, so the popup reads them and runs whatever I pick.
- cheatsheetlays the same bindings out in four boxes, most-pressed first, off the usage log the picker was already writing.
- projectis one session per project, with live sessions and everything zoxide knows in one list, and- project savecaptures the pane layout that project comes back with.
- runpicks a command out of shell history into a pane that slides out.
- opentakes the URL or the- file:line:colunder the cursor.
- theme pickdraws a swatch per theme and applies it on the spot.
- shell-initprints the OSC 133 prompt marks that tmux’s- next-prompthas wanted since 3.3 and almost nobody wires up.
- doctorputs everything a bug report needs on one screen.
The project sessions came from a problem I had while context switching. Previously whatever I worked on was connected to some larger piece of work, so even if, in the strict sense, I was working on multiple projects, they were interconnected. Now I work on multiple things that have nothing to do with each other, and separating them into sessions helps me context switch better.
Through 21 and 22 September 2026 I gave every project its own session with a fixed colour and one key to toggle it, cached the key list so the cheat sheet on prefix+? opens without thinking about it, put a second cheat sheet on prefix+C-c ordered by what I press rather than alphabetically, and fixed the three bugs that made half my themes unreadable.
I found a lot of existing tools, but like always I built my own first because it was simple and I knew what I wanted, and if something turns out complicated I see if it’s worth switching to someone else’s maintained tool.
There are background tasks too, fetching my repositories so the ahead and behind counts mean something, reloading the config when it changes, naming windows after what’s running in them, and telling me a long command finished in a session I wasn’t looking at. All of it is off till a config line turns it on.
What I didn’t build Link to heading
Plenty of plugins already do pieces of this, so before writing any of it I checked stars and last-push dates.
tmux-resurrect and tmux-continuum are still the answer for crash recovery across a whole server, and my autosave shells out to resurrect’s own save script instead of reimplementing it.
Hint-based copy was on my list until I looked: tmux-fingers has 1,473 stars, tmux-thumbs is already Rust, and the only argument for a third one was that it would share a pattern table with open, which isn’t a reason, so open will ship a snippet that hands off to thumbs.
It won’t restore your sessions on its own: it saves them on a timer, and restoring stays on a key I press, because an automatic restore would drop a stale layout over a session I’d already started working in.
A container to try it in Link to heading
A couple of colleagues once asked for my tmux config and the Go CLI, I assume because of the value they saw me getting out of it. I don’t think they liked it, since nobody followed up, and it was very opinionated and personal to me.
tmux-companion has a Docker image, so you can try it without installing anything.
docker run --rm -it ghcr.io/lonkar-org/tmux-companion:playgroundIt has tmux, the binary, the config with every feature turned on, five fake projects and a guided tour through the bindings.
Nothing is mounted from your machine and nothing leaves the container, and it all goes away when you exit.
Each of the five projects sits in a different git state so the bar has something different to say in every one, and orchard-api/build.log holds a compiler error with a path, a line and a column for the copy-mode o binding to open.
The tour is sixteen steps, and each one says what to press, pins itself to a second status line so it’s still in front of you after you’ve switched sessions, and waits for Enter.
s skips a step, q drops you into a shell, and tour starts it again.
Step one offers nine optional screens on tmux itself: servers and clients, the prefix, how sessions, windows and panes nest, and what detaching does. I stopped at those because I didn’t want to build a tmux tutorial, but since the playground is in Docker anyone can use it, including people who have never used tmux, so I made those screens optional on the first step for anyone who wants the basic concepts. They end by pointing at learntmux.dev, which is 42 tasks against a real tmux in the browser and is better than anything I’d write.
A Nerd Font has to be installed and picked in your own terminal, and step two prints four glyphs so you find out in the first minute.
If you start the container from inside tmux, Ctrl-b reaches the tmux you were already in and every binding in the tour looks broken, so run it from a terminal that isn’t in a session, or press the prefix twice.
The pickers want tmux 3.2 for display-popup -E and the bar is happy on 3.0.
If you’ve got your own pile Link to heading
If your bar has a few #() calls, watch ps for a minute and count what it starts, since the cost is in how many programs start and not in what they compute.
I haven’t planned anything next for it beyond moving more of my tmux customisations in. The code is at lonkar-org/tmux-companion, MIT, macOS and Linux.