Core Library0.xHome

Choose documentation 3

latest

Current version

latest
English

Early release libtmux is released, and its APIs may still change between releases. This site's structure and URLs may change too.

Edit this page on GitHub

tmux client library for Rust

Control tmux, connect to and create servers, navigate sessions, windows, and panes, send keys, watch for changes via control mode, capture pane contents.

Install

cargo add libtmux

libtmux 0.1.0 is pre-1.0 and can change between minor versions. Pin to a range:

libtmux = "0.1"

At a glance

Server → Session → Window → Pane. Each level of the tmux hierarchy has its own entry in the Rust API, with the tmux object it wraps.

TypeWraps
server.Server A tmux server
session.Session A session, ID $n
window.Window A window in a session, ID @n
pane.Pane A pane in a window, ID %n

What it does

Capability: Navigate tmux via a typed object graph
Example details
use libtmux::{PaneWait, Server};
use std::{error::Error, time::Duration};
type ExampleError = Box<dyn Error>;
async fn work(server: &Server) -> Result<(), ExampleError> {
let session = server.new_session("work").await?;
let window = session.new_window("editor").await?;
let pane = window.active_pane().await?.ok_or("missing pane")?;
pane.send_line("printf '\\nhello\\n'").await?;
let seen = pane
.wait_until(Duration::from_secs(5), |lines| {
lines.iter().any(|line| *line == "hello")
})
.await?;
if seen != PaneWait::Arrived {
return Err("The pane did not print hello".into());
}
for line in pane.capture().await? {
if line == "hello" {
println!("{}", line.to_string_lossy());
}
}
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("home-").tempdir_in(root)?.keep();
let server = Server::builder()
.socket_path(directory.join("tmux.sock"))
.config_file("/dev/null")
.default_timeout(Duration::from_secs(5))
.build()?;
let outcome =
tokio::time::timeout(Duration::from_secs(10), work(&server))
.await;
server.shutdown().await?;
outcome??;
Ok(())
}
use libtmux::{PaneWait, Server};
use std::{error::Error, time::Duration};
type ExampleError = Box<dyn Error>;
async fn work(server: &Server) -> Result<(), ExampleError> {
let session = server.new_session("work").await?;
let window = session.new_window("editor").await?;
let pane = window.active_pane().await?.ok_or("missing pane")?;
pane.send_line("printf '\\nhello\\n'").await?;
let seen = pane
.wait_until(Duration::from_secs(5), |lines| {
lines.iter().any(|line| *line == "hello")
})
.await?;
if seen != PaneWait::Arrived {
return Err("The pane did not print hello".into());
}
for line in pane.capture().await? {
if line == "hello" {
println!("{}", line.to_string_lossy());
}
}
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("home-").tempdir_in(root)?.keep();
let server = Server::builder()
.socket_path(directory.join("tmux.sock"))
.config_file("/dev/null")
.default_timeout(Duration::from_secs(5))
.build()?;
let outcome =
tokio::time::timeout(Duration::from_secs(10), work(&server))
.await;
let closed = server.shutdown().await;
let mut failures = Vec::new();
match outcome {
Ok(Ok(())) => {}
Ok(Err(error)) => failures.push(format!("work: {error}")),
Err(error) => failures.push(format!("deadline: {error}")),
}
if let Err(error) = closed {
failures.push(format!("close executor: {error}"));
}
if failures.is_empty() {
Ok(())
} else {
Err(failures.join("; ").into())
}
}
use libtmux::{PaneWait, Server};
use std::{error::Error, time::Duration};
type ExampleError = Box<dyn Error>;
async fn work(server: &Server) -> Result<(), ExampleError> {
let session = server.new_session("work").await?;
let window = session.new_window("editor").await?;
let pane = window.active_pane().await?.ok_or("missing pane")?;
pane.send_line("printf '\\nhello\\n'").await?;
let seen = pane
.wait_until(Duration::from_secs(5), |lines| {
lines.iter().any(|line| *line == "hello")
})
.await?;
if seen != PaneWait::Arrived {
return Err("The pane did not print hello".into());
}
for line in pane.capture().await? {
if line == "hello" {
println!("{}", line.to_string_lossy());
}
}
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("home-").tempdir_in(root)?;
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), work(&server))
.await;
let killed = server.kill().await;
let closed = server.shutdown().await;
if killed.is_err() || closed.is_err() {
directory.keep();
} else {
directory.close()?;
}
killed?;
closed?;
outcome??;
Ok(())
}
use libtmux::{PaneWait, Server};
use std::{error::Error, time::Duration};
type ExampleError = Box<dyn Error>;
async fn work(server: &Server) -> Result<(), ExampleError> {
let session = server.new_session("work").await?;
let window = session.new_window("editor").await?;
let pane = window.active_pane().await?.ok_or("missing pane")?;
pane.send_line("printf '\\nhello\\n'").await?;
let seen = pane
.wait_until(Duration::from_secs(5), |lines| {
lines.iter().any(|line| *line == "hello")
})
.await?;
if seen != PaneWait::Arrived {
return Err("The pane did not print hello".into());
}
for line in pane.capture().await? {
if line == "hello" {
println!("{}", line.to_string_lossy());
}
}
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("home-").tempdir_in(root)?;
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), work(&server))
.await;
// kill stops our daemon; shutdown closes this 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!("work: {error}")),
Err(error) => failures.push(format!("deadline: {error}")),
}
if let Err(error) = killed {
failures.push(format!("stop owned daemon: {error}"));
}
if let Err(error) = closed {
failures.push(format!("close executor: {error}"));
}
if cleanup_failed {
let retained = directory.keep();
failures.push(format!("inspect {}", retained.display()));
} else if let Err(error) = directory.close() {
failures.push(format!("remove directory: {error}"));
}
if failures.is_empty() {
Ok(())
} else {
Err(failures.join("; ").into())
}
}
Run this example

POSIX shell, tmux 3.3a or newer, rustup with Rust 1.97.1 (rust-toolchain.toml), and a C linker.

Save the program above as src/main.rs in a new directory, along with the files below.

Cleanup is omitted; the private tmux server and its temporary files may remain. Select Cleanup to remove them when the program finishes. The program stops its private tmux server and removes any temporary files it created when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.

Cargo.toml
[package]
name = "libtmux-home-quickstart"
version = "0.0.0"
edition = "2024"
publish = false
[dependencies]
libtmux = "0.1.0"
tempfile = "3.20"
tokio = { version = "1.40", features = ["macros", "rt", "time"] }
Cargo.lock
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
dependencies = [
"memchr",
]
[[package]]
name = "bitflags"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "bytes"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "caseless"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8"
dependencies = [
"unicode-normalization",
]
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "libc"
version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]]
name = "libtmux"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5a6ac3b0304461e4d8825397b931cbaab705e9cacfcc485974ff5a4252fffb6"
dependencies = [
"caseless",
"regex",
"rustix",
"thiserror",
"tokio",
]
[[package]]
name = "libtmux-home-quickstart"
version = "0.0.0"
dependencies = [
"libtmux",
"tempfile",
"tokio",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "mio"
version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
dependencies = [
"libc",
"wasi",
"windows-sys",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "regex"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rustix"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "thiserror"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinyvec"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
[[package]]
name = "tokio"
version = "1.54.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
dependencies = [
"bytes",
"libc",
"mio",
"pin-project-lite",
"signal-hook-registry",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "unicode-normalization"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d"
dependencies = [
"tinyvec",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
rust-toolchain.toml
[toolchain]
channel = "1.97.1"
components = ["clippy", "rust-src", "rustfmt"]
profile = "minimal"

Setup and run

cargo build --locked --jobs 2
unset TMUX TMUX_PANE
export ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/sh
cargo run --quiet --locked

Expected output

hello

Library revision: ce7610a3b463.

crates/libtmux/examples/api_input.rs
//! Send literal text and a named Enter key to a ready shell.
use std::error::Error;
use std::time::Duration;
use libtmux::{NewSessionOptions, PaneWait, Server};
type ExampleError = Box<dyn Error>;
async fn demonstrate(server: &Server) -> Result<(), ExampleError> {
let session = server
.new_session(
NewSessionOptions::new("input").command("env ENV=/dev/null PS1='api-ready> ' sh"),
)
.await?;
let panes = session.panes().await?;
let pane = panes.first().ok_or("session has no pane")?;
if pane
.wait_for_text("api-ready>", Duration::from_secs(5))
.await?
!= PaneWait::Arrived
{
return Err("shell prompt did not become ready".into());
}
pane.send_keys("printf '\\nC-c is text\\n'").await?;
pane.send_key_names(["Enter"]).await?;
loop {
let lines = pane.capture().await?;
if lines.iter().any(|line| line.as_bytes() == b"C-c is text") {
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
println!("input: C-c is text");
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("api-input-")
.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())
}
}

The whole program, crates/libtmux/examples/api_input.rs. The port's CI runs it in .github/workflows/ci.yml: just check.

crates/libtmux/examples/api_creation.rs
//! Create a session and window, then split a window and a pane.
use std::error::Error;
use std::time::Duration;
use libtmux::{NewSessionOptions, NewWindowOptions, Server, SplitDirection, SplitOptions};
type ExampleError = Box<dyn Error>;
fn check(condition: bool, message: &str) -> Result<(), ExampleError> {
if condition {
Ok(())
} else {
Err(message.into())
}
}
async fn demonstrate(server: &Server) -> Result<(), ExampleError> {
let session = server
.new_session(
NewSessionOptions::new("work")
.window_name("editor")
.command("cat"),
)
.await?;
let window = session
.new_window(NewWindowOptions::new("logs").command("cat"))
.await?;
let pane = window
.split(SplitOptions::new(SplitDirection::Below).command("cat"))
.await?;
pane.split(SplitOptions::new(SplitDirection::Right).command("cat"))
.await?;
check(
session.windows().await?.len() == 2,
"expected two session windows",
)?;
check(
window.panes().await?.len() == 3,
"expected three panes in split window",
)?;
check(
server.panes().await?.len() == 4,
"expected four panes on server",
)?;
println!("creation: 2 windows, 4 panes");
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("api-creation-")
.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())
}
}

The whole program, crates/libtmux/examples/api_creation.rs. The port's CI runs it in .github/workflows/ci.yml: just check.

Typed API

What you get over the tmux CLI

Know where you're running

Inside tmux, find the pane your program runs in from TMUX and TMUX_PANE: pane.Pane.from_env.

Testing

Run tests against an isolated tmux server: test.TestServer.

Esc

Type to search.

Reference previewOpen reference