Coding Challenge #127 - Tmux
This challenge is to build your own tmux.
Hi, this is John with this week’s Coding Challenge.
🙏 Thank you for being a subscriber, I’m honoured to have you as a reader. 🎉
If there is a Coding Challenge you’d like to see, please let me know by replying to this email📧
Coding Challenge #127 - Tmux
This challenge is to build your own version of tmux, the terminal multiplexer.
If you spend any time working in a terminal, sooner or later you’ll meet tmux. It lets you run multiple terminal sessions inside a single window, split that window into panes, and, best of all, detach from a session and come back to it later with everything still running. It’s the tool that saves your work when your SSH connection drops. Under the hood it brings together some fascinating systems programming: pseudo-terminals, client-server architecture over Unix domain sockets, raw terminal input handling, and screen rendering.
Building a simplified version yourself is one of the best ways I know to really understand how a terminal works.
The Challenge - Building Your Own Tmux
In this challenge you’re going to build a simplified terminal multiplexer. Your tool will let you create named sessions that keep running in the background, detach from them and reattach later, open multiple windows within a session, and split windows into panes, each running its own shell.
This is an advanced challenge. The individual steps are manageable, but you’ll be working with pseudo-terminals, raw terminal modes, Unix domain sockets, and screen rendering, so it helps to be comfortable with systems programming.
The concepts are the same in any language, so pick one you know well that gives you good access to operating system APIs. This challenge is best tackled on Linux, macOS, or another Unix-like platform, since that is where pseudo-terminals and Unix domain sockets live.
Step Zero
In this introductory step you’re going to set your environment up ready to begin developing and testing your solution.
Choose your target platform and programming language. You’ll need access to pseudo-terminal (PTY) APIs, Unix domain sockets, and the ability to put the terminal into raw mode, so check your chosen language has libraries for these before you commit.
If you haven’t used tmux before, install it and spend some time with it. Create a session, detach with Ctrl+b d, reattach, split some panes, and switch between windows. Getting a feel for the real thing will make every step of this challenge clearer. The tmux man page and the tmux getting started guide are both excellent references.
It’s also worth reading up on how pseudo-terminals work (man pty on most systems) and how terminal escape sequences control the screen. These two topics are the foundation of everything you’ll build here.
Step 1
In this step your goal is to allocate a pseudo-terminal, spawn a shell inside it, and connect it to your own terminal.
This is the core building block of a terminal multiplexer. Your program should create a PTY, launch the user’s shell as a child process attached to it, put your own terminal into raw mode, and then shuttle data in both directions: keystrokes from your terminal go to the shell, and the shell’s output is displayed on your screen. When the shell exits, your program should restore the terminal to its original state and exit cleanly.
Testing: Run your program:
cctmuxYou should see a shell prompt. Try running some commands, including full-screen programs:
ls -la
vim
topEverything should behave exactly as it would in a normal terminal. Type exit and verify your program shuts down and your terminal is restored to a usable state.
Step 2
In this step your goal is to split your program into a server and a client that communicate over a Unix domain socket.
Real tmux is built as a client-server system. The server owns the sessions and the shells running inside them; the client is a thin process that connects to the server, sends it your keystrokes, and displays whatever the server tells it to. This separation is what makes detaching possible later: the shells belong to the server, so they survive when a client goes away.
When your client starts, it should check whether a server is already running. If not, it should start one in the background. The two should communicate over a Unix domain socket, and you’ll need a simple protocol for passing keyboard input in one direction and screen output in the other.
Testing: Run your client:
cctmuxYou should get a working shell, just like in Step 1. In another terminal, verify the socket exists (for example with ls -l on wherever you chose to put it, such as /tmp/cctmux-*) and that the server appears in ps as a separate process from your client.
Step 3
In this step your goal is to support named sessions.
A session is a collection of shells managed by the server under a single name. Your tool should support creating a new named session:
cctmux new-session -s mysessionThe server should be able to hold several sessions at once, each with its own shell. If a user tries to create a session with a name that already exists, they should get a clear error message.
Testing: Create a session and verify you get a shell:
cctmux new-session -s workRun a command like echo hello to confirm the shell works. Then try creating a duplicate:
cctmux new-session -s workYou should see an error telling you the session already exists.
Step 4
In this step your goal is to implement the prefix key and detaching from a session.
Tmux is driven by a prefix key, Ctrl+b by default. When the user presses the prefix, the next keystroke is a command for the multiplexer instead of input for the shell. Your first command is d for detach: the client should disconnect and exit, restoring the terminal, while the session and its shell keep running on the server.
Make the prefix key configurable so users can choose something other than Ctrl+b, for example via a command line flag or a simple configuration file. Any key that isn’t a recognised command after the prefix should be handled gracefully, and pressing the prefix twice should send the prefix key itself through to the shell.
Testing: Create a session and start something long-running in it:
cctmux new-session -s workInside the session run:
sleep 300 &
echo "still here"Press Ctrl+b then d. You should be back at your normal terminal prompt. Verify with ps that the shell and the sleep process are still running. Then configure a different prefix key, start a new client, and verify detaching works with the new prefix and that Ctrl+b now passes through to the shell.
Step 5
In this step your goal is to support reattaching to a running session.
Detaching is only half the magic. Your tool should let the user reconnect to a session that’s running in the background:
cctmux attach-session -t mysessionOn attaching, the user should see the session’s current screen content, including output that was produced while they were detached, and be able to carry on working as if they never left. Attaching to a session that doesn’t exist should produce a clear error.
Testing: Create a session, run echo before detach, then detach with Ctrl+b d. Reattach:
cctmux attach-session -t workYou should see the screen as you left it, including before detach. Detach again, then try attaching to a session that doesn’t exist:
cctmux attach-session -t nosuchsessionYou should get an error message, not a hang or a crash.
Step 6
In this step your goal is to support listing sessions, killing sessions, and cleaning up the server.
Your tool should report the sessions the server is currently managing:
cctmux list-sessionsThe output should show each session’s name and some useful detail, such as how many windows it has and whether a client is attached, similar to the real tmux output.
It should also support killing a session:
cctmux kill-session -t mysessionKilling a session should terminate the shells running inside it. When the last session is closed, whether by kill-session or by all of its shells exiting, the server should shut itself down and remove its socket, leaving nothing behind.
Testing: Create two sessions, work and play, detaching from each. Run:
cctmux list-sessionsYou should see both sessions listed. Kill one:
cctmux kill-session -t play
cctmux list-sessionsOnly work should remain. Kill work too, then verify with ps that the server process has exited and that the socket file has been removed.
Step 7
In this step your goal is to add a status bar.
Tmux reserves the bottom line of the terminal for a status bar. Yours should show the session name on the left, the list of windows in the middle (just one window for now), and the current time on the right. The status bar should update as things change, including ticking over as the time changes, and the shell should now render in the remaining lines above it without ever overwriting the bar.
Testing: Attach to a session. You should see the status bar on the bottom line showing the session name and the time. Run a command that produces lots of output, such as:
seq 1 100The output should scroll in the area above the status bar and the bar should stay intact. Watch for a minute and confirm the clock updates.
Step 8
In this step your goal is to support multiple windows within a session.
A window is like a tab: each window has its own shell, but only one window is visible at a time. Pressing the prefix key followed by c should create a new window with its own shell and switch to it. The status bar should list all the windows with their numbers, starting from 0, and indicate which one is currently active.
Testing: Attach to a session and run echo window zero. Press Ctrl+b c. You should get a fresh shell and the status bar should now show two windows, with the second one marked as active. Run echo window one in the new window. Create a third window with Ctrl+b c and confirm the status bar shows three windows.
Step 9
In this step your goal is to support switching between windows.
With multiple windows created, the user needs to move between them:
The prefix key followed by a digit (
0to9) should switch directly to that window number.The prefix key followed by
nshould switch to the next window, wrapping around from the last back to the first.The prefix key followed by
pshould switch to the previous window, wrapping the other way.
When switching to a window, its screen content should be restored exactly as it was, and the status bar should update to show the newly active window.
Testing: Using the session from Step 8, press Ctrl+b 0. You should see the first window with window zero still on screen. Press Ctrl+b 1 and verify you see window one. Press Ctrl+b n twice and Ctrl+b p twice, checking the status bar each time to confirm you cycle through the windows in order and wrap around at each end. Try Ctrl+b 9 and confirm switching to a window that doesn’t exist is handled gracefully.
Step 10
In this step your goal is to split a window into two panes, one above the other, with the prefix key followed by ".
This is where your multiplexer really starts to earn its name. Pressing Ctrl+b " should split the current pane horizontally, creating a new pane below it with its own PTY and shell. Both panes run at the same time: output arriving in either pane should render in that pane’s region of the screen, whichever pane is active.
Draw a border between the panes using box-drawing characters (such as ─ and │), and make sure each shell knows its actual pane size so that programs running inside it wrap and draw correctly. Keyboard input should go to the active pane.
Testing: Attach to a session and press Ctrl+b ". The screen should split into a top and bottom pane with a horizontal border between them. In the active pane run:
seq 1 5The output should appear only in that pane. Start a long-running command in one pane, for example top, and confirm it keeps updating its own region while you type in the other pane after switching (for now you can test switching in the next step, or verify the inactive pane keeps rendering). Run ls and confirm the columns fit the pane width, not the full terminal width.
Step 11
In this step your goal is to split a window into two panes side by side with the prefix key followed by %.
Pressing Ctrl+b % should split the current pane vertically, creating a new pane to the right with its own shell. Vertical borders should be drawn between side-by-side panes, and splits should compose: the user should be able to mix " and % splits to build up a grid of panes, each rendering independently in its own region.
Testing: In a fresh window press Ctrl+b %. You should see two panes side by side with a vertical border. Run seq 1 5 and confirm the output stays within the pane. Now press Ctrl+b " and confirm the current pane splits into top and bottom, giving you three panes. Run a different command in each and verify all three render correctly in their own regions.
Step 12
In this step your goal is to support navigating between panes with the prefix key followed by the arrow keys.
Pressing Ctrl+b followed by an arrow key should move the active pane in that direction: up, down, left, or right. Your tool should give a visual indication of which pane is active, for example by highlighting its border. Keyboard input should always go to the active pane.
Testing: Build a layout with at least three panes. Use Ctrl+b followed by the arrow keys to move between them, confirming the active pane indicator follows you. In each pane type a different command, such as echo top left, and verify the input lands in the pane you expect. Try navigating in a direction where there is no pane and confirm nothing breaks.
Step 13
In this step your goal is to support resizing panes with the prefix key followed by Ctrl and the arrow keys.
Pressing the prefix followed by Ctrl+Up, Ctrl+Down, Ctrl+Left, or Ctrl+Right should move the border of the active pane in that direction, making it bigger or smaller. The neighbouring pane should shrink or grow to match, borders should be redrawn, and the shells in the affected panes should be told their new size so that running programs adapt.
Be warned if you’re working on a Mac Ctrl+Arrow gets captured by the OS, you’ll want to add support for the Alt key just like tmux itself does.
Testing: Split a window with Ctrl+b ". Press Ctrl+b Ctrl+Up a few times and watch the border move up, making the bottom pane taller. Run top in one pane, resize it, and confirm top redraws itself to fit the new size. Repeat with a side-by-side split using Ctrl+Left and Ctrl+Right. Confirm you can’t resize a pane away to nothing.
Step 14
In this step your goal is to handle the terminal itself being resized.
When the user resizes their terminal emulator window, your client should notice, tell the server, and the server should redistribute the space: recalculating the pane layout, informing every shell of its new size, redrawing all borders, and keeping the status bar on the bottom line.
Testing: Attach to a session with several panes and resize your terminal window by dragging its corner. The layout should adapt smoothly: panes share the new space, borders are redrawn correctly, and the status bar stays on the bottom row. Run top in a pane while resizing and confirm it adapts. Make the terminal very small and then large again, and confirm nothing is corrupted.
Step 15
In this step your goal is to add a command prompt.
Pressing the prefix key followed by : should open a prompt on the status bar line where the user can type a command, edit it, and press Enter to run it, or Escape to cancel. Support the commands your multiplexer already understands, for example new-window, split-window, list-sessions, and kill-session. Unknown commands should show an error message on the status line rather than doing anything surprising.
Testing: Press Ctrl+b : and the status bar should turn into a prompt. Type new-window and press Enter; a new window should be created just as if you had pressed Ctrl+b c. Open the prompt again, type nonsense, and confirm you get an error message on the status line. Finally, press Ctrl+b : then Escape and confirm the prompt closes without running anything.
Step 16
In this step your goal is to add copy mode, so users can scroll back through a pane’s output history.
Each pane should keep a scrollback buffer of output that has moved off the top of the screen. Pressing the prefix key followed by [ should enter copy mode for the active pane: the pane freezes, and the user can scroll back through the history using the arrow keys and page up/page down. Give a visual indication that copy mode is active, such as a position indicator showing where the user is in the history. Pressing q should leave copy mode and return the pane to live output.
Testing: In a pane, generate more output than fits on the screen:
seq 1 500Press Ctrl+b [ and scroll up. You should be able to scroll back and find line 1. Scroll down again, then press q and confirm the pane returns to the live view and typing works normally. Verify that output arriving in another pane doesn’t disturb your position while you’re scrolling.
Going Further
Once you have the core multiplexer working, here are some ideas to take it further:
Add text selection and copying in copy mode, with a paste buffer and a paste key binding (prefix followed by
]).Support renaming sessions and windows, and show the custom names in the status bar.
Add a session chooser (like tmux’s prefix followed by
s) for switching sessions interactively.Support a configuration file that sets key bindings, colours, and status bar content, like
.tmux.conf.Add zooming a pane to full screen and back with prefix followed by
z.Implement mouse support: clicking to select a pane, dragging borders to resize, and scrolling with the mouse wheel.
Add synchronised panes, where input is sent to every pane in the window at once.
Support layout presets such as even-horizontal, even-vertical, and tiled.
Share Your Solutions!
If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via Bluesky or LinkedIn or just post about it there and tag me. Alternately please add a link to it in the Coding Challenges Shared Solutions Github repo
Request for Feedback
I’m writing these challenges to help you develop your skills as a software engineer based on how I’ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback is greatly appreciated.
You can reach me on Bluesky, LinkedIn or through SubStack
Thanks and happy coding!
John

