# Attach and send keys

Source: https://libtmux.org/en/tmux/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/tmux/guides/attaching-to-tmux/).

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

```python
>>> import libtmux
>>> server = libtmux.Server()
>>> session = server.new_session(session_name='demo')
Session(...)

>>> window = session.active_window
>>> pane = window.split(shell='sh')
>>> pane.capture_pane()
['$']

>>> pane.send_keys('echo "Hello world"', enter=True)

>>> pane.capture_pane()
['$ echo "Hello world"', 'Hello world', '$']
```

```typescript file="examples/quickstart/quickstart.ts"
import { Server, TmuxCommandError, type ServerSnapshot } from "libtmux";

/**
 * A runnable tour of the API, driven by the tests so it cannot rot.
 *
 * Every step here appears in README.md.
 */
export async function quickstart(server: Server): Promise<ServerSnapshot> {
  // Nothing is read until you ask. `snapshot()` is the only step that talks to
  // tmux; everything reachable from it resolves locally.
  const session = await server.newSession({ name: "quickstart" });
  const editor = await session.newWindow({ name: "editor" });
  await editor.split();

  const snapshot = await server.snapshot();

  // Declarative filtering, serializable and stable on the wire.
  const found = snapshot.windows.where({ name: "editor" }).one();

  // Relations are plain properties: no await, no tmux command.
  const paneCount = found.panes.length;
  if (paneCount !== 2) throw new Error(`expected two panes, saw ${String(paneCount)}`);

  // A criterion is spelled like the handle accessor it filters.
  if (snapshot.panes.count({ currentCommand: { contains: "" } }) === 0) {
    throw new Error("expected panes to report a current command");
  }
  const first = found.panes.at(0);
  if (first === undefined) throw new Error("expected a pane");
  await first.sendKeys("echo hello-from-libtmux", { literal: true });

  // Failures carry their parts rather than a formatted sentence.
  try {
    await server.setOption("not-a-real-option", "1");
  } catch (error) {
    if (!(error instanceof TmuxCommandError)) throw error;
    if (error.args[0] !== "set-option") throw error;
  }

  return snapshot;
}
```

```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(())
}
```

```go file="examples/quickstart/main.go"
// Command quickstart demonstrates a complete session, window, and pane lifecycle.
package main

import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/libtmux/libtmux-go/tmux"
)

func main() {
	if err := start(); err != nil {
		log.Fatal(err)
	}
}

// start owns cleanup because log.Fatal skips deferred calls in main.
func start() error {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	server, err := tmux.NewServer(tmux.ServerOptions{})
	if err != nil {
		return fmt.Errorf("configure tmux server: %w", err)
	}
	return run(ctx, server)
}

// run accepts injected server state so tests can isolate the example.
func run(ctx context.Context, server tmux.Server) (err error) {
	// docs:quickstart given:ctx context.Context; server tmux.Server
	session, err := server.NewSession(ctx, tmux.NewSessionRequest{
		Name: "libtmux-go-quickstart", WindowName: "start",
	})
	if err != nil {
		return fmt.Errorf("create session: %w", err)
	}
	defer func() {
		cleanupCtx, cleanupCancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second)
		defer cleanupCancel()
		err = errors.Join(err, session.Kill(cleanupCtx))
	}()

	window, err := session.NewWindow(ctx, tmux.NewWindowRequest{Name: new("work")})
	if err != nil {
		return fmt.Errorf("create window: %w", err)
	}
	pane, err := window.SplitPane(ctx, tmux.SplitPaneRequest{
		Direction: tmux.PaneDirectionRight, Command: "sh",
	})
	if err != nil {
		return fmt.Errorf("split window: %w", err)
	}
	output, err := pane.OpenObservation(ctx)
	if err != nil {
		return fmt.Errorf("watch pane: %w", err)
	}
	defer func() { err = errors.Join(err, output.Close()) }()
	if _, err := fmt.Fprintln(pane.Writer(ctx), "printf 'libtmux ready\\n'"); err != nil {
		return fmt.Errorf("send command: %w", err)
	}
	// docs:end

	scanner := bufio.NewScanner(output.Reader(ctx))
	for scanner.Scan() {
		if scanner.Text() == "libtmux ready" {
			fmt.Println("libtmux ready")
			return nil
		}
	}
	return fmt.Errorf("read pane: %w", scanner.Err())
}
```

```java file="examples/src/main/java/io/github/libtmux/examples/BuildAWorkspace.java"
package io.github.libtmux.examples;

import io.github.libtmux.Layout;
import io.github.libtmux.Pane;
import io.github.libtmux.Server;
import io.github.libtmux.ServerConfig;
import io.github.libtmux.ServerEndpoint;
import io.github.libtmux.Session;
import io.github.libtmux.Window;
import io.github.libtmux.exception.ServerUnavailableException;
import java.nio.file.Path;
import java.util.Optional;

/**
 * Lays out a session the way you would set one up by hand before starting work.
 *
 * <pre>{@code
 * java BuildAWorkspace.java /tmp/libtmux-java-dev/demo/s
 * }</pre>
 */
public final class BuildAWorkspace {

    private BuildAWorkspace() {}

    public static void main(String[] args) {
        System.out.println(run(Path.of(args.length > 0 ? args[0] : "/tmp/libtmux-java-dev/demo/s")));
    }

    /** Separated from {@code main} so the suite can run exactly what a reader runs. */
    public static String run(Path socket) {
        ServerConfig config = ServerConfig.builder()
                .endpoint(ServerEndpoint.socketPath(socket))
                .configFile(Path.of("/dev/null"))
                .build();

        // Closing a server closes this client. The tmux server, and the session, outlive the program
        // — which is the whole point of tmux and the reason nothing here kills it.
        try (Server server = Server.open(config)) {
            // One read decides and answers. An absent daemon means the name cannot be taken, so
            // that specific failure is the other way this resolves to "not found".
            Optional<Session> existing;
            try {
                existing = server.session("work");
            } catch (ServerUnavailableException absent) {
                existing = Optional.empty();
            }
            Session session = existing.orElseGet(() -> server.newSession("work"));

            Window editor = session.newWindow(window -> window.named("editor").detached());
            Pane shell = editor.split(split -> split.toRight());
            shell.sendLine("git status --short");

            editor.selectLayout(Layout.MAIN_VERTICAL);

            return "session " + session.name() + " has "
                    + session.refresh().windows().size() + " windows";
        }
    }
}
```

```csharp file="examples/LibTmux.Examples/Snippets/OneShot.cs"
using System.Runtime.Versioning;

namespace LibTmux.Examples.Snippets;

/// <summary>The default mode: one command, one client, one materialized object.</summary>
[UnsupportedOSPlatform("windows")]
public static class OneShot
{
    /// <summary>Connects, builds a hierarchy, and types into the pane it made.</summary>
    [Example("Connect, build a session and window, and type into a pane")]
    public static async Task ConnectAndBuild()
    {
        #region ConnectAndBuild
        // Requires a tmux server already listening on this socket:
        // ConnectAsync() discovers one, it never starts one. With nothing
        // running yet, call Server.CreateOwnedAsync() instead.
        Server server = await Server.ConnectAsync();
        Session session = await server.CreateSessionAsync(new NewSessionRequest { Name = "build" });
        Window window = await session.CreateWindowAsync(new NewWindowRequest { Name = "tests" });
        Pane pane = (await window.GetPanesAsync())[0];

        await pane.SendTextAsync("dotnet test");
        #endregion
    }

    /// <summary>Creates a window and prints what tmux answered about it.</summary>
    [Example("One command, one materialized window")]
    public static async Task CreateWindow(Session session, CancellationToken ct)
    {
        #region CreateWindow
        Window window = await session.CreateWindowAsync(new NewWindowRequest { Name = "build" }, ct);
        Console.WriteLine($"{window.Id} {window.Index}:{window.Name}");
        #endregion
    }
}
```

```cpp
// No tmux failure is thrown. Every call answers with a value that is either
// the result or the reason there isn't one.
const auto sessions = server.sessions();
if (!sessions.has_value()) {
  std::fprintf(stderr, "%s\n", sessions.error().diagnostic.c_str());
  return 1;
}

for (const libtmux::Session& session : *sessions) {
  std::printf("%s has %lld window(s)\n", std::string{session.name()}.c_str(),
              session.window_count());
}

const libtmux::Session& session = sessions->at(0);

// Build an arrangement without composing a single tmux argument.
const auto editor = session.new_window({.name = "editor"});
if (!editor.has_value()) {
  std::fprintf(stderr, "%s\n", editor.error().diagnostic.c_str());
  return 1;
}

const auto logs = editor->split({.horizontal = true, .percentage = 30});
if (!logs.has_value()) {
  std::fprintf(stderr, "%s\n", logs.error().diagnostic.c_str());
  return 1;
}

(void)logs->send_text("journalctl -f");
(void)logs->send_key("Enter");
```

```swift file="Examples/Sources/ExampleCode/Changing.swift"
// The examples in the README's "Change what is there" section.

import LibTmux

public func buildASessionByHand(_ server: Server) async throws -> Pane {
    let session = try await server.newSession(named: "work", windowName: "editor")
    _ = try await server.setOption("@purpose", to: "development", scope: .session(session))
    let logs = try await server.newWindow(in: session, named: "logs").window
    let pane = try await server.splitWindow(logs, direction: .right)
    try await server.run("tail -f /tmp/build.log", in: pane)
    return pane
}

public func readBackWhatAPanePrinted(_ server: Server, _ pane: Pane) async throws -> [String] {
    let lines = try await server.capture(pane)
    print(lines.suffix(5).joined(separator: "\n"))
    return lines
}

public func spendOneProcessOnAllOfIt(_ server: Server) async throws {
    var plan = TmuxCommandList()
    for name in ["edit", "test", "logs"] {
        plan = plan.then("new-window", ["-d", "-n", name])
    }
    _ = try await server.run(plan)
}
```

## 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/tmux/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/tmux/guides/querying-and-filtering/) covers absent and ambiguous matches.

## Where this comes from

### Python

**Source:** [`src/libtmux/server.py`](<https://github.com/tmux-python/libtmux/blob/9fdd083a8181a827889337b63cd4e6daea661a8c/src/libtmux/server.py>), [`session.py`](<https://github.com/tmux-python/libtmux/blob/9fdd083a8181a827889337b63cd4e6daea661a8c/src/libtmux/session.py>), [`pane.py`](<https://github.com/tmux-python/libtmux/blob/9fdd083a8181a827889337b63cd4e6daea661a8c/src/libtmux/pane.py>) docstrings

**In this page:** hand-quoted, composed from three separate docstrings

**Checked by:** `pytest` runs every `>>>` doctest (`testpaths` includes
`src/libtmux`) against a real, isolated tmux session on every test run

### TypeScript

**Source:** [`examples/quickstart/quickstart.ts`](<https://github.com/libtmux/libtmux-ts/blob/46cccfef2a546de55ce8b308249b8c76339089a0/examples/quickstart/quickstart.ts>)

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

**Checked by:** run against real tmux by `bun test examples`; its first half is
also mirrored into README.md under a `<!-- runs: ... -->` marker, checked
line-for-line by [`scripts/check-doc-runnable.ts`](<https://github.com/libtmux/libtmux-ts/blob/46cccfef2a546de55ce8b308249b8c76339089a0/scripts/check-doc-runnable.ts>)

### Rust

**Source:** [`crates/libtmux/examples/scratch.rs`](<https://github.com/libtmux/libtmux-rs/blob/a6fc2a65674177b92b17fa380757155d2ba150fd/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/a6fc2a65674177b92b17fa380757155d2ba150fd/scripts/run-examples.sh>) (`just examples`), which CI runs and which also
asserts the example leaves no session behind

### Go

**Source:** [`examples/quickstart/main.go`](<https://github.com/libtmux/libtmux-go/blob/6f3df55f8a41999e41225eefb3940545b834e6a2/examples/quickstart/main.go>)

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

**Checked by:** the whole file runs against a real tmux server as
`TestQuickstart`; the `docs:quickstart` region inside it is additionally
mirrored into README.md by `go generate ./tmux`, and CI fails if the two drift

### Java

**Source:**
[`examples/src/main/java/io/github/libtmux/examples/BuildAWorkspace.java`](<https://github.com/libtmux/libtmux-java/blob/a0ecbc16da00140e2520462d478a2af470878272/examples/src/main/java/io/github/libtmux/examples/BuildAWorkspace.java>)

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

**Checked by:** run against real tmux by the `examples` module's own
`ExamplesRunTest`

### C#

**Source:** [`examples/LibTmux.Examples/Snippets/OneShot.cs`](<https://github.com/libtmux/libtmux-dotnet/blob/ec8b6ab2a4f65e23664f43fba538ba200d4ae8bc/examples/LibTmux.Examples/Snippets/OneShot.cs>)

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

**Checked by:** its `ConnectAndBuild` region is mirrored into README.md and
checked by `sync_snippets.py --check`; the mirrored `csharp run` block is
additionally compiled and run by `ReadmeExampleTests`

### C++

**Source:** [`examples/05-readme.cpp`](<https://github.com/libtmux/libtmux-cxx/blob/3f93b021cb51e8349bfb9cecc6fd4091f6e79a0f/examples/05-readme.cpp>), the [`connect`](<https://libtmux.org/en/cxx/latest/reference/libtmux-connection-connect/>) and [`build`](<https://libtmux.org/en/cxx/latest/workspace/reference/libtmux-workspace-build/>) regions

**In this page:** Copied excerpts from the [`connect`](<https://libtmux.org/en/cxx/latest/reference/libtmux-connection-connect/>) and [`build`](<https://libtmux.org/en/cxx/latest/workspace/reference/libtmux-workspace-build/>) regions.

**Checked by:** quoted verbatim into README.md, checked for drift by
[`tools/docs/check_readme.py`](<https://github.com/libtmux/libtmux-cxx/blob/3f93b021cb51e8349bfb9cecc6fd4091f6e79a0f/tools/docs/check_readme.py>), and the whole file is built and run by CTest

### Swift

**Source:** [`Examples/Sources/ExampleCode/Changing.swift`](<https://github.com/libtmux/libtmux-swift/blob/7993e7044347629b08020e9b02ae8bcffa0e5898/Examples/Sources/ExampleCode/Changing.swift>)

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

**Checked by:** matched against the README's "Change what is there" section by
[`Scripts/check_examples.py`](<https://github.com/libtmux/libtmux-swift/blob/7993e7044347629b08020e9b02ae8bcffa0e5898/Scripts/check_examples.py>); compiled and run through the package's public
products by `swift test --package-path Examples`

### 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.
