Attach and send keys
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.
The examples include setup, error handling, and cleanup. Source and verification identifies their files and checks.
>>> 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', '$']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;}//! 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(())}// 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())}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"; } }}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 }}// 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");// 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 shows that pattern, and Filtering and querying, in practice covers absent and ambiguous matches.
Where this comes from¶
Python¶
Source: src/libtmux/server.py, session.py, 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
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
Rust¶
Source: 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 (just examples), which CI runs and which also
asserts the example leaves no session behind
Go¶
Source: 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
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
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, the connect and build regions
In this page: Copied excerpts from the connect and build regions.
Checked by: quoted verbatim into README.md, checked for drift by
tools/docs/check_readme.py, and the whole file is built and run by CTest
Swift¶
Source: 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; 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.