Read OSS

tmux from 10,000 Feet: Architecture and How to Navigate the Source

Intermediate

Prerequisites

  • Basic C familiarity
  • Understanding of Unix processes, fork(), and Unix domain sockets
  • General knowledge of terminal emulators and multiplexers

tmux from 10,000 Feet: Architecture and How to Navigate the Source

tmux is one of the most widely used pieces of Unix infrastructure, yet surprisingly few developers have ever looked inside it. At ~82,000 lines of C spread across roughly 144 source files, the codebase is large enough to be intimidating but small enough that a single person can understand the whole thing. This article gives you the map.

The core insight behind tmux's design is deceptively simple: a single binary serves as both client and server. When you type tmux, the process tries to connect to an existing server over a Unix domain socket. If no server is running, it forks one into the background and then reconnects. Every terminal you see — every pane, every scrollback buffer — lives in that server process. Clients are just thin relays between your physical terminal and the server.

What tmux Is and Its Design Philosophy

tmux is a terminal multiplexer: it lets you run multiple terminal sessions inside a single window, detach from them, and reattach later. The project was started by Nicholas Marriott in 2007 as a BSD-licensed alternative to GNU Screen, and it has been part of OpenBSD's base system since OpenBSD 4.6.

The single-binary design is central. There's no tmux-server package to install separately. The same tmux executable handles argument parsing, becomes the client process, and — when needed — forks off the server daemon. This is declared right at the top of the codebase with four global option trees and a handful of global variables:

tmux.c#L36-L44

flowchart LR
    A["tmux binary"] -->|"connect to socket"| B["Server Process"]
    A -->|"fork if no server"| B
    C["Client 1"] <-->|"imsg over Unix socket"| B
    D["Client 2"] <-->|"imsg over Unix socket"| B
    B --> E["Session 1"]
    B --> F["Session 2"]

The server owns all state: sessions, windows, panes, paste buffers, options. Clients are stateless relays that forward keystrokes to the server and receive drawing instructions back. This separation is what makes detach/reattach possible — killing a client process doesn't affect the server or its sessions.

The Flat Directory Structure and File Naming Conventions

tmux has a flat directory structure: all ~144 .c files live in the project root. There are no src/ or lib/ subdirectories. Instead, files follow strict naming conventions that make navigation surprisingly easy once you learn the prefixes.

Prefix Count Purpose
cmd-*.c ~50 One file per tmux command (cmd-new-session.c, cmd-split-window.c)
server*.c 4 Server daemon: main loop, client handling, ACLs
tty*.c 6 Terminal I/O: output, key decoding, ACS, features, terminfo
screen*.c 4 Virtual screen abstraction and redraw compositing
window*.c 7 Window/pane lifecycle and interactive modes (copy, tree, buffer)
grid*.c 3 Cell storage, view coordinate translation, cursor reading
layout*.c 3 Pane layout tree: splitting, preset layouts, custom layouts
input*.c 2 VT100 parser (input.c) and key encoding (input-keys.c)
osdep-*.c ~6 Platform-specific code, one file per OS
format*.c 2 #{...} string expansion and styled rendering
control*.c 2 Control mode protocol for iTerm2 integration

The full source list is in Makefile.am#L81-L216. A single master header, tmux.h, forward-declares every struct and function across the entire project — it's over 3,800 lines and serves as the project's table of contents.

Tip: When you want to find where a feature lives, start with the naming convention. Wondering how split-window works? Open cmd-split-window.c. Curious about the status line? Check status.c. The naming is remarkably consistent.

Startup Flow: From main() to Client-Server Split

The main() function in tmux.c#L349-L540 does four things: locale setup, option parsing, global initialization, and handoff to client_main().

sequenceDiagram
    participant User
    participant main as main() [tmux.c]
    participant client as client_main() [client.c]
    participant connect as client_connect()
    participant server as server_start() [server.c]

    User->>main: tmux new-session
    main->>main: Parse CLI flags, init globals
    main->>main: Create option trees (L481-L491)
    main->>main: Resolve socket path
    main->>client: client_main(base, argc, argv, flags, feat)
    client->>connect: client_connect(base, socket_path, flags)
    connect->>connect: socket() + connect()
    alt Server not running
        connect->>server: server_start() → fork()
        server-->>connect: Return child fd
    end
    client->>client: proc_add_peer(), send identify
    client->>client: proc_loop() — enter event loop

The critical handoff happens at the very last line of main():

exit(client_main(osdep_event_init(), argc, argv, flags, feat));

Inside client_main() at client.c#L231-L401, the client first parses the command to check if it requires a running server (the CMD_STARTSERVER flag). Then it calls client_connect(), which attempts a Unix socket connection. If the connection fails with ECONNREFUSED, the client takes a lock file, calls server_start(), and the server forks into the background.

The server initialization in server.c#L175-L260 is worth studying:

server_proc = proc_start("server");
/* ... */
RB_INIT(&windows);
RB_INIT(&all_window_panes);
TAILQ_INIT(&clients);
RB_INIT(&sessions);
key_bindings_init();

All the global data structures are initialized here, and the server enters its event loop with proc_loop(server_proc, server_loop).

The IPC Layer: imsg Over Unix Sockets

tmux uses OpenBSD's imsg framework for inter-process communication. The proc.c file wraps imsg with two key abstractions:

proc.c#L36-L68

struct tmuxproc represents a process (client or server) and manages signal handlers. struct tmuxpeer represents the other end of a connection and wraps an imsgbuf with a libevent callback. When data arrives on the socket, proc_event_cb reads imsg frames and dispatches them through a callback function pointer.

The message types are defined in tmux-protocol.h#L26-L71 and fall into three groups:

flowchart TD
    subgraph "Identify (100-115)"
        A[MSG_IDENTIFY_FLAGS]
        B[MSG_IDENTIFY_TERM]
        C[MSG_IDENTIFY_TTYNAME]
        D[MSG_IDENTIFY_DONE]
    end
    subgraph "Command (200-218)"
        E[MSG_COMMAND]
        F[MSG_DETACH / MSG_EXIT]
        G[MSG_RESIZE / MSG_SUSPEND]
    end
    subgraph "File I/O (300-307)"
        H[MSG_READ_OPEN / MSG_READ]
        I[MSG_WRITE_OPEN / MSG_WRITE]
    end

When a client connects, it sends a burst of MSG_IDENTIFY_* messages containing its terminal name, TTY path, environment variables, terminal capabilities, and file descriptors for stdin/stdout (sent via sendmsg fd passing). The server uses this information to set up the client's struct tty. After MSG_IDENTIFY_DONE, the server processes the actual command.

Tip: The protocol version (PROTOCOL_VERSION 8) is checked on every message. If client and server versions mismatch, the server sends MSG_VERSION and the client prints "protocol version mismatch" — a common error after upgrading tmux without restarting the server.

The libevent Event Loop

Both client and server are driven by libevent. The server's loop is the more interesting one. After server_start() initializes everything, it calls:

proc_loop(server_proc, server_loop);

The proc_loop() function in proc.c runs event_loop() with EVLOOP_ONCE, then calls the provided callback between iterations. The server callback is server_loop() at server.c#L263-L306:

static int
server_loop(void)
{
    struct client *c;
    u_int items;

    current_time = time(NULL);
    do {
        items = cmdq_next(NULL);            /* drain global queue */
        TAILQ_FOREACH(c, &clients, entry) {
            if (c->flags & CLIENT_IDENTIFIED)
                items += cmdq_next(c);      /* drain per-client queues */
        }
    } while (items != 0);

    server_client_loop();
    /* ... exit checks ... */
}
flowchart TD
    A["event_loop(EVLOOP_ONCE)"] --> B["server_loop() callback"]
    B --> C["Drain global command queue"]
    C --> D["Drain per-client command queues"]
    D --> E{"More items?"}
    E -->|Yes| C
    E -->|No| F["server_client_loop()"]
    F --> G["Check exit conditions"]
    G --> H{"Should exit?"}
    H -->|No| A
    H -->|Yes| I["Cleanup and exit"]

This design means that I/O events (socket reads, PTY data, timers) are handled by libevent, but commands are processed synchronously between event loop iterations. A command can return CMD_RETURN_WAIT to defer, and it will be retried on the next tick.

Global Data Structures at a Glance

tmux organizes its world into a three-tier hierarchy: sessions contain windows (via winlinks), and windows contain panes. All of these are stored in global BSD red-black trees and tail queues:

classDiagram
    class sessions {
        RB_HEAD sessions
    }
    class session {
        +u_int id
        +char *name
        +struct winlinks windows
        +struct options *options
    }
    class winlink {
        +int idx
        +struct window *window
    }
    class window {
        +u_int id
        +struct window_panes panes
        +struct layout_cell *layout_root
    }
    class window_pane {
        +u_int id
        +int fd (PTY)
        +struct screen base
        +struct input_ctx *ictx
    }
    sessions --> session : RB_TREE
    session --> winlink : RB_TREE
    winlink --> window : pointer
    window --> window_pane : TAILQ

There are also three option scopes — server (global_options), session (global_s_options), and window/pane (global_w_options) — initialized in main() at tmux.c#L481-L491. We'll explore these data structures in depth in the next article.

Building for Development

tmux uses autotools. The standard build flow is:

sh autogen.sh
./configure --enable-debug
make

The configure.ac at configure.ac#L1-L53 handles platform detection and selects the right osdep-*.c file. On Linux, osdep-linux.c provides process name lookup via /proc; on macOS, osdep-darwin.c uses different APIs. The selection happens via @PLATFORM@ substitution in Makefile.am#L217:

nodist_tmux_SOURCES = osdep-@PLATFORM@.c

For non-BSD systems, the compat/ directory provides shims for BSD-specific APIs like imsg, strlcpy, and forkpty. Feature flags like ENABLE_SIXEL, HAVE_SYSTEMD, and HAVE_UTF8PROC enable optional functionality.

Tip: Run tmux with -v for basic logging or -vv for verbose debug output (written to /tmp/tmux-*.log). At log level > 1, tty_create_log() dumps raw terminal I/O for deep debugging. You can also toggle logging on a running server by sending SIGUSR2.

What's Next

You now have the architectural roadmap: a single binary, a fork-based client-server split, imsg IPC over Unix sockets, and a libevent main loop that drains command queues each tick. In the next article, we'll dive into the data structures that make tmux tick — the session → winlink → window → pane hierarchy, the layout tree that controls pane geometry, and the vtable pattern used for interactive modes like copy-mode.