Every Java snippet here is executed by a test: ExamplesTest for the ones that
build something, ExecutionModeConformanceTest for the ones about precedence.
One switch, chosen once:
ServerConfig config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(socket))
.mode(ExecutionMode.CONTROL)
.build();
config.mode(); // → CONTROLNothing else changes. The same handles answer the same questions with the same types; only the carrying differs.
| mode | how a command travels | costs | can stream pane output |
|---|---|---|---|
DIRECT (default) |
one tmux process per command | a process each time | no |
CONTROL |
one persistent tmux client | one client, then reused | yes |
VIRTUAL |
a process per command, waited for on a virtual thread | a process each time | no |
DIRECT is what the tmux binary does when a shell runs it. Nothing is held
between commands, so nothing can be stale and nothing needs a session to exist.
CONTROL attaches one client and sends everything down it.
ServerConfig config = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(socket))
.mode(ExecutionMode.VIRTUAL)
.build();
config.mode(); // → VIRTUALVIRTUAL is not an async mode, and nothing about it returns sooner. The call
still blocks until tmux answers; what changes is which thread waits. It is for a
caller stuck on a small pool of platform threads who cannot choose their own —
anyone already on a virtual thread should leave it alone, because every carrier
here is safe to call from one. CarrierStarvationTest runs the suite under a
scheduler with exactly one carrier thread to keep that true.
Measured numbers are in the benchmark, regenerated by
./gradlew modeBenchmark.
A program that names no mode takes the one it is given. Nothing needs rebuilding to try another:
$ LIBTMUX_MODE=control java -jar app.jar$ java -Dlibtmux.mode=control -jar app.jarA value that is not a mode is refused rather than ignored, because falling back to a default would leave you believing a carrier was in force that never was — and every carrier answers the same, so nothing else would give it away.
Highest first:
- Per-call —
server.cmd(argv, timeout, ExecutionMode.DIRECT). - In code —
ServerConfig.builder().mode(…), which settles it. - This JVM —
-Dlibtmux.mode. - This environment —
LIBTMUX_MODE. - Default —
DIRECTwhen nothing says otherwise.
config.mode() reports the one that won, so what is in force is a question with
an answer rather than a guess about what the environment held.
List<String> argv = List.of("display-message", "-p", "#{session_name}");
// carried the way the config said
List<String> byConfig = server.cmd(argv, timeout).stdout();
// carried the way this call said
List<String> byCall = server.cmd(argv, timeout, ExecutionMode.DIRECT).stdout();
byConfig.equals(byCall); // → trueBoth answer the same thing. That is the point, and it is also the reason the override is rarely worth reaching for: nothing a handle returns depends on which carrier answered, so this changes cost and nothing else. The one case where the carrier affects correctness — a command group — routes itself without being asked. Reach for the override when you have measured a reason, not by default.
A carrier made for an override belongs to the server that made it and is closed with it, however the server's own transport was obtained.
Batching and chaining are often listed alongside these. They are not alternatives to them, and putting them on one switch would say they were.
| this | is | and works |
|---|---|---|
server.batch() |
several commands in one request | under any carrier |
server.chain() |
steps acting on what the last one made | under any carrier |
What each buys is measured alongside the carriers in the benchmark.
Control mode attaches to a session, so it cannot carry the command that creates the first one. Until a session exists the carrier falls back to a process, and attaches as soon as there is something to attach to.
try (Server open = Server.open(config)) {
Session first = open.newSession("work"); // carried by a process
first.newWindow(w -> w.named("logs")); // carried by the control client
open.hasSession("work"); // → true
}Nothing in the API marks the difference, and nothing needs to: both answer with the same types. The only trace is the process count in the benchmark.