A terminal multiplexer as a reusable C++20 library. A server process owns PTY sessions and runs their terminal emulators. Terminals and UIs attach to it as clients: they receive a screen snapshot and incremental dirty-row diffs, and send input and resizes. When a UI quits, crashes or restarts, its sessions keep running, and reattaching restores the state.
In the Bro ecosystem,
bromux provides session persistence and multiplexing for the <terminal>
element and broterm. It builds on
bropty for terminal emulation and PTY
management, and brosearch for regex
search. Compressed inline images can optionally be decoded with
broimage. bromux does not depend on bro or
bronze; any terminal or GUI can embed the client library or use the CLI standalone.
Platform support is verified in continuous integration across GCC, Clang, and MSVC:
| Platform | Compiler | Local IPC | Event Loop | PTY Substrate |
|---|---|---|---|---|
| Linux (x86-64, AArch64) | GCC 12+, Clang 16+ | Unix domain sockets (mode 0600 in 0700 dir, SO_PEERCRED verification) |
poll + self-pipe |
bropty POSIX PTY |
| Windows (x86-64) | MSVC 2022+ | Named Pipes (\\.\pipe\bromux-<SID>-<name>, restricted DACL, remote clients rejected) |
I/O Completion Ports (IOCP) | bropty ConPTY |
| macOS (Apple Silicon, Intel) | Apple Clang | Unix domain sockets (mode 0600, LOCAL_PEERCRED verification) |
poll + self-pipe |
bropty POSIX PTY |
- Server-side emulation. The server runs a bropty
Terminalper session. Clients receive state (rows, cursor, modes, title, palette), never a raw byte replay. Reattaching sends one snapshot. Updates are dirty-row diffs computed per client, with scroll detection. Scrollback is fetched on demand. - Many clients per session, under a resize policy (latest / smallest / largest / fixed). Clients can also attach read-only or no-resize.
- Forwarded events: title, cwd (OSC 7), bell, notifications (OSC 9 / 777), progress, OSC 133 marks, and OSC 52 clipboard writes and queries behind a per-session policy.
- Flow control:
- A flooding session gets a bounded slice per loop turn, so it cannot starve others.
- A slow or stalled client gets fewer, larger frames: each frame is a diff to the latest state, under an acknowledgement window, so it is never sent a backlog.
- A dead client costs nothing.
- Local IPC security:
- Unix sockets: mode 0600 in a 0700 directory, with peer credentials (
SO_PEERCRED/LOCAL_PEERCRED) verified. - Windows named pipes: a DACL admitting only the current user SID, remote callers rejected via
PipeMode::RejectRemoteClients, and the server process owner verified by the client.
- Unix sockets: mode 0600 in a 0700 directory, with peer credentials (
- Remote attach:
ssh host bromux proxyrelays bytes to the remote host's local server. Security is ssh's; bromux adds no crypto and no libssh dependency. - Daemon lifecycle:
- The first client auto-starts the server, which runs detached.
- The server exits cleanly after an idle timeout (no clients and no active sessions).
- Race-free start: multiple simultaneous servers resolve to exactly one owner, and orphaned or crashed server sockets are safely reclaimed.
- Self-contained event loop: poll plus a self-pipe on POSIX, IOCP on Windows. No libuv, no Asio, no external dependencies.
- Documented protocol: Versioned and length-prefixed binary framing. See docs/protocol.md.
bromux requires bropty and, through it, brosearch. CMake resolves each sibling in this order:
- An existing target already defined in a parent superbuild (e.g.
bro). - Sibling checkouts beside the top-level project (
../bropty,../brosearch, or-DBROPTY_DIR=<path>/-DBROSEARCH_DIR=<path>). - Flat vendored submodules under
third_party/(third_party/bropty,third_party/brosearch).
Optionally, broimage (with bromath) can be provided at ../broimage or via superbuild target to decode compressed Kitty and iTerm2 inline images.
# Sibling layout (clone side by side):
git clone https://github.com/wlejon/brosearch
git clone https://github.com/wlejon/bropty
git clone https://github.com/wlejon/bromux
# Or single checkout with flat submodules:
git clone https://github.com/wlejon/bromux
cd bromux && git submodule update --init --recursive
# Linux / macOS (Ninja)
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failure -j 1
# Windows (MSVC / Visual Studio 2022)
cmake -B build
cmake --build build --config Release
ctest --test-dir build -C Release --output-on-failure -j 1The build produces:
bromux: Static library (CMake targetbromux::bromuxorbromux).bromux_cli: Server, SSH proxy, and command-line client executable (binary namedbromux).
Consumers embed bromux by including its directory and linking against bromux::bromux:
my_project/
third_party/
bromux/
bropty/
brosearch/
In your CMakeLists.txt:
add_subdirectory(third_party/bromux)
target_link_libraries(my_terminal PRIVATE bromux::bromux)Configuration options:
BROMUX_BUILD_TESTS: Build the test suite (defaultONwhen top-level,OFFwhen embedded viaadd_subdirectory).BROMUX_WITH_BROIMAGE: Decode compressed inline images via broimage if found (defaultON).BROMUX_COVERAGE: Build with gcov coverage instrumentation on GCC/Clang (defaultOFF).
| Header | Role |
|---|---|
bromux/client.h |
Client, ConnectOptions, SessionSpec, ClientEvent, ScreenModel |
bromux/server.h |
Server, ServerConfig, daemon lifecycle, session and client management |
bromux/stream.h |
IStream, local Unix domain socket, Windows named pipe, and SSH proxy streams |
bromux/screen_model.h, bromux/screen_source.h |
Client-side screen state and bropty::RowSource adapter for viewport, search, and selection |
bromux/protocol.h, bromux/wire.h |
Wire protocol messages, framing, and serialization |
bromux/tee.h |
Session input/output recording and verification replay |
#include <bromux/client.h>
#include <iostream>
int main() {
bromux::ConnectOptions opt; // default server; auto-started if not running
std::string err;
auto c = bromux::Client::connect(opt, &err);
if (!c) {
std::cerr << "Connect failed: " << err << "\n";
return 1;
}
// Remote connect alternative:
// auto c = bromux::Client::connect_ssh({.host = "me@box"}, opt, &err);
bromux::SessionSpec spec; // empty command launches user's default shell
spec.meta = {{"name", "build"}};
auto info = c->create_session(spec);
c->attach(info->id, 120, 40);
std::vector<bromux::ClientEvent> events;
for (;;) {
c->wait(std::chrono::milliseconds(16));
events.clear();
c->dispatch(events); // applies incoming frames, sends acknowledgements
const bromux::ScreenModel* m = c->screen(info->id);
if (m) {
// m->row(y) provides a bropty::RowView; m->cursor(), m->modes(), m->title() ...
}
}
}- Input & Resizing: Driven through
send_key,send_text,paste,send_mouse,focus, andresize. - History Access:
fetch_history(session, start, count)reads scrollback asynchronously. Rows carry absolute identifiers surviving eviction (ScreenModel::history_first_row(),history_epoch()). - Terminal Views:
view(session)returns abropty::TerminalViewover the session: viewports, selection, copy, regex search, and hyperlinks function identically to a localbropty::Terminal. - Thread Safety:
Clientis not thread-safe and is designed to run on the UI/render thread; internal stream readers operate asynchronously.
bromux server [-L name|--socket addr] [--daemon] [--idle-exit SEC] [--log FILE]
[--tee-dir DIR] [--clipboard deny|write|readwrite]
bromux proxy [-L name|--socket addr] # remote end invoked by ssh
bromux ls | new | attach | kill | kill-server [target] ...
bromux version
target: -L name | --socket addr | --ssh [user@]host [--remote-bromux PATH] [--ssh-arg ARG]...
bromux new -- htop: Spawns a session and outputs its numeric ID.bromux attach 1: Attaches to session 1 in the current terminal (Ctrl-]detaches).bromux ls --ssh me@box: Lists running sessions on remote hostbox.
| Platform | Default endpoint |
|---|---|
| POSIX | $XDG_RUNTIME_DIR/bromux/<name>.sock, fallback ${TMPDIR:-/tmp}/bromux-<uid>/<name>.sock |
| Windows | \\.\pipe\bromux-<user SID>-<name> |
The default server name is default. Override with -L name or set BROMUX_SOCKET.
Every test is a real ctest executable that fails in Release builds (no reliance on assert()).
| Test | Coverage |
|---|---|
test_wire |
Binary primitives, framing, all message types, fuzzed input |
test_codec |
Row and screen diff codecs against random terminals, scroll operations, malformed packets |
test_source |
ScreenSource and bropty::TerminalView over remote streams, asynchronous history arrivals, epoch numbering, eviction |
test_server |
Session management, input routing, resize policies, forwarded events, clipboard policies, metadata/blobs, frame diffs, disconnects |
test_oracle |
Random VT streams and real shells under multi-client churn; vi session testing on POSIX |
test_flow |
Flood fairness across sessions, stalled client acknowledgement windows, client process abrupt kills |
test_daemon |
Daemon auto-start, background persistence, idle exit timeout, concurrent start races, crashed server cleanup, CLI and proxy operations |
test_remote |
End-to-end oracle stream verification over live ssh host bromux proxy |
test_remoteskip conditions:-
test_remoteis skipped by default (exits with code 77) unless theBROMUX_TEST_SSHenvironment variable specifies a target host:Variable Purpose BROMUX_TEST_SSHTarget [user@]hostBROMUX_TEST_SSH_BROMUXOptional absolute path to bromuxbinary on remote hostBROMUX_TEST_SSH_CHILDOptional path to mux_childtest helper on remote hostBROMUX_TEST_SSH_ARGSOptional extra ssh command-line arguments -
Windows CI: Skipped on Windows runners because the test driver manages session recordings via a POSIX shell (
mkdir,cat, andrmunder/tmp), which standard Windows runners lack. -
Linux and macOS CI: Automatically enabled via
.github/ci/ssh-localhost.sh, which configures an isolated localhost-only SSH key pair with forwarding and PTY disabled, runs the localsshd, and executestest_remoteover loopback.
-
- Process isolation:
- End-to-end server tests run serially (
-j 1in CI) to ensureprocess_censuscan strictly verify that no orphaned daemon servers or child PTY processes are left running after test execution.
- End-to-end server tests run serially (
MIT; see LICENSE.