Control mode vs one-shot
libtmux sends commands to tmux through subprocesses or persistent control-mode connections. tmux also accepts several commands in one invocation:
- One-shot subprocess. Each command spawns a fresh
tmuxprocess, which sends the request to the server, prints the result, and exits. - A persistent control-mode client.
tmux -C attach-sessionstarts 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. - One invocation, several commands. tmux accepts more than one command
per invocation (
;-joined, or one-F-taggedlist-*per line). Grouping operations this way reduces process starts without opening a control-mode connection.
Available transports¶
Python¶
Each library call starts a tmux subprocess. ControlMode in
libtmux._internal is an internal test helper, not a public command transport.
TypeScript¶
Commands use a subprocess by default. pipeline() and batch() group
operations. connect() and watch() keep a connection for notifications;
ordinary commands still use separate processes.
Go¶
The process transport starts a tmux subprocess for each call. Plan.Run
groups commands into fewer invocations. Use Session.OpenControl for a
persistent command connection and Session.OpenNotifications for a notification
stream.
Rust¶
Commands use subprocesses by default. CommandChain and the planning API
group commands; the control-mode feature enables persistent connections.
C#¶
The default one-shot mode runs commands through subprocesses. Server.Chain
groups commands, and EnterControlModeAsync opens a persistent command
connection.
C++¶
The default transport uses bounded subprocesses. Chain groups commands
into an invocation. Server::control() opens a persistent Connection.
Java¶
Ordinary calls run through tmux subprocesses. Batch groups commands,
and ControlClient provides a persistent connection for commands and events.
Swift¶
Ordinary calls use subprocesses. Server.connected(attachingTo:_:) provides
a persistent connection, and ControlConnection.watch(_:) receives events.
Choose based on whether you need command results, notifications, or a batch of changes.
Notifications and commands are separable¶
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.
Session.OpenControl opens a persistent command connection. Session.OpenNotifications
opens a notification stream. Close each handle when finished. Starting an
observer does not change the transport used by an existing server handle.
Use EnterControlModeAsync for commands on a persistent connection.
ControlClient.send sends commands through the persistent connection.
Enable the control-mode feature to send commands through a persistent
connection.
A control client is a real client¶
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 invocation¶
Creating an object can require a second command to read its resulting state. Batching can reduce repeated reads and process starts.
batch() resolves planned mutations from one final snapshot.
A Plan groups operations without attaching a control client.
A Chain groups operations without attaching a control client.
What this costs in practice¶
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.
Sending a command¶
The example uses the subprocess API. See the reference for batching and control-mode setup.
import libtmux
# One-shot: every call underneath this handle spawns a `tmux` process.server = libtmux.Server()session = server.new_session(session_name="work")session.active_window.active_pane.send_keys("echo hello")import { Server } from "libtmux";
// Each awaited command uses a tmux subprocess.const server = new Server();const session = await server.newSession({ name: "work" });const editor = await session.newWindow({ name: "editor" });await editor.panes.at(0)?.sendKeys("echo hello");use libtmux::Server;
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { // One-shot: every call underneath this handle spawns a `tmux` process. let server = Server::new()?; let session = server.new_session("work").await?; let window = session.active_window().await?.expect("a session has a window"); let pane = window.active_pane().await?.expect("a window has a pane"); pane.send_line("echo hello").await?; Ok(())}ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()
// One-shot: every call underneath this handle spawns a `tmux` process.server, err := tmux.NewServer(tmux.ServerOptions{})if err != nil { return err}session, err := server.NewSession(ctx, tmux.NewSessionRequest{Name: "work"})if err != nil { return err}window, err := session.ResolveActiveWindow(ctx)if err != nil { return err}pane, err := window.ResolveActivePane(ctx)if err != nil { return err}command := "echo hello"return pane.SendKeys(ctx, tmux.SendKeysRequest{Command: &command, Literal: true})ServerConfig config = ServerConfig.builder() .endpoint(ServerEndpoint.defaultSocket()) .build();
// One-shot: every call underneath this handle spawns a `tmux` process.try (Server server = Server.open(config)) { Session session = server.newSession("work"); Pane pane = session.windows().get(0).panes().get(0); pane.sendLine("echo hello");}using LibTmux;
// One-shot: every call underneath this handle spawns a `tmux` process.Server server = await Server.ConnectAsync();Session session = await server.CreateSessionAsync(new NewSessionRequest(name: "work"));Window window = (await session.GetWindowsAsync())[0];Pane pane = (await window.GetPanesAsync())[0];
await pane.SendTextAsync("echo hello");#include <libtmux/libtmux.hpp>
// One-shot: every call answers with a value; no tmux failure is thrown.const auto server = libtmux::Server::at_default();if (!server.has_value()) { return 1;}
const auto session = server->new_session("work");if (!session.has_value()) { return 1;}
const auto pane = session->active_pane();if (pane.has_value()) { (void)pane->send_text("echo hello"); (void)pane->send_key("Enter");}import LibTmux
// One-shot: every call underneath this handle spawns a `tmux` process.let server = try Server(socketName: "default")let session = try await server.newSession(named: "work", windowName: "editor")let window = try await server.newWindow(in: session, named: "logs").windowlet pane = try await server.splitWindow(window, direction: .right)try await server.run("echo hello", in: pane)To inspect tmux's control protocol, attach a control client:
$ tmux -C attach-session -t work