Skip to content

Repository files navigation

libtmux for Rust

CI libtmux tmux-mcp MSRV License

Drive tmux from Rust: typed, async control over servers, sessions, windows, and panes — and a query layer that makes "which pane is running the tests?" one expression instead of a parsing problem.

Alpha. Releases carry an -alpha prerelease tag. The API is not settled, and any release may change or remove exported identifiers without a deprecation period. Pin an exact version. Not recommended for production.

You may be looking for:

Is this for you?

You want to Use
Script tmux from Rust, async libtmux
Ask "which pane/window/session matches X?" libtmux query layer, below
Let an AI agent read and drive tmux tmux-mcp
Filter your own structs with the same grammar libtmux-macros
Build a workspace from a tmuxp-style YAML file tmux-workspace

Not for you if you need Windows without WSL — tmux does not run there — or a synchronous-first API. Blocking callers get a runtime, not a mirrored API.

Install

The version has to be written out in full: Cargo does not resolve a prerelease unless the requirement names one, so a plain 0.1 requirement selects nothing.

$ cargo add libtmux@0.1.0-alpha.8
Cargo.toml
[dependencies]
libtmux = "0.1.0-alpha.8"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

Drive tmux

use libtmux::test::TestServer;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // This example runs. `TestServer` is an isolated tmux on its own socket
    // under `/tmp/libtmux-rs-test/`, torn down at the end, so it cannot touch
    // sessions you are using. In your own code, that line is
    // `let server = libtmux::Server::new()?;` and the rest is unchanged.
    let guard = TestServer::new().await?;
    let server = guard.server();

    let session = server.new_session("work").await?;
    let window = session.new_window("editor").await?;
    let pane = window.active_pane().await?.expect("a window has a pane");

    pane.send_keys("echo built").await?;
    pane.send_key_names(["Enter"]).await?;

    assert_eq!(server.sessions().await?.len(), 1);

    guard.shutdown().await?;
    Ok(())
}

The examples on this page run as written, against a throwaway tmux. To run them yourself, enable the test-support feature as a dev-dependency: libtmux = { version = "0.1.0-alpha.8", features = ["test-support"] }.

Everything that reaches tmux is async. Everything that reads an already-taken snapshot is not, so walking a tree you already have costs no round trips.

Query what is there

Typed field handles build the expression, so a comparison that has no meaning for a field is a compile error rather than an empty result:

use libtmux::query::{Filterable as _, QueryIteratorExt as _};
use libtmux::test::TestServer;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let guard = TestServer::new().await?;
    let server = guard.server();
    server.new_session("work").await?;

    let panes = server.panes().await?;
    let fields = libtmux::Pane::filter_fields();

    // `pane_active` is a flag, so `.eq(true)` compiles; `.gt(..)` would not.
    let active = fields
        .pane_current_command
        .starts_with("sh")
        .and(fields.pane_active.eq(true));

    let found = panes.iter().matching(&active).count();
    assert_eq!(found, 1, "the session's one pane is active and running a shell");

    guard.shutdown().await?;
    Ok(())
}

Ask what a session contains, and the relation is part of the expression:

use libtmux::query::{Filterable as _, QueryIteratorExt as _};
use libtmux::test::TestServer;
use libtmux::{SessionTree, WindowTree};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let guard = TestServer::new().await?;
    let server = guard.server();

    let building = server.new_session("ci").await?;
    building.new_window("build").await?;
    server.new_session("idle").await?;

    let sessions = SessionTree::filter_fields();
    let windows = WindowTree::filter_fields();
    let has_build = sessions.windows.any(windows.window.window_name.eq("build"));

    // `hierarchy` gathers the whole tree in three tmux commands, not one
    // per object.
    let matched: Vec<_> = server
        .hierarchy()
        .await?
        .iter()
        .matching(&has_build)
        .map(|branch| branch.session.name().to_string_lossy().into_owned())
        .collect();

    assert_eq!(matched, ["ci"], "only the session holding a `build` window");

    guard.shutdown().await?;
    Ok(())
}

With the serde feature an expression lowers to a versioned JSON envelope, so a CLI, a config file, or an MCP tool call can carry one.

Pick how commands reach tmux

Three switches, each a Cargo feature, none of them the default. The same workload under each — this is cargo run --example matrix --all-features, verbatim:

mode                     feature            dispatches processes    wall  attribution
blocking/sequential      plan                        6         6   168ms  per-command
async/sequential         plan                        6         6   189ms  per-command
async/folded             plan                        3         3    59ms  merged
async/marked-fold        plan                        3         3    57ms  merged
control-mode/streaming   plan,control-mode           6         1     8ms  per-command

every mode built the same thing: true

Same result, different cost — and different evidence. Folding a chain into one tmux invocation halves the dispatches, but tmux reports one status for the group, so a failure cannot be blamed on a member: attribution degrades to unknown by design rather than guessing. Control mode keeps per-command evidence over a single process.

The libtmux guide has the behavior table, the how-to-turn-it-on table, and the named presets.

Drive tmux from an agent

tmux-mcp is a separate package: a Model Context Protocol server over this API, read-biased, where every tool that changes state names exactly what it changes.

$ cargo install tmux-mcp --version 0.1.0-alpha.9

See its README for the tool list and for wiring it into an MCP client.

Requirements

Rust 1.85 or newer, tmux 3.2a or newer, and a Unix target. Native Windows is unsupported because tmux is unavailable there; WSL works.

Every supported tmux release — 3.2a, 3.4, 3.5a, 3.6b, and 3.7b — is built from source in CI and runs the whole workspace, because tmux's own output and error wording change between releases. Where a release is wrong rather than merely old, the crate says so: run_shell refuses on 3.3 through 3.4, which drop the command's output instead of returning it.

Crates

Crate Version What it is
libtmux libtmux The async tmux client and object model
libtmux-macros libtmux-macros #[derive(Filterable)], for your own structs
tmux-mcp tmux-mcp A Model Context Protocol server over tmux
tmux-workspace tmux-workspace A tmuxp-style YAML builder

libtmux does not require proc macros: its own Filterable implementations are hand-written, and the derive exists for structs outside this workspace.

The last two crates are also how the API gets used from outside it. When either needs a workaround, that is treated as a finding about libtmux rather than about them.

Development

just lists every recipe. Cargo is authoritative; the justfile only groups Cargo commands.

$ just check

That runs what CI runs: formatting, Clippy, tests, doctests, docs, the feature powerset, cargo deny, both MSRV builds, and a packaging check.

Tests run against real tmux rather than a mock. libtmux::test::TestServer gives each test an isolated socket under /tmp/libtmux-rs-test/ and deterministic cleanup.

Every Rust example on this page is compiled by cargo test --doc, including the ones in this file. See .github/CONTRIBUTING.md for the gates and the things that will bite, and .github/WRITING.md for how the prose reads.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this workspace by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

Alpha. libtmux for Rust: a typed, async tmux client. Prerelease; the API changes between releases.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages