Over the past several months, I've been working on tab, a new terminal multiplexer. Tab is a terminal multiplexer written in Rust. It's designed for software & systems engineers who use the terminal as a launchpad for other tools, and who need to frequently context switch between code repositories.
In this series, I'll tell the story of why I decided to write tab, how I developed the requirements, and how I actually implemented it.
Why I decided to write a Terminal Multiplexer
I'm a software engineer. Every day, I need to launch things. I need to launch a few local servers, and launch git
, github
, atom
, code
, etc. I sometimes need SSH sessions, or tunnels.
After a few hours, my toolbar fills up with Alacritty windows:
Now, I need to find something. One of those windows has the docker exec
session I need. All the others have random stuff, many of them I don't care about. But I don't close things. I never know what I'll need a few hours from now, and if I close the window, I'd lose the command history. So I don't.
About 6 months ago, I started looking into terminal multiplexers.
What I wanted to find
I needed a solution to an alt-tab problem. I was hoping to find a simple navigator. The faster it can get me somewhere, and the less I have to say about where I want to go, the better.
Zoxide is a great example of a navigator. Zoxide is a cd
replacement that takes short text input, and uses a database of local paths to find the directory you wanted. That's what I wanted from my terminal multiplexer.
Also, I'm often tailing logs, or running servers, and I wanted to keep the natural scrollback buffer in my terminal emulator (Alacritty). I didn't want a tool that destroys my ability to scroll up.
tmux
I looked into tmux, which is popular and very configurable. It supports panes, windows, virtual sessions, and configurable keyboard shortcuts.
I tried it, and it partially worked. But all these Tmux features kick in after you reach your destination. It didn't help with the 'getting there' part. It provided a windowing system (with windows and panes), not a navigation system.
The Tmux command-line interface also requires you to memorize your tab state. In order to create a session in tmux, you have to know it doesn't exist. And in order to reattach, you have to know it exists. I wanted 10-20 tabs, so keeping track of this wasn't practical. And these subcommands require a few extra keystrokes for the common path: select a new tab.
screen
Screen had similar state issues, but was simple enough to 'hide the crimes' in fish
helpers. They looked roughly like:
tab <tab-name>
tab-close <tab-name>
tab-list
It was great. I kept to one window. I could easily check if the server was was running tab server
. And with a .screenrc
hack, screen
filled the Alacritty scrollback buffer, so I could just trackpad scroll-up to find errors in the logs. I hooked up Zoxide, so my tabs were cd'd if the name matched the Zox database.
I used it for a while, and I started to see the potential of a terminal multiplexer which is designed to navigate and orient. By finding and remembering.
And so, tab
I decided to write an intuitive, config-driven terminal multiplexer. Designed for navigation, not multi-tasking. Designed to deliver persistent historical context, not to provide throwaway workspaces.
The requirements:
- Tabs should be selected with
tab <NAME>
. No sub-commands, and no flags. Autocomplete my tab names, so I can type 7 characters (tab so\t
) instead of 19 characters (tab somerepo/serve
). - The
tab
user interface should be consistent and stateless, so I can type without thinking. Alwaystab <NAME>
, from anywhere, to anywhere, no matter the state. - When
tab
takes me somewhere, it should provide orientation. I probably want to run what I ran last time.cargo run --release
, ornpm run serve
. Save the working directory, and save the shell command history. The up arrow key should be my best friend. - Config should be dead simple. It should take 5 minutes to set up, and 1 minute to make tweaks. I'm an engineer, I have a few workspace roots, and a ton of repositories in each workspace. I need to write code, and I don't have time to spend 2 hours configuring my multiplexer (ok, I had time to write one, irony noted).
This tool would transform my life. And so I started figuring out how to write a terminal multiplexer.
Why tab
didn't already exist
After getting started, I started to realize that it's not easy to write a terminal multiplexer, and it's very hard to write this one.
The problems:
- Latency has to be insanely low. You type a lot of characters, and if one of them takes >25ms to appear, you'll feel it. Make it 1-3ms, every time, whenever I hammer that keyboard. That server you launched might be throwing out 2Kb/s of stdout right now, and it needs to feel fast too, which means hundreds of packets per second mixed with your keystroke packet. It's a really tough performance target.
- It is very difficult to correctly forward stdin/stdout and get
bash
to work. Let alone actually getvi
/nano
to work. If you are lucky, you get garbled output. But often you get 0 bytes of stdout, and are left readingscreen
source code, eventually figuring out what went wrong with the pty device. - It's a big project. Today
tab
has 10k lines of Rust. It took me over 6 months to write, and about 2 months to stabilize.
How tab
now exists
I'm going to write more on this in the next post, but the short answer is Rust
, async/await
, and a message-based architecture with extreme concurrency.
More coming in Part II.