Core Library0.xAPI Reference

Choose documentation 3

latest

Current version

latest
English

Early release libtmux is released, and its APIs may still change between releases. This site's structure and URLs may change too.

method libtmux-java latest

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.awaitText
Package
io.github.libtmux
Declared in
Pane
Distribution
io.github.libtmux:libtmux
Source
libtmux/src/main/java/io/github/libtmux/Pane.java

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 name to it and block on Server#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-limit is 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/cat and /bin/sh available. In an empty directory, save this consumer setup as settings.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 source

org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8
kotlin.daemon.jvmargs=-Xmx2g
org.gradle.workers.max=2

Save 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/sh
set -eu
if [ "$#" -eq 0 ]; then
printf 'Usage: sh run.sh program [arguments...]\n' >&2
exit 2
fi
binary=$(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_PANE
for name in $(env | sed -n 's/^\(LIBTMUX_SOCKET[A-Za-z0-9_]*\)=.*/\1/p'); do
unset "$name"
done
mkdir -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 1
attempts=0
while serves "$1" && [ "$attempts" -lt 60 ]; do
sleep 0.05
attempts=$((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 '' PIPE
set +e
stuck=
left=
if [ -S "$socket" ] && serves "$socket" && ! stop "$socket"; then
stuck="$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=$IFS
IFS='
'
set -f
for candidate in $(find "$tmpdir" "$directory" -type s 2>/dev/null); do
[ "$candidate" = "$socket" ] && continue
serves "$candidate" || continue
case "$candidate" in
"$tmpdir"/*) name=${candidate#"$tmpdir"/} ;;
*) name=${candidate#"$directory"/} ;;
esac
left="$left $name"
stop "$candidate" || stuck="$stuck $name"
done
set +f
IFS=$old_ifs
if [ -n "$stuck" ]; then
printf 'tmux still answers on%s after kill-server; kept %s and %s under the runner root\n' \
"$stuck" "${directory##*/}" "${tmpdir##*/}" >&2
exit 1
fi
rm -rf "$directory" "$tmpdir" || exit 1
if [ -n "$left" ]; then
printf 'The program left a tmux server running:%s\n' "$left" >&2
exit 1
fi
exit "$status"
}
trap cleanup 0
trap '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 ]; then
printf 'API_RUN_ROOT is too long for a tmux socket\n' >&2
exit 1
fi
TMUX_TMPDIR=$tmpdir
export TMUX_TMPDIR
printf '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 source

package 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-example

Expected output:

api-keys
api-line
Parameters
  • text (String) # – the text to wait for, matched anywhere in a captured line for awaitText(text, timeout)

  • timeout (Duration) # – how long to keep looking for awaitText(text, timeout)

  • text (String) # for awaitText(text, timeout, every)

  • timeout (Duration) # for awaitText(text, timeout, every)

  • every (Duration) # – how long to leave between looks, at least 10 ms for awaitText(text, timeout, every)

Returns

why the wait ended, which tells text that appeared from text that was already there for awaitText(text, timeout)

Raises
  • LibTmuxException – 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 as TextOutcome.PANE_GONE, and #watchText() also returns what it had shown for awaitText(text, timeout)

  • InterruptedException – if the waiting thread is interrupted, which is a cancellation rather than a timeout and so is not reported as one for awaitText(text, timeout)

  • IllegalArgumentException – if every is shorter than 10 ms for awaitText(text, timeout, every)

Esc

Type to search.

Reference previewOpen reference