Why I decided to write a Terminal Multiplexer - Part 1

Why I decided to write a Terminal Multiplexer - Part 1

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:

alacritty.png

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. Always tab <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, or npm 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 get vi/nano to work. If you are lucky, you get garbled output. But often you get 0 bytes of stdout, and are left reading screen 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.