Core LibraryalphaConcepts

Choose documentation 3

latest

Current version

latest
English

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

Edit this page on GitHub

Subprocesses, plans, and control mode

Ordinary Server calls use tmux subprocesses. No optional Cargo feature is required for that route. Add plan to record and group commands, or control-mode to use a persistent connection for commands and notifications.

Available transports

Your taskRust APICargo feature
List objects, create a session, send keys, or capture a paneOrdinary Server, Session, Window, and Pane methodsNone
Send a known command sequence in one invocationServer::chain(CommandChain)None
Describe operations, validate dependencies, and choose how to group themPlan with Planner::Sequential, Folding, or Markedplan
Attach a persistent command and event connectionControlMode::attachcontrol-mode
Route typed object methods through that connectionServer::over_control_modecontrol-mode

Choose based on whether your application needs occasional command results, a known batch of changes, or an ongoing connection. Start with ordinary calls unless another route solves a measured problem. Fewer process starts alone do not establish an end-to-end latency improvement.

Ordinary calls and command results

A Server is a handle to a tmux endpoint. Constructing it does not start a daemon; creating the first session does. A typed call can dispatch more than one command, for example to read the state of an object it just created. Do not equate one Rust method call with exactly one subprocess.

Typed operations report their documented failures through Result. The lower level Server::cmd instead returns a CommandResult: reaching tmux and tmux accepting the command are separate outcomes. Check success() or the exit status before treating its output as a successful operation.

The complete program below lists its session, then asks whether a missing session exists. cmd() returns Ok with an unsuccessful command result for the refused has-session; a missing executable, timeout, or capture failure would return Err instead.

Setup and run

Use an empty directory with Git, rustup with the 1.97.1 toolchain installed, tmux 3.3a or newer, and a Unix environment. The program creates a private socket under /tmp/libtmux-rs-dev, starts cat in its panes, and stops only the server it created. No existing session, socket, or environment variable is required.

Save this file as Cargo.toml:

Cargo.toml
[package]
name = "libtmux-subprocess-example"
version = "0.0.0"
edition = "2024"
publish = false
[workspace]
exclude = ["libtmux-source"]
[dependencies]
libtmux = { path = "libtmux-source/crates/libtmux", default-features = false }
tempfile = "=3.27.0"
tokio = { version = "=1.53.1", features = ["macros", "rt", "time"] }
[[bin]]
name = "subprocess"
path = "subprocess.rs"

Fetch the library revision used by the example:

Terminal window
$ git clone https://github.com/libtmux/libtmux-rs libtmux-source &&
git -C libtmux-source checkout e9be0b6f6d22cd2eb79b0ec08964f82e717e5fe4

This dependency disables all default features to show that subprocess calls do not depend on query, plan, or control-mode.

Save the complete program as subprocess.rs:

subprocess.rs
use std::error::Error;
use std::time::Duration;
use libtmux::{Command, NewSessionOptions, Server};
type ExampleError = Box<dyn Error>;
async fn demonstrate(server: &Server) -> Result<(), ExampleError> {
server
.new_session(NewSessionOptions::new("work").command("cat"))
.await?;
let sessions = server.sessions().await?;
if sessions.len() != 1 || sessions[0].name().as_bytes() != b"work" {
return Err("expected only the work session".into());
}
println!("sessions: work");
// Transport success and a successful tmux command are separate results.
let missing = server
.cmd(Command::new("has-session").arg("-t").arg("missing"))
.await?;
if missing.success() {
return Err("the missing session unexpectedly exists".into());
}
println!("missing session: tmux refused the command");
Ok(())
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), ExampleError> {
let root = std::path::Path::new("/tmp/libtmux-rs-dev");
std::fs::create_dir_all(root)?;
let directory = tempfile::Builder::new()
.prefix("docs-subprocess-")
.tempdir_in(root)?;
std::fs::write(
directory.path().join("owner"),
std::process::id().to_string(),
)?;
let server = Server::builder()
.socket_path(directory.path().join("tmux.sock"))
.config_file("/dev/null")
.default_timeout(Duration::from_secs(5))
.build()?;
let outcome = tokio::time::timeout(Duration::from_secs(10), demonstrate(&server)).await;
// Stop the owned daemon before closing the client executor.
let killed = server.kill().await;
let closed = server.shutdown().await;
let cleanup_failed = killed.is_err() || closed.is_err();
let mut failures = Vec::new();
match outcome {
Ok(Ok(())) => {}
Ok(Err(error)) => failures.push(format!("example failed: {error}")),
Err(error) => failures.push(format!("example deadline: {error}")),
}
if let Err(error) = killed {
failures.push(format!("daemon cleanup: {error}"));
}
if let Err(error) = closed {
failures.push(format!("executor cleanup: {error}"));
}
if cleanup_failed {
let retained = directory.keep();
failures.push(format!("inspect retained directory {}", retained.display()));
} else if let Err(error) = directory.close() {
failures.push(format!("directory cleanup: {error}"));
}
if failures.is_empty() {
Ok(())
} else {
Err(failures.join("; ").into())
}
}
Terminal window
$ cargo +1.97.1 run --quiet --bin subprocess

Expected output:

sessions: work
missing session: tmux refused the command

Group a known sequence

Server::chain sends a CommandChain as one invocation. It gives the group one exit status and merged output. tmux stops at the first refused command; it does not undo earlier commands.

A Plan adds recorded operations, typed references to objects created earlier in the plan, validation, and per-operation reports. The planner can retain separate invocations or combine compatible neighbors. A failed combined invocation may leave individual outcomes Unknown. Ok(PlanResult) therefore does not mean every operation succeeded.

Batch commands with a plan runs one workload with each planner, checks the resulting pane output, and handles a refused operation. It explains which counts and failure details the result can prove.

Notifications and commands are separable

Attaching ControlMode opens another real tmux client. It appears in list-clients and affects attachment counts, client hooks, and options such as destroy-unattached.

The original Server continues to use subprocesses. Calling over_control_mode() returns another handle; typed objects obtained through that handle inherit its route. The sender must address the same endpoint. Setup may probe the tmux version through the process route; later routed calls use the connection.

Send commands over control mode provides a complete program that sends input, captures output, drains notifications, and closes the connection while keeping the daemon alive until final cleanup.

Deadlines, partial effects, and cleanup

ServerBuilder::default_timeout bounds ordinary subprocess commands and the opening control handshake. A connected sender has its own reply_timeout, initially copied from that server limit. The control guide sets it explicitly.

A timeout or cancelled future does not roll back an accepted mutation. Inspect the server before repeating a creation or another operation that is unsafe to run twice. A plan can be partly applied, and dropping its future also loses the returned report of which steps completed.

Server::shutdown closes the client executor and its connections; it does not kill the daemon. Use Server::kill only when your application owns that daemon. The example bounds each command to five seconds and its work to ten seconds, then attempts both daemon and executor cleanup. A cleanup failure reports and retains the private directory for inspection.

Read the implementation contract

These examples use the public APIs at libtmux@0.1.0-alpha.15. The pinned Server contract describes command outcomes, shutdown, and routing. The guides above explain plans and persistent connections as separate application workflows.

Esc

Type to search.

Reference preview Open reference