Every snippet here is run by DocumentationSnippetsTest, except one marked
compile-only, which says why. If one stops working the build fails, rather
than the page quietly going stale.
Every Java block in this guide runs against a real tmux server when the build
runs, and every value shown after a → is asserted.
A Server is a client, not the tmux process. Closing one closes your connection;
it never ends anybody's sessions.
// Given: Path socket
ServerConfig config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(socket))
.build();
try (Server server = Server.open(config)) {
Session session = server.newSession("demo");
Window window = session.newWindow("build");
Pane pane = window.split();
pane.sendLine("echo hello from libtmux");
}And what it leaves behind:
// Given: Server server
Session session = server.newSession("demo");
Window window = session.newWindow("build");
session.name(); // → demo
window.name(); // → buildEach command runs through one tmux process. Use a batch or chain when several commands should share an invocation.
Server.open owns the transport it creates and closes it. Server.using borrows
one you own and never closes it, so several servers can share a transport.
To end the tmux server itself, ask plainly:
// Given: Server server
server.killServer();Sessions, windows and panes are all made the same way: call it plainly, describe it with a lambda, or hand it a description you built earlier.
// Given: Server server
Session build = server.newSession(s -> s.named("build").firstWindowNamed("editor"));
Window logs = build.newWindow(w -> w.named("logs").running("sleep", "30"));
build.name(); // → build
build.refresh().windows().get(0).name(); // → editor
logs.name(); // → logsA new window or pane is selected, because that is what new-window and
split-window do. Say detached() to leave the current one where it is. A
session is the exception and is always detached: new-session attaches unless
told not to, and attaching needs a terminal that a build, a service or an agent
does not have.
split() takes tmux's defaults. Anything else is described by a lambda over a
builder:
// Given: Pane pane, Path directory
Pane side = pane.split(s -> s.toRight().percent(30));
Pane app = pane.split(s -> s.running("sleep", "30").in(directory));
side.edges().right(); // → true
pane.window().refresh().panes().size(); // → 3A description is also a value, so one can be named and applied wherever it fits:
// Given: Session session
SplitSpec sidebar = SplitSpec.builder().toRight().percent(25).build();
Pane leftSide = session.newWindow("left").split(sidebar);
Pane rightSide = session.newWindow("right").split(sidebar);
leftSide.edges().right(); // → true
rightSide.edges().right(); // → trueA size is one thing with two spellings — cells(5) or percent(30) — so there
is no size-and-percentage pair to hold consistent. What runs in the pane is one
choice too: a shell, a command, or nothing at all. tmux rejects a command on an
empty pane, so no spec can carry both.
Options that arrived in tmux 3.7 — an empty pane, keeping a pane after its
command exits, per-pane styles — throw UnsupportedFeatureException on an
older server:
// Given: Server server, Pane pane
if (!server.version().atLeast(new TmuxVersion(3, 7, ""))) {
assertThrows(UnsupportedFeatureException.class, () -> pane.split(s -> s.empty()));
}Being told is the point. A split that silently ignored empty() would hand back
a pane with a shell in it, and nothing downstream could tell that apart from the
pane that was asked for.
Accessors read tmux once and hand back handles over what they saw. Walking the hierarchy afterwards issues no commands at all:
// Given: Server server
for (Session session : server.sessions()) {
for (Window window : session.windows()) {
for (Pane pane : window.panes()) {
// The window a pane reports is the one it was reached through.
pane.window().id().equals(window.id()); // → true
}
}
}
// One read. Walking it asked tmux nothing further.
server.sessions().get(0).windows().size(); // → 1That is deliberate. tmux offers no transaction across separate listings, so a
traversal that re-queried could observe a hierarchy that never existed. To see
newer state, take a new capture with refresh().
Live listings, finders and server.snapshot() throw LibTmuxException when a
capture fails, including when no daemon is running. Empty lists and optionals
mean a successful capture found no matches. Use isAlive() when you only need a
liveness probe; transport failures still throw.
A handle's identity is what a user cannot change. A session is its server and its
id, so renaming it does not produce a different session. A window is its
winlink — session, index and window together — because a window linked into two
sessions is one window at two positions, and tmux orders and addresses those
separately. Window.id() compares the underlying window across links.
A scope is chosen when you take the view, so you cannot read one scope and write another:
// Given: Server server, Session session
server.globalOptions().set("base-index", "1");
session.options().get("base-index").orElseThrow(); // → 1get reports the value tmux will act on, inherited when the scope does not set
it. all() answers the narrower question — what this scope sets itself.
A batch is one tmux invocation, and every operation gets its own outcome:
// Given: Server server
BatchResult result = server.batch()
.add("new-window", "-d", "-n", "one")
.add("new-window", "-d", "-n", "two")
.run();
result.succeeded(); // → true
result.operations().get(0).outcome(); // → COMPLETE
result.operations().get(1).outcome(); // → COMPLETEtmux discards a group after its first failure, so a single exit status cannot say
which command failed or which never ran. Each operation is reported as
COMPLETE, FAILED, SKIPPED or UNKNOWN.
A chain is the same machinery where each step acts on what the last one made, using tmux's own current-target following:
// Given: Server server
server.chain()
.newWindow("built")
.splitLeftRight()
.sendLine("echo chained")
.run();
// One request made the window and split it, with no round trip to learn its id.
server.windows().stream().anyMatch(w -> w.name().equals("built")); // → trueNo step names a target, and no round trip is needed to learn the id of something just created.
A control client stays attached and pushes terminal output as it happens:
// Given: Server server, Session session
try (ControlClient client = server.control(session);
EventSubscription<PaneOutput> output = client.subscribeOutput(32)) {
client.send("send-keys", "-t", session.name(), "echo streamed", "Enter");
// Output arrives in frames as tmux flushes it, so one line can span several.
StringBuilder seen = new StringBuilder();
while (seen.indexOf("streamed") < 0) {
seen.append(Delivery.kept(output.next(Duration.ofSeconds(5)).orElseThrow()).data());
}
seen.indexOf("streamed") >= 0; // → true
}Control-mode requests are independent: a failure discards nothing behind it, and
every reply carries the request that produced it. Attaching is what makes tmux
push output at all. The bounded subscription reports overflow through
droppedCount() and never runs caller code on the reply reader. A full buffer's
next read is a Delivery.Gap before the events that remain. Delivery.kept
fails that read. Match on Delivery.Gap to continue. A subscription does
not reconnect.
A run that reads the developer's own .tmux.conf is a run whose behaviour nobody
can predict. Pin one:
// Given: Path directory
Path tmuxConf = Files.writeString(directory.resolve("tmux.conf"), "set -g base-index 5\n");
ServerConfig pinned = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(directory.resolve("pinned")))
.configFile(tmuxConf)
.build();
try (Server server = Server.open(pinned)) {
server.newSession("configured").windows().get(0).index().value(); // → 5
server.killServer();
}| you want to | read |
|---|---|
| select things without lambdas | filtering |
| read and write tmux's settings | options and hooks |
| send several commands at once | batching and chaining |
| understand what a handle is | snapshots and handles |
| watch output as it happens | streaming |
| call it from several threads | concurrency |
| test your own code against tmux | testing |
See the migration notes when upgrading.