# Attach and send keys

Source: https://libtmux.org/en/rs/latest/examples/attach-and-send-keys/

> Get a session handle, send a command to a pane, and capture output.

Get a session handle, send a command to a pane, and capture output. These
examples use libtmux from your program; to attach your terminal interactively,
see [Attaching to tmux](https://libtmux.org/en/rs/latest/guides/attaching-to-tmux/).

The examples include setup, error handling, and cleanup. [Source and
verification](https://libtmux.org/en/rs/latest/examples/attach-and-send-keys/#where-this-comes-from) identifies their files and checks.

```rust file="crates/libtmux/examples/scratch.rs"
//! Build a throwaway session, use it, and leave nothing behind.
//!
//! ```console
//! $ cargo run --example scratch
//! ```

use std::time::Duration;

use libtmux::test::unique_name;
use libtmux::{NewWindowOptions, PaneWait, Server, SplitDirection, SplitOptions};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // An example must not build sessions on whatever server the reader
    // happens to be using, so this one gets a socket of its own. More than one
    // libtmux runs on a developer's machine, so it goes in a directory this
    // one owns rather than straight into the temporary directory.
    let root = std::path::Path::new("/tmp/libtmux-rs-dev");
    std::fs::create_dir_all(root)?;
    let socket = root.join(format!("{}.sock", unique_name("libtmux-scratch")));
    let server = Server::builder().socket_path(&socket).build()?;

    // The scope kills the session whether the body succeeds or fails, so a
    // failure partway through does not leave a session behind.
    println!("server on {}", socket.display());

    let output = server
        .with_session(unique_name("scratch").as_str(), async |session| {
            println!("  session {} created", session.id());

            let window = session
                .new_window(NewWindowOptions::new("work").command("sh"))
                .await?;
            println!("  window {} running sh", window.id());

            window
                .split(SplitOptions::new(SplitDirection::Below).command("sh"))
                .await?;
            println!("  split it: {} panes", window.panes().await?.len());

            // A window always has an active pane, but saying so with a panic
            // would be a worse example than handling it.
            let Some(pane) = window.active_pane().await? else {
                return Ok(0);
            };
            println!("  typing into {}", pane.id());
            pane.send_line("printf 'hello from tmux\\n'").await?;

            // tmux runs the shell asynchronously, so wait for the output
            // rather than sleeping and hoping. The outcome is checked because
            // a wait that reached its deadline still returns successfully.
            match pane.wait_for_text("hello", Duration::from_secs(5)).await? {
                PaneWait::Arrived => println!("  the pane printed it"),
                other => println!("  gave up: {other:?}"),
            }

            // region: capture
            let lines = pane.capture().await?;
            for line in lines.iter().filter(|line| !line.as_bytes().is_empty()) {
                println!("  | {}", line.to_string_lossy());
            }
            // endregion

            Ok::<_, Box<dyn std::error::Error>>(lines.len())
        })
        .await?;

    println!("captured {output} lines, then the scope killed the session");

    // The lenient form is the one that answers this question. The scope killed
    // the only session, so tmux exited with it, and the loud form reports that
    // as the failure it is rather than as the empty listing this is asking for.
    assert!(
        server.sessions().await.unwrap_or_default().is_empty(),
        "the scope cleaned up",
    );

    println!(
        "sessions left behind: {}",
        server.sessions().await.unwrap_or_default().len()
    );

    server.shutdown().await?;

    // tmux does not unlink its socket when the server exits, so whatever named
    // one owns removing it. Leaving it behind is invisible until /tmp fills up.
    std::fs::remove_file(&socket)?;
    Ok(())
}
```

## Finding an existing session instead

For a script that runs repeatedly, look up a session before creating it.
[Attaching to
tmux](https://libtmux.org/en/rs/latest/guides/attaching-to-tmux/#finding-a-session-instead-of-always-creating-one)
shows that pattern, and [Filtering and querying, in
practice](https://libtmux.org/en/rs/latest/guides/querying-and-filtering/) covers absent and ambiguous matches.

## Where this comes from

**Source:** [`crates/libtmux/examples/scratch.rs`](<https://github.com/libtmux/libtmux-rs/blob/84de9258a07d10bdd5f6da01b016a9ae06fe1210/crates/libtmux/examples/scratch.rs>)

**In this page:** read whole from the file

**Checked by:** run to completion against a throwaway tmux by
[`scripts/run-examples.sh`](<https://github.com/libtmux/libtmux-rs/blob/84de9258a07d10bdd5f6da01b016a9ae06fe1210/scripts/run-examples.sh>) (`just examples`), which CI runs and which also
asserts the example leaves no session behind

### Source inclusion

A `file="..."` fence reads the named source during the site build. Hand-quoted
excerpts are copies; their source files and regions are listed above.
