Skip to content

Latest commit

 

History

History
272 lines (205 loc) · 9.53 KB

File metadata and controls

272 lines (205 loc) · 9.53 KB

Getting started

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.

Reaching a server

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();                             // → build

Each 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();

Describing what you create

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();                               // → logs

A 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.

Describing a split

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();    // → 3

A 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();                 // → true

A 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.

A capture is a moment

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();   // → 1

That 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.

Identity survives change

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.

Options and hooks

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();   // → 1

get 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.

Running several commands

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();      // → COMPLETE

tmux 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"));   // → true

No step names a target, and no round trip is needed to learn the id of something just created.

Watching output

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.

Pinning tmux's configuration

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();
}

Where to next

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.