Pane.awaitText
Waits until this pane's text contains something, and says why the wait ended.
Source and package
- Qualified name
-
io.github .libtmux .Pane. await Text - Package
- io.github.libtmux
- Declared in
Pane- Distribution
- io.github.libtmux:libtmux
- Source
- libtmux/src/main/java/io/github/libtmux/Pane.java
- @Operation(Kind
.WAIT ) public TextOutcome awaitText(String text, Duration timeout) throws InterruptedException @Operation(Kind.WAIT ) public TextOutcome awaitText( String text, Duration timeout, Duration every) throws InterruptedException -
Description#
Reach for this last. It reads the screen on a timer, which is a heuristic about a program's output rather than a fact about it, and the cheaper waits are exact:
- If you wrote the command, append
; tmux wait-for -S nameto it and block onServer#channel(). tmux blocks server-side and returns on the signal itself, so nothing is inferred from the screen and one tmux process covers the whole wait. That is the only deterministic wait here, and it is the right one whenever the command is yours. - If you did not write the command — a daemon printing that it is ready, a build somebody else started — this is the case polling is for.
The timeout bounds the whole wait, reads included. Each read is given only what is left of it, so a slow tmux cannot stretch a short wait out to the server's default deadline, and text that first appears after the deadline is not reported. The first read alone is allowed at least a quarter of a second, so a timeout shorter than one read can still answer
TextOutcome.PRESENT_AT_ENTRY.Waiting longer is also less reliable, not more: tmux frees the oldest scrollback once
history-limitis reached, so a long wait on a productive pane can end up reading past the lines it was watching for.Examples#
Send input and wait for output before capturing the pane. Use JDK 25, Gradle 9 or newer, and tmux 3.3a or newer on Linux, with
/bin/catand/bin/shavailable. In an empty directory, save this consumer setup assettings.gradle.kts:rootProject.name = "api-example"Save this consumer setup as
build.gradle.kts. It depends on the 0.1.2 release from Maven Central:plugins {application}repositories { mavenCentral() }dependencies {implementation("io.github.libtmux:libtmux:0.1.2")}java { toolchain { languageVersion.set(JavaLanguageVersion.of(25)) } }application { mainClass.set(providers.gradleProperty("exampleMain")) }Save this consumer setup as
gradle.properties: View sourceorg.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8kotlin.daemon.jvmargs=-Xmx2gorg.gradle.workers.max=2Save this launcher as
run.sh. It creates and stops a private tmux server, including after program failure. A shutdown failure retains its socket directory and exits unsuccessfully. View source#!/bin/shset -euif [ "$#" -eq 0 ]; thenprintf 'Usage: sh run.sh program [arguments...]\n' >&2exit 2fibinary=$(command -v "${TMUX_BIN:-tmux}")root=${API_RUN_ROOT:-/tmp/libtmux-java-dev}# A client that names no socket follows TMUX or LIBTMUX_SOCKET*, and otherwise lands on the default# socket under TMUX_TMPDIR. The program gets none of them, and a TMUX_TMPDIR of its own below.unset TMUX TMUX_PANEfor name in $(env | sed -n 's/^\(LIBTMUX_SOCKET[A-Za-z0-9_]*\)=.*/\1/p'); dounset "$name"donemkdir -p "$root"directory=$(mktemp -d "$root/api.XXXXXX")tmpdir=$(mktemp -d "$root/tmpdir.XXXXXX")socket="$directory/tmux.sock"config="$directory/tmux.conf"serves() {"$binary" -S "$1" display-message -p '#{pid}' >/dev/null 2>&1}# Stops the server on the socket, then waits for it to stop answering.stop() {"$binary" -S "$1" kill-server || return 1attempts=0while serves "$1" && [ "$attempts" -lt 60 ]; dosleep 0.05attempts=$((attempts + 1))done! serves "$1"}cleanup() {status=$?trap - 0 HUP INT TERM PIPE# A closed output must not end the cleanup, whose messages are the only thing left to write.trap '' PIPEset +estuck=left=if [ -S "$socket" ] && serves "$socket" && ! stop "$socket"; thenstuck="$stuck tmux.sock"fi# Every socket anywhere under either directory, without following links. Only one a server answers# on is a leak; the runner's own is expected to be there.old_ifs=$IFSIFS=''set -ffor candidate in $(find "$tmpdir" "$directory" -type s 2>/dev/null); do[ "$candidate" = "$socket" ] && continueserves "$candidate" || continuecase "$candidate" in"$tmpdir"/*) name=${candidate#"$tmpdir"/} ;;*) name=${candidate#"$directory"/} ;;esacleft="$left $name"stop "$candidate" || stuck="$stuck $name"doneset +fIFS=$old_ifsif [ -n "$stuck" ]; thenprintf 'tmux still answers on%s after kill-server; kept %s and %s under the runner root\n' \"$stuck" "${directory##*/}" "${tmpdir##*/}" >&2exit 1firm -rf "$directory" "$tmpdir" || exit 1if [ -n "$left" ]; thenprintf 'The program left a tmux server running:%s\n' "$left" >&2exit 1fiexit "$status"}trap cleanup 0trap 'exit 1' HUP INT TERM PIPE# A default socket path longer than this does not fit in sun_path.default_socket="$tmpdir/tmux-$(id -u)/default"if [ "$(printf '%s' "$default_socket" | wc -c)" -gt 100 ]; thenprintf 'API_RUN_ROOT is too long for a tmux socket\n' >&2exit 1fiTMUX_TMPDIR=$tmpdirexport TMUX_TMPDIRprintf 'set-option -g default-shell /bin/sh\n' > "$config""$binary" -S "$socket" -f "$config" new-session -d -s work-one -n editor /bin/cat"$binary" -S "$socket" split-window -h -t '=work-one:editor' /bin/cat"$binary" -S "$socket" new-window -d -t '=work-one' -n logs /bin/cat"$binary" -S "$socket" new-session -d -s work-two -n editor /bin/cat"$@" "$binary" "$socket" "$config""$binary" -S "$socket" has-session -t '=work-one'"$binary" -S "$socket" has-session -t '=work-two'Save this complete program as
src/main/java/Capture.java. It opens an ordinary client for the launcher's server; closing the client releases its transport. View sourcepackage io.github.libtmux.examples.api.java;import io.github.libtmux.Server;import io.github.libtmux.ServerConfig;import io.github.libtmux.ServerEndpoint;import io.github.libtmux.SessionSpec;import io.github.libtmux.TextOutcome;import java.nio.file.Path;import java.time.Duration;import java.util.List;/** Send input and wait for output before capturing the pane. */public final class Capture {private Capture() {}public static void main(String[] args) {try {if (args.length != 3) {throw new IllegalArgumentException("expected: tmux-binary socket-path config-file");}var config = ServerConfig.builder().binary(args[0]).endpoint(ServerEndpoint.socketPath(Path.of(args[1]))).configFile(Path.of(args[2])).build();try (Server server = Server.open(config)) {var session = server.newSession(SessionSpec.builder().named("api-capture").running("/bin/sh").build());var pane = session.windows().getFirst().panes().getFirst();pane.sendKeys(List.of("printf 'api-%s\\n' keys", "Enter"));if (pane.awaitText("api-keys", Duration.ofSeconds(5)) == TextOutcome.TIMED_OUT) {throw new IllegalStateException("timed out waiting for keys output");}pane.sendLine("printf 'api-%s\\n' line");if (pane.awaitText("api-line", Duration.ofSeconds(5)) == TextOutcome.TIMED_OUT) {throw new IllegalStateException("timed out waiting for line output");}var lines = pane.capture();for (String expected : List.of("api-keys", "api-line")) {if (lines.stream().noneMatch(line -> line.strip().equals(expected))) {throw new IllegalStateException("capture is missing " + expected);}System.out.println(expected);}}} catch (Exception error) {if (error instanceof InterruptedException) Thread.currentThread().interrupt();System.err.println("Example failed: " + error.getMessage());System.exit(1);}}}Build the minimal consumer with Gradle and run the saved program with the private fixture. An operation or cleanup error makes the command exit unsuccessfully:
Terminal window $ gradle --console=plain --quiet installDist -PexampleMain=io.github.libtmux.examples.api.java.Capture && \sh run.sh build/install/api-example/bin/api-exampleExpected output:
api-keysapi-line- Parameters
- Returns
-
why the wait ended, which tells text that appeared from text that was already there for
awaitText(text, timeout) - Raises
-
-
Lib
Tmux – if a read fails for a reason of the caller's while the server is still answering and still lists this pane. A pane killed mid-wait is not one: that ends the wait asException TextOutcome.PANE_GONE, and#watchText()also returns what it had shown forawaitText(text, timeout) -
Interrupted
Exception – if the waiting thread is interrupted, which is a cancellation rather than a timeout and so is not reported as one forawaitText(text, timeout) -
Illegal
Argument – ifException everyis shorter than 10 ms forawaitText(text, timeout, every)
-
- If you wrote the command, append