libtmux Reference MCP Search

Capture pane output

Edit this page on GitHub

The other half of Attach and send keys: reading what a pane printed, and — since tmux accepts a command before the shell running it has necessarily finished (see Sending keys) — waiting for the right moment to read rather than reading immediately or sleeping a guessed amount. Capturing output is the guide-level discussion of why each pattern below exists; this page is the sourced code behind it — see the table at the end for exactly which file each block came from and how it’s checked.

Read what’s on screenLink to section

import type { Server } from "libtmux";
/**
* Move text through a tmux buffer, and read what a pane is showing.
*
* The half of orchestration that is not making things happen. A named buffer
* is tmux's own clipboard: anything in it can be pasted into any pane on the
* server, by this process or by a person at the keyboard, without the text
* passing through your program a second time.
*/
export async function moveTextThroughABuffer(
server: Server,
text: string,
): Promise<{ named: readonly string[]; roundTripped: readonly string[] }> {
// tmux stores nothing for empty text and reports success, so a buffer whose
// content is computed has to be checked before it is written — otherwise the
// name is absent and the next call to read it is what fails.
if (text === "") throw new Error("tmux holds no empty buffer");
await server.setBuffer("report", text);
const roundTripped = await server.showBuffer("report");
const named = await server.listBuffers();
// Buffers outlive the program that made them.
await server.deleteBuffer("report");
return { named, roundTripped };
}
/**
* What a pane is showing, scrollback included.
*
* `start` counts back from the visible top, so -100 asks for the last hundred
* lines or as many as exist. A pane that has printed nothing answers with
* nothing rather than with blank lines.
*/
export async function readPane(server: Server): Promise<readonly string[]> {
const session = await server.newSession({ name: "capture" });
const pane = session.activePane;
if (pane === undefined) throw new Error("a new session always has one pane");
return pane.capture({ start: -100 });
}

No checked .NET example calls the ordinary Pane.CaptureAsync by itself outside a wait — the README’s own read is the block under “Wait for text instead of guessing a delay” below, and the separate Psmux surface has its own CaptureAsync for that different transport (examples/LibTmux.Examples/Snippets/Psmux.cs).

Wait for text instead of guessing a delayLink to section

Python’s checked wait is wait_for, tmux’s own signal channel, rather than a helper that scrapes pane text for a pattern — no checked helper that waits on pane text was found in the source for this page.

import type { Server } from "libtmux/server";
/**
* Drive tmux the way an agent does: act, then wait for the result.
*
* One control observer carries notifications while commands use the server's
* engine. Keeping the observer open makes event-driven waits persistent; it
* does not turn command output into a control-mode protocol.
*/
export async function runAndWait(server: Server, command: string, marker: string): Promise<string> {
// A connection attaches, so the session has to exist first. On a server with
// none, `connect()` fails saying exactly that.
const session = await server.newSession({ name: "agent" });
await using live = await server.connect({ target: session.id });
const pane = (await live.snapshot()).sessions.one({ id: session.id }).panes.one();
// Subscribe before acting. A marker printed between the command and the
// subscription is one nobody is listening for, and the wait never ends.
const printed = live
.subscribe()
.find(
(event) => event.kind === "output" && event.paneId === pane.id && event.data.includes(marker),
{ timeoutMs: 30_000 },
);
await pane.sendKeys(command);
const event = await printed;
if (event === undefined) throw new Error(`${command} never printed ${marker}`);
return event.kind === "output" ? event.data : "";
}
/**
* Wait for the server to reach a shape, rather than for one event.
*
* `waitFor` reads the server, then re-reads on each notification, so it returns
* at once when the condition already holds and does not miss a change that
* lands while it is subscribing.
*/
export async function buildAndSettle(server: Server, windows: readonly string[]): Promise<number> {
const session = await server.newSession({ name: "settling" });
await using live = await server.connect({ target: session.id });
const bound = (await live.snapshot()).sessions.one({ id: session.id });
for (const name of windows) {
// eslint-disable-next-line no-await-in-loop -- window order is observable.
await bound.newWindow({ name });
}
const settled = await live.waitFor(
(snapshot) => windows.every((name) => snapshot.windows.exists({ name })),
{ timeoutMs: 30_000 },
);
return settled.windows.count({ session: { is: { id: session.id } } });
}

Session.OpenNotifications streams what tmux does as it happens rather than polling — tmux pushes each change instead of a poll guessing how often to ask. tmuxtest.WaitForText (see Testing with libtmux) is the equivalent built specifically for tests.

Rust’s wait_for_text looks before it sleeps, joins wrapped lines so a needle spanning a wrap still matches, and returns PaneWait::Dead rather than hanging forever if the pane’s process ends first.

Attaching a ControlClient is what makes tmux push %output at all — a client that never attaches only ever hears command replies.

TmuxWait.UntilAsync polls a read against a predicate rather than sleeping a fixed amount.

C++ has no checked snippet that waits on pane text. Server::wait_for(channel, timeout), in include/libtmux/server.hpp, uses tmux’s own wait-for signal instead of scraping output, and its doc comment explains why that is the safer choice when the command you are waiting on can be made to announce itself: “a server that dies under a waiter makes tmux exit zero, which is indistinguishable from being signalled … this reports that as a failure instead.”

waitForOutput takes patterns for both success and failure, so a process that fails fast is discovered immediately rather than by timing out.

Where this comes fromLink to section

PortSourceIn this pageChecked by
Pythonsrc/libtmux/pane.py (capture_pane), src/libtmux/server.py (wait_for) docstringshand-quotedpytest runs every >>> doctest against a real, isolated tmux session on every test run
TypeScriptexamples/capture/capture.ts (read), examples/agent/agent.ts (wait)read whole from each fileboth run against real tmux by bun test examples; agent.ts is additionally mirrored into README.md under a <!-- runs: ... --> marker checked by scripts/check-doc-runnable.ts
Goexamples/quickstart/main.go (read, already shown whole on the previous page), examples/control-mode-subscribe/main.go (wait)read: hand-quoted; wait: read whole from the fileboth run against real tmux as TestQuickstart / TestControlModeSubscribe; the wait file’s docs:watching region is additionally mirrored into README.md by go generate ./tmux
Rustcrates/libtmux/examples/scratch.rs, already shown whole on the previous pagehand-quoted excerpts of the same filerun to completion against a throwaway tmux by scripts/run-examples.sh, which CI runs
Javaroot README.md Quickstart (read), examples/src/main/java/io/github/libtmux/examples/WatchPaneOutput.java (wait)read: hand-quoted; wait: read whole from the fileevery README fence is compiled and run against real tmux by docs-tests; WatchPaneOutput is additionally run by the examples module’s ExamplesRunTest
.NETroot README.md, “Running something, and reading it back”hand-quotedone of the csharp run blocks compiled and run against real tmux by ReadmeExampleTests
C++examples/05-readme.cpp capture region (read); include/libtmux/server.hpp doc comment (wait, no fence)hand-quotedthe capture region is quoted verbatim into README.md and checked by tools/docs/check_readme.py; the whole file is built and run by CTest
SwiftExamples/Sources/ExampleCode/Changing.swift (read, already shown whole on the previous page), Waiting.swift (wait)read: hand-quoted excerpt; wait: read whole from the fileboth matched against the README by Scripts/check_examples.py and run by swift test --package-path Examples

Go, Rust, and Swift each reuse a file already shown in full on Attach and send keys: rather than dump the same file a second time, this page quotes just the relevant lines by hand, with a comment naming the source, and points back at the full listing there.

Esc

Type to search.