Prerelease This site documents an alpha of libtmux. Its structure, URLs and APIs are subject to change.

Edit this page on GitHub

libtmux sends commands to tmux through subprocesses or persistent control-mode connections. Some ports also batch commands into one invocation:

  1. One-shot subprocess. Each command spawns a fresh tmux process, which parses argv, executes the command, prints its output, and exits. This is the default in every port, and the only lane in Python: every .cmd() call underneath the object API is a subprocess.Popen around a tmux invocation.
  2. A persistent control-mode client. tmux -C attach-session starts one long-lived tmux process that stays attached and speaks a line-oriented protocol over its stdout: commands go in, replies and asynchronous notifications (%window-add, %output, …) come out, without starting a process per call.
  3. One invocation, several commands. tmux accepts more than one command per invocation (;-joined, or one -F-tagged list-* per line). A port can fold several logical operations into a single process start without opening a control-mode connection at all.

Where each port draws the lineLink to section

PortOne-shotFolded invocationPersistent control client
Pythonevery call-test-only (ControlMode, libtmux._internal)
TypeScriptdefaultpipeline(), batch()connect() / watch(): notifications only, commands stay per-process
Goprocess pathPlan.Runconnection (Session.OpenControl), streaming (OpenNotifications)
Rustplan feature, sequentialplan, foldedcontrol-mode feature
C#“One-shot” mode“Chained” mode (server.Chain())“Control” mode (EnterControlModeAsync)
C++bounded subprocess (default)ChainServer::control() → Connection
Javaevery callBatchControlClient (attach, send, subscribeEvents)
Swiftdefault-server.connect() / .watch() (notifications; see below)

Choose based on whether you need command results, notifications, or a batch of changes.

Notifications and commands are separableLink to section

TypeScript’s connect() adds an event observer while commands such as session.newWindow(...) and pane.sendKeys(...) continue to run as separate tmux processes. A dedicated process provides a completion boundary for output from alias-expanded or waiting commands.

Go’s Session.OpenControl, .NET’s EnterControlModeAsync, Java’s ControlClient.send, and Rust’s control-mode feature can send commands through the persistent connection. Check your port’s transport API before assuming that subscribing to events also changes how commands run.

A control client is a real clientLink to section

A persistent control connection attaches a tmux client. It appears in list-clients, increments session_attached, and affects destroy-unattached, client hooks, and idle-client accounting. Each connection counts separately. Python’s internal ControlMode test helper uses this behavior for commands that require an attached client, such as display-popup and detach-client.

Why fold several commands into one invocationLink to section

Creating an object can require a second command to read its resulting state. Batching reduces those repeated reads and process starts. TypeScript’s batch() resolves planned mutations from one final snapshot. Go’s Plan, .NET’s Chain, and C++‘s Chain also group operations without attaching a control client.

What this costs in practiceLink to section

The Rust matrix example and .NET README compare process counts and timings. Their results describe specific workloads and environments:

  • Rust’s matrix example runs the same create-and-query workload five ways. Blocking sequential and async sequential both cost 6 processes for 6 dispatches; folding the same 6 into async batches costs 3 processes; routing them over a control-mode connection costs exactly 1.
  • C#‘s README reports the marginal cost of one more command in each mode, as medians against tmux 3.7b: roughly 2.3 ms for another one-shot process, roughly 0.2 ms for another command over an already-open control client, and roughly 0.02 ms for another command folded into one chained invocation.

A persistent connection avoids starting a client for each command. A chain groups a known sequence into one invocation. For occasional commands, use the default subprocess transport; measure your workload before changing transports for performance. Use a notification stream when your program needs tmux events.

Choosing a laneLink to section

These examples use each port’s subprocess API. See the port reference for batching and control-mode setup.

To inspect tmux’s control protocol, attach a control client:

Terminal window
$ tmux -C attach-session -t work
Esc

Type to search.