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.
Quickstart
Install the package and run a complete program.
Guides
Task guides for Rust.
Topics
Subject-by-subject, with Rust in every code block.
Examples
Complete programs, sourced from the code each port tests.
API reference
Types and functions of the Rust API.
Testing
Test code that uses tmux with an isolated server.
Install
cargo add libtmuxlibtmux 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.
| Type | Wraps |
|---|---|
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
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 2unset TMUX TMUX_PANEexport ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/shcargo run --quiet --lockedExpected output
helloLibrary revision: ce7610a3b463.
//! 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.
//! 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.
APIs used
PaneWaitServerServer::new_sessionSession::new_windowWindow::active_panePane::send_linePane::wait_untilPane::captureServer::builderServerBuilder::build- +2 more
Typed API
- Typed objects instead of parsed format strings:
pane.Pane - Addressing by tmux ID:
target.PaneId - Filtering and queries over tmux objects:
query.FilterExpr - Typed errors instead of exit codes and stderr:
error.Error
What you get over the tmux CLI
- Connect to or start a tmux server:
server.Server - Traversal from a server down to its panes:
server.Server.panes - Send keys to a pane:
pane.Pane.send_keys - Watch for changes through tmux control mode:
control.ControlMode - Capture pane contents:
pane.Pane.capture - A typed error when the running tmux lacks a feature:
error.Error.UnsupportedCapability - Async operations:
server.Server.new_session
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.