Skip to main content

Crate libtmux

Crate libtmux 

Source
Expand description

§libtmux

crates.io docs.rs MSRV

Drive tmux from Rust: typed, async control over servers, sessions, windows, and panes.

Alpha. The API changes between releases, including in ways that will not be called out as breaking, because nothing here is stable yet. Cargo will not resolve a prerelease unless the requirement names one, so a plain 0.1 requirement does not pick this up: depend on the exact version below, and expect to edit it.

use libtmux::test::TestServer;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Runs for real. `TestServer` is an isolated tmux on its own socket under
    // `/tmp/libtmux-rs-test/`, torn down at the end. Your own code says
    // `let server = libtmux::Server::new()?;` instead; nothing else changes.
    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_line("echo hello").await?;

    for line in pane.capture().await? {
        println!("{}", line.to_string_lossy());
    }

    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.11", features = ["test-support"] }.

Every accessor that reaches tmux is async; everything that reads an already-taken snapshot is not. Commands run without a shell, and results keep stdout and stderr as raw bytes, so decoding stays the caller’s decision.

§Requirements and installation

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.

[dependencies]
libtmux = "0.1.0-alpha.11"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

Async operations need an entered Tokio runtime. The default executable is tmux, resolved through the PATH captured when the Server is built; use ServerBuilder::tmux_executable to select another.

Minimum supported Rust version. 1.85, the first compiler to ship Edition 2024. It is checked in CI against the whole test suite, not just a build. A raise is a minor version bump and is called out in the release notes, so a patch release never moves it.

§Features

query is the only one on by default. full turns on every capability below, so a caller who wants them all does not have to list them.

FeatureWhat it adds
queryTyped filter expressions over listings. On by default
planRecording tmux work before running it, and choosing what it costs
control-modeOne persistent tmux connection, so the server reports changes as they happen rather than being polled
blockingA runtime for calling from code that is not async
derive#[derive(Filterable)], for filtering your own structs with the same expressions
serdeVersioned serialization for FilterExpr<T>, for sending expressions over a wire
schemaJSON Schema for serialized plans and portable filter expressions
tracingSanitized command instrumentation
test-supportThe real-tmux test guard, for your own tests
fullEvery capability above, but not test-support

§Where to start, by what you came to do

I want toCallShown in
Make a session and tear it downServer::with_sessionexamples/scratch.rs
Reach a session someone left runningServer::session, Server::sessionsexamples/inspect.rs
Find the pane my process is inPane::from_envexamples/inspect.rs
Add a window, split a paneSession::new_window, Pane::splitexamples/scratch.rs
Type into a panePane::send_line, Pane::send_keys, Pane::send_key_namesexamples/scratch.rs
Read what a pane printedPane::capture, Pane::capture_withexamples/scratch.rs
Wait for output rather than sleepPane::wait_for_text, Pane::wait_for_quietexamples/scratch.rs
Follow a pane as it writesPane::stream_outputexamples/watch.rs
Hear about changes as they happenControlMode::attach, ControlSender::subscribeexamples/watch.rs
Select panes by a typed expressionFilterable::filter_fields, QueryIteratorExt::matchingexamples/find.rs
Send tmux something this crate has no method forServer::cmdbelow
Compare what each transport costsplan::Plan, plan::Plannerexamples/matrix.rs
Clean up fixtures a killed run lefttest::reap_abandoned_serversexamples/sweep.rs
Tell a gone object from a gone linkError::is_object_gone, Error::is_transientexamples/recover.rs

Waiting needs no feature: Pane::wait_for_text and Pane::wait_for_quiet are in the library, poll the scrollback with wrapped lines joined, and answer PaneWait::Dead when the pane’s process ends rather than running to the deadline. docs/design.md records what they had to survive, and what a control-mode doorbell was measured to buy.

§Waiting for something to happen

If the work can announce itself, do not poll. End it with tmux wait-for -S <channel> and wait on that channel:

use libtmux::ChannelWait;
use std::time::Duration;

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

    // Stands in for the pane running `make; tmux wait-for -S build-done`.
    // Signalling first is safe: tmux keeps it for the next waiter.
    server.signal_channel("build-done").await?;

    match server.wait_for_channel("build-done", Duration::from_secs(60)).await? {
        ChannelWait::Signalled => println!("the build ended"),
        // Running out of time is an outcome, not an error, so this stays
        // distinguishable from tmux being unreachable.
        _ => println!("it is still going"),
    }

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

tmux keeps a signal nobody is waiting on, so the job finishing first does not lose the race, and nothing polls. examples/orchestrate.rs runs three jobs this way.

For a pane that was not written to announce itself, Pane::wait_for_text does the same job without a channel:

use libtmux::{PaneWait, test::TestServer};
use std::time::Duration;

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

    pane.send_line("printf 'ready\n'").await?;

    // Checked rather than discarded: a wait that reached its deadline still
    // returns successfully, so the outcome is the answer.
    assert_eq!(
        pane.wait_for_text("ready", Duration::from_secs(10)).await?,
        PaneWait::Arrived,
    );

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

What it handles, and what a loop written by hand usually does not: it looks before it sleeps, so text already present is an answer rather than a wait; it reads the scrollback with wrapped lines joined, so output that scrolled away is still found and a needle spanning a wrap still matches; and it answers PaneWait::Dead when the pane’s process ends rather than holding the deadline.

Three things that are not obvious:

  • Every command is already bounded. A dispatch carries the server’s default_timeout, 30 seconds unless you change it, so a hung tmux ends the call rather than the loop. The deadline above is for the condition, not the transport.
  • Polling costs a tmux process per tick, and a tick is 120ms. One capture-pane each, so a wait stays under ten dispatches a second and answers within an interval of the text appearing. With control-mode you can subscribe to a format instead and be told when it changes – see ControlSender::subscribe. tmux coalesces those reports to at most once a second, so a subscription says what a value became, not every step it took.

The subscription form, which costs one connection rather than a process per tick. It watches a format rather than the pane’s text, so it suits “how many windows are there now” better than “did this line appear”:

use libtmux::control::{ControlMode, Event, Subscription};
use libtmux::test::TestServer;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let guard = TestServer::new().await?;
    let session = guard.server().new_session("watching").await?;
    let (commands, mut events) = ControlMode::attach(guard.server(), session.id())
        .await?
        .split();

    commands
        .subscribe("windows", &Subscription::Session, "#{session_windows}")
        .await?;

    // The first report arrives without anything having changed, so a
    // subscription reads the value as well as watching it.
    let mut reports = 0;
    while let Some(event) = events.next_event().await {
        if let Event::SubscriptionChanged { name, value, .. } = event {
            println!("{} = {}", name.to_string_lossy(), value.to_string_lossy());
            reports += 1;
            if reports == 1 {
                break;
            }
        }
    }

    commands.unsubscribe("windows").await?;
    events.shutdown().await?;
    guard.shutdown().await?;
    Ok(())
}
  • command_prompt and display_menu wait for a person, not for a condition, and the dispatch timeout is what ends that wait. They are not building blocks for this.

§Choosing how commands reach tmux

Three switches decide what a run costs and what it can prove. They compose: async is the API, control mode is the transport, and chaining is what a subprocess transport does instead of having one.

§What each one does

SwitchOff (the default)On
AsyncNothing: every method is already async. blocking::Runtime is a runtime you drive them from, not a second API to keep in stepblocking::Runtime::new()?.run(future) for scripts and tests
Control modeOne tmux process per commandOne connection for every command, and tmux reports changes as they happen
ChainingOne tmux process per commandNeighbouring commands share one process, trading the ability to say which one failed

Measured on one workload of six operations, all leaving identical tmux state:

ModeProcessesAttribution on failure
One command per invocation6names the failing command
Folded into shared invocations3Unknown – tmux reports one status for the group
Control mode1names the failing command

Control mode is the only one that buys back the processes without giving up the answer, because its %begin/%end blocks are per command. Folding is what a subprocess transport offers instead of having that. Run cargo run --example matrix --features full,test-support to reproduce the table on your own machine.

§How to turn each one on

SwitchCargo featureIn code
Asyncnonealready the default; every method is async
Blocking runtimeblockinglibtmux::blocking::Runtime::new()?
Control modecontrol-modeControlMode::attach(&server, session).await?
Chaining, by handnoneCommandChain::new(a).then(b), then server.chain(chain).await?
Chaining, by plannerplanplan.run(&server, Planner::Folding).await?
Folding a pane creation in tooplanPlanner::Marked
Never folding across your own workplanPlanner::steps_bounded(&plan, &boundaries)
Per-command answers over one connectionplan, control-modeplan.run_over_control_mode(&sender).await?

Control mode is never the default transport, and turning the feature on does not make it one: normal commands stay one process per command until you attach a connection and use it.

§When the typed API does not cover it

tmux has more commands than this crate has methods, and a method can be narrower than the command behind it. Server::cmd runs anything, through the same transport, timeout and error classification as the typed calls – so reaching for it costs you the method, not the machinery.

use libtmux::{Command, test::TestServer};

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

    // `Pane::clock_mode` has no typed way back out for every mode, so this is
    // how you leave one the API does not model.
    server
        .cmd(
            Command::new("copy-mode")
                .arg("-q")
                .arg("-t")
                .arg(pane.id().to_string()),
        )
        .await?;

    // The answer carries tmux's own bytes. A command tmux refuses is an `Err`
    // classified the same way a typed call's would be.
    let version = server
        .cmd(Command::new("display-message").arg("-p").arg("#{version}"))
        .await?;
    assert!(!version.stdout().is_empty());

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

A command you needed here is worth reporting: tmux-mcp and tmux-workspace use this crate from outside it, and when either reaches for cmd that counts as a gap in the API rather than a use of the escape hatch.

§What tmux is made of

Server ──┬── Session ──── Window ──── Pane
         └── Client ─────┘
TypeHeld byIdentified byNotes
Servera socketits socket pathone daemon; Server::new() finds the default
Sessionthe server$0what a client attaches to
Windowlinked into sessions@0one window can be linked into several sessions at once
Paneexactly one window%0holds the process and the scrollback
Clientthe serverits ttyattached to one session at a time

The third row is the one that surprises people, and it decides API shape. A window has one identity and several links, so a command that removes one link has to name the session too – session:@id, because @id alone could not say which link to remove, and a bare index names a slot rather than whichever window is sitting in it. Window::unlink does exactly that, and reports Error::LinkGone rather than Error::ObjectGone when the link is already gone, because the window itself may still be running under another session.

§Walking the hierarchy

Server::hierarchy gathers the whole tree in three tmux commands, rather than one per object:

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?;

    for branch in server.hierarchy().await? {
        println!("{}", branch.session.name().to_string_lossy());

        for window in &branch.windows {
            println!("  {}", window.window.name().to_string_lossy());

            for pane in &window.panes {
                println!("    {pane}");
            }
        }
    }

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

Listings come in pairs, and the short name is the honest one. sessions() returns Result<Vec<Session>>, so an unreachable tmux is an error rather than an empty list. sessions_or_empty() collapses failure into no rows, which suits a status line and nothing that reconciles state – a reconciler reading “no sessions” from an outage will happily delete everything.

§Filtering

Typed field handles build an expression without accepting an untyped field name or value, so a comparison that has no meaning for a field does not compile:

use std::time::Duration;

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

#[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 fields = libtmux::Pane::filter_fields();

    let active = fields
        .pane_current_command
        .starts_with("sh")
        .and(fields.pane_active.eq(true));

    // tmux hands back a pane the moment it forks, before the shell in it has
    // started, so what a pane is running is worth waiting for rather than
    // assuming. A wait must assert the outcome it got: this one fails if the
    // deadline passes without the expression ever matching.
    retry_until(Duration::from_secs(5), async || {
        server
            .panes()
            .await
            .is_ok_and(|panes| panes.iter().matching(&active).count() == 1)
    })
    .await?;

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

A question about what a session contains needs the shape that holds its windows, so SessionTree and WindowTree carry relations:

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 ci = server.new_session("ci").await?;
    ci.new_window("build").await?;
    server.new_session("idle").await?;

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

    let names: Vec<_> = server
        .hierarchy()
        .await?
        .iter()
        .matching(&building)
        .map(|branch| branch.session.name().to_string_lossy().into_owned())
        .collect();
    assert_eq!(names, ["ci"]);

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

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

§Text from tmux is bytes

tmux permits names, titles, and pane contents that are not valid UTF-8, so they arrive as TmuxText rather than String. There is no implicit conversion:

let text = libtmux::TmuxText::from("editor");

assert_eq!(text.as_bytes(), b"editor");
assert_eq!(text.as_str().expect("valid UTF-8"), "editor");
assert_eq!(text.to_string_lossy(), "editor");

§Failures say what to do about them

Error has a variant per failure mode; Error::kind reduces those to the decision a caller makes. Error::is_object_gone is the branch most programs write, because an object disappearing is an ordinary race rather than a failed request.

Error::is_transient answers the narrower replay question: true means the same call can be repeated without duplicating an effect and may work after the condition clears. A timeout is false even when the server handle remains usable, because tmux may have completed the command before its caller stopped waiting.

A composed call can also fail after tmux accepted one of its effects. That is Error::AfterEffect, classified as ErrorKind::PartialEffect: its source still explains the later failure, but is_transient is false for the whole call because replay may repeat the accepted effect.

The block below shows the shape against a healthy server, so its interesting arms do not run. examples/recover.rs makes each failure happen instead – including the one that costs people, where a link is gone and the window it pointed at is still running.

use libtmux::test::TestServer;

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

    match session.windows().await {
        Ok(windows) => println!("{} windows", windows.len()),
        Err(error) if error.is_object_gone() => println!("gone"),
        Err(error) if error.is_transient() => println!("retry: {error}"),
        Err(error) => return Err(error.into()),
    }

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

§Cancellation and shutdown

Each subprocess command runs in an isolated process group with a supervised deadline, 30 seconds by default and configurable through ServerBuilder::default_timeout. The deadline starts before the command waits for dispatch capacity. Dropping the command future, reaching its timeout, or shutting the server down signals the group and waits for the direct child while the runtime is alive.

A control-mode connection has its own isolated process group. Its attach handshake and each command response use the same default timeout; a response’s deadline starts before its line is written and ends with the complete block. Dropping both handles terminates and reaps the connection.

Server::shutdown() is shared by all clones: it cancels active subprocess work and persistent control connections, rejects later commands and attaches, and is safe to call concurrently or repeatedly. Await it, or await your commands, before tearing the runtime down — runtime destruction signals best-effort but cannot promise that child reaping finished.

§Testing against real tmux

test-support exports libtmux::test::TestServer, the same guard this crate’s own suite uses:

[dev-dependencies]
libtmux = { version = "0.1.0-alpha.11", features = ["test-support"] }

Each guard owns a tmux child on a private socket with an empty config, so tests cannot reach your real server or each other. shutdown().await closes escaped clients, waits the daemon, and reports cleanup failures; Drop forces best-effort cleanup even after the runtime has ended. On Linux, cleanup also sweeps processes by an exact environment marker through pidfds, so PID reuse cannot redirect a signal.

The guarantees and their limits are set out in docs/design.md, which ships with the crate.

§Compatibility

Every supported tmux release is built from source in CI and runs the whole workspace: 3.2a, 3.4, 3.5a, 3.6b, and 3.7b. The floor and the ceiling matter equally — 3.4 and 3.5a are the releases that wrap command output differently, which is why the format codec carries a second dialect.

§If you know the Python libtmux

This is a port of it, and the object model is the same one: a server holding sessions, windows linked into them, panes inside those. Code you have written against the Python library will read as familiar here. Four things differ, and they are the ones worth knowing before you start.

Python libtmuxthis crate
Callingsynchronous throughoutasync throughout; blocking::Runtime drives it from code that is not
Pane textcapture_pane returns list[str]capture returns TmuxText, which keeps bytes no String would hold
FilteringQueryList lives in _internalquery is public, and field handles are generated per type
Transportone tmux process per commandthe same by default, plus control mode and command chaining

One thing differs, and in this crate’s favour: waiting. The Python library keeps retry_until in libtmux/test/retry.py, a test helper, so production code that waits on a pane writes the loop itself. Pane::wait_for_text and Pane::wait_for_quiet are here in the library and need no feature.

§Documentation

API documentation covers the public surface. Two longer documents ship inside the crate, next to the source:

  • docs/design.md — why the crate is shaped the way it is: the transport, the snapshot and format boundary, the query grammar, the test guard, and the compatibility lanes.
  • docs/parity.md — the capability ledger against Python libtmux, naming each Rust symbol and the test that exercises it.

§License

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

§Query iterators

Listings hand back an ordered Vec<T> that the caller owns. Borrow it with .iter(), use Iterator::filter for an inline closure, and query::QueryIteratorExt::matching for a portable expression or a named query::Matcher. Exact cardinality inspects at most two items.

If another iterator extension trait, such as itertools::Itertools, adds the same method name, use universal function call syntax to select this crate’s method:

use libtmux::query::QueryIteratorExt;

let values = vec![1];
let item = QueryIteratorExt::exactly_one(values.iter());
assert_eq!(item, Ok(&1));

§Finding what is already there

Naming an object is cheaper than listing and scanning for it:

let server = libtmux::Server::new()?;

// Find one object rather than listing and scanning.
if let Some(session) = server.session("work").await? {
    if let Some(window) = session.window("editor").await? {
        if let Some(pane) = window.active_pane().await? {
            pane.send_line("cargo test").await?;
        }
    }
}

Listings come in pairs. The _or_empty form returns an empty Vec when the underlying tmux command fails, which suits a status line; the plain form keeps the reason, which suits anything that must not guess:

let quiet = server.sessions_or_empty().await; // empty on failure
let loud = server.sessions().await?;          // Err on failure

§Building something and cleaning up

Once polled, a scoped operation owns creation and cleanup. Cancellation can let an in-flight creation finish, but an object whose creation yields a handle is killed while the Tokio runtime remains active. Ordinary handle Drop is deliberately non-destructive.

let id = server
    .with_session("throwaway", async |session| {
        session.new_window("build").await?;
        Ok::<_, libtmux::Error>(session.id().to_string())
    })
    .await?;

Setup and teardown failures convert into the operation’s own error type, so there is one ? rather than two. Once creation succeeds, a cleanup failure is returned as an after-effect; it owns the replay guidance even when the operation also failed.

§Options carry types

tmux reports no type over the command line, so the crate generates the schema from tmux’s own table. That matters more than it sounds: status holds "on" but is a choice, because tmux also accepts 2 through 5.

use libtmux::{OptionValue, option_names};

// Names are constants, so a typo does not compile.
let mouse = server.typed_global_option(option_names::MOUSE).await?;
assert!(matches!(mouse, Some(OptionValue::Flag(_))));

§A name reaches tmux as a format

tmux expands a name through its format machinery before it checks it, so #{session_id} in a name becomes the id and #(command) runs command in a shell and becomes its output. That holds for new_session, Session::rename, Window::rename, and every other name tmux takes from a command. tmux is consistent here: whoever can run tmux new-session can already run commands, so a name given on a command line is trusted by construction.

A library moves that boundary. tmux’s caller is a person at a shell; this crate’s caller is a program, and the name it passes may have come from an argument, a request field, or a configuration file. Passing untrusted text as a name gives whoever wrote it a shell, so escape # as ## before it reaches tmux, or refuse the name.

Expansion is not the only way the name you asked for is not the name you get. tmux releases through 3.6b rewrite : and . in a session name to _, because a target is split on those, and they do it silently: 3.7 refuses such a name outright, and 3.7a keeps it. So new_session("a:b") succeeds on every supported release except 3.7 and hands back a session called a_b on most of them. The handle reports what tmux stored, so Session::name is always the truth; the request is what may differ from it. Compare the two when the name has to round-trip.

§Examples

Runnable programs live in examples/. inspect reports what a server is running and find selects panes with a typed expression, both of which only read. scratch builds a throwaway session on its own socket and cleans it up, which is the shortest complete tour: a window, a split, keys sent, output waited for rather than slept on, and a scope that kills the session whether the body succeeded or not. watch reacts to what a server does over one control-mode connection while driving it down the same one. matrix runs one workload five ways, so the cost of each execution mode is visible side by side. sweep reaps servers that abandoned fixtures left behind, which is maintenance rather than orchestration.

just examples runs every one of them against a server it owns and fails if any leaves a socket behind.

§Filtering the hierarchy

Session, Window, Pane, and Client carry generated field handles, so an expression names the same type a listing returns:

use libtmux::query::Filterable as _;

let fields = libtmux::Session::filter_fields();
let expression = fields.session_name.starts_with("build");
let sessions: Vec<libtmux::Session> = Vec::new();
assert_eq!(sessions.iter().count(), 0);

Field types decide which operations exist, so a mismatched comparison is a compile error rather than a predicate that is always false:

use libtmux::query::Filterable as _;

let fields = libtmux::Session::filter_fields();
// `session_name` is text, so it has no integer comparison.
let _ = fields.session_name.eq(3_u32);
use libtmux::query::Filterable as _;

let fields = libtmux::Session::filter_fields();
// `session_windows` is an integer, so it has no substring operation.
let _ = fields.session_windows.contains("3");

A question about what a session contains needs a value that holds its windows. Server::hierarchy returns one, and SessionTree and WindowTree carry relations for it:

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

let sessions = SessionTree::filter_fields();
let windows = WindowTree::filter_fields();

// The session's own fields sit beside the relation, not behind it.
let building = sessions
    .session
    .session_name
    .starts_with("build")
    .and(sessions.windows.any(windows.window.window_name.eq("editor")));

for branch in server.hierarchy().await?.iter().matching(&building) {
    println!("{}", branch.session);
}

Query extensions intentionally apply only to borrowed iterators:

use libtmux::query::QueryIteratorExt;

let values = vec![1, 2, 3];
let _ = values.into_iter().matching(|candidate: &i32| *candidate > 1);

§Being told instead of asking

Everything above runs a tmux command and reads the answer. The control-mode feature opens one connection and keeps it, so tmux reports what happens as it happens – no polling interval, and nothing missed between two polls.

Pane::stream_output is the narrow version: what one pane writes, as it writes it, where Pane::capture gives only what is on screen now.

let mut output = pane.stream_output().await?;

while let Some(chunk) = output.next_chunk().await {
    println!("{} bytes", chunk.len());
}

output.shutdown().await

control::ControlMode is the whole connection: every notification the server sends, plus commands that travel down the connection rather than spawning a process. Sending and watching are separate handles, so a task can act on what it sees. See the control module.

§Calling from code that is not async

The blocking feature adds a blocking::Runtime that drives this crate’s futures to completion. It is deliberately a runtime rather than a mirrored blocking API: one type to learn, and no second surface to keep in step.

let runtime = libtmux::blocking::Runtime::new()?;
let server = libtmux::Server::new()?;

let sessions = runtime.run(server.sessions())?;
println!("{} sessions", sessions.len());

Re-exports§

pub use hooks::IndexedHooks;
pub use hooks::ReplaceMode;
pub use hooks::SparseValues;

Modules§

blockingblocking
Running the async API without writing an async program.
controlcontrol-mode
Watching a tmux server over control mode.
hooks
Reading the hooks tmux is holding.
option_names
Option names, so a typo is a compile error rather than a runtime one.
planplan
Describe tmux work before doing any of it.
queryquery
Predicates and cardinality helpers for borrowed iterators.
since
The tmux release each version-gated capability arrived in.
testtest-support
Isolated real-tmux support for downstream tests.

Structs§

AccessRule
One entry of the server’s access list.
CaptureOptions
How far back a capture reaches, and in what form.
CapturedLine
One line of a capture, with what tmux knows about it.
Client
One client attached to the tmux server.
ClientFieldsquery
Typed filter field handles for a client.
Command
A logical tmux command with classified diagnostic arguments.
CommandChain
Several tmux commands dispatched as one tmux a \; b invocation.
CommandResult
The exact status and output captured for one tmux command.
CommandSummary
A rendering of a dispatched command that is safe to log.
ControlClientLimitscontrol-mode
How many persistent control-mode clients one server may hold.
ControlLimitscontrol-mode
What one control-mode connection may accumulate before it gives up.
DispatchLimits
How much work one server may have in flight at once.
EngineCapabilities
Immutable capability state detected for one configured tmux executable.
IdParseError
An invalid scope-specific tmux object ID.
JoinOptions
Where a pane lands when it is moved into another window.
ListingDecodeError
Payload-free metadata describing why tmux output could not be decoded.
NewSessionOptions
Options for creating a session.
NewWindowOptions
Options for creating a window in a session.
OptionSchema
What tmux declares about one option.
OutputLimits
How many bytes one dispatch may read from each stream.
Pane
One tmux pane, as reached through one window link.
PaneFieldsquery
Typed filter field handles for a pane.
PaneId
A native tmux pane ID such as %1.
ReleaseSuffix
The suffix of a numbered tmux release.
ReleaseVersion
A numbered tmux release.
Server
A cloneable handle to one captured tmux server endpoint.
ServerBuilder
A consuming builder for one inert Server handle.
ServerGeneration
Which tmux daemon is answering on an endpoint.
ServerIdentity
The structural identity of one tmux server endpoint.
Session
One tmux session, together with the snapshot it was discovered with.
SessionFieldsquery
Typed filter field handles for a session.
SessionId
A native tmux session ID such as $1.
SessionName
A session name tmux can address.
SessionTree
One session and everything under it, from Server::hierarchy.
SessionTreeFieldsquery
Typed filter handles for SessionTree.
SplitOptions
Options for splitting a window or pane into a new pane.
TmuxText
Immutable text bytes returned by tmux.
TmuxVersion
A parsed raw tmux version.
Window
One tmux window, as reached through one session that links it.
WindowFieldsquery
Typed filter field handles for a window.
WindowId
A native tmux window ID such as @1.
WindowTree
One window and its panes, from Server::hierarchy.
WindowTreeFieldsquery
Typed filter handles for WindowTree.

Enums§

AccessMode
How much a user on the server’s access list may do.
ChannelWait
What came of waiting on a wait-for channel.
Chooser
One of tmux’s interactive choosers.
ControlModeErrorKindcontrol-mode
Why a control-mode connection failed.
EnvironmentEntry
What a session’s environment holds for one name.
Error
An error returned by libtmux.
ErrorKind
A coarse failure category for reporting and routing.
Layout
One of the pane arrangements tmux knows by name.
LayoutSpec
What to arrange a window’s panes as.
ObjectKind
The kind of tmux object a failure refers to.
OptionErrorKind
Which way tmux would not accept an option.
OptionKind
What kind of value an option holds.
OptionScope
Which table an option primarily lives in.
OptionValue
One option’s value, decoded according to what tmux declares about it.
PaneDirection
Which way to move focus among a window’s panes.
PaneProgressState
Closed progress-state vocabulary tmux emits for a pane’s progress bar.
PaneSize
How much space a new pane gets.
PaneTarget
A target accepted by pane-scoped operations.
PaneWait
What came of waiting for a pane to do something.
PromptKind
Which prompt tmux is remembering answers for.
ResizeDirection
Which edge a resize moves.
Rotation
Where a split puts the new pane, relative to the one being divided.
ServerConfigurationErrorKind
The category of an invalid crate::ServerBuilder configuration.
ServerGoneKind
Which way a tmux server was not there.
SessionNameError
Why a session name is one tmux could not address.
SessionTarget
A target accepted by session-scoped operations.
SplitDirection
Where a split puts the pane it makes.
WindowPlacement
Where a new window goes, relative to the index it is given.
WindowTarget
A target accepted by window-scoped operations.

Functions§

escape_format
Escape text so tmux uses it as written rather than expanding it.
option_schema
Look up what tmux declares about one option.

Derive Macros§

Filterablederive
Derive a stable typed filter schema for a named struct.