An index of what remains: task-oriented guides, the generated reference and benchmark pages, decisions still in force, and background reference material.
API reference: current trunk, or a released version on javadoc.io.
Task-oriented. Start here.
- Getting started — the first program, compiled and run against a real tmux server.
- Filtering — an expression is a value: printable, storable, and usable as a predicate.
- Options and hooks — reading and setting options and hooks at each of tmux's four scopes.
- Batching and chaining — putting several independent or dependent commands into one tmux invocation.
- Snapshots and handles — what a capture
freezes, and when to call
refresh(). - Streaming — watching pane output as it happens, cheapest first.
- Operating a service — driving tmux from a service: retries, telemetry, and pane input.
- Threads, cancellation, and what runs at once — the blocking and thread-safety contract, and what cancellation leaves behind.
- Driving tmux from a model — the
libtmux-mcpserver, for any MCP client. - Testing with real tmux —
libtmux-junit5: one tmux server per test, always cleaned up. - Kotlin — why the core is already null-safe from Kotlin, and
what
libtmux-kotlinadds. - Scala — the direct-Java path and the separate Scala facade, with their runnable examples.
Generated; do not edit by hand.
- Operation catalog — every
@Operation, generated from its annotation. - Operation costs — measured wall-clock and
tmux-process cost for one-at-a-time, batched, and chained calls; regenerated
by
./gradlew operationBenchmark. - Scala facade modes — the blocking and Cats Effect facades against the Java core on one live tmux, as raw samples with allocation and latency percentiles; a run describes its machine, so none is committed.
Short ADRs for decisions still in force, each citing the tmux (or JDK, Gradle, or Kotlin) behaviour that forced it. Format: Status, Context, Decision, Consequences.
- 0001 Build-logic convention build
- 0002 Blocking process transport
- 0003 Hierarchy hydration, per-entity listings
- 0004 Query expressions, hand-written metamodel
- 0005 Pushdown lowering is exact or refused
- 0006 Real-tmux JUnit 5 fixture lifecycle
- 0007 Row framing with a random separator
- 0008 Control-mode framing and quoting
- 0009 Command groups are transport-agnostic
- 0010 Single process carrier, no execution mode
- 0011 Wait outcomes name why a wait ended
- 0012 Creation specs are builder-built
- 0013 Server identity is pid and start time
- 0014 Watch a server with refresh-client
- 0015 Atomic capture and cursor position
- 0016 libtmux-mcp serves synchronously
- 0017 Kotlin sees core collections as read-only
- 0018 Control-backed transport rejected
- tmux behaviour — version quirks and protocol facts cited from source comments, kept in one lean, reachable place.
- Python API parity — every public Python declaration, and what this port does with it.
- Python test parity map — every Python test, mapped to the contract test that would port it.
This directory is also a build module. It compiles and runs the code in every guide and README, and checks the claims around it.
A snippet is the part of a project people copy and the part nothing compiles, so it goes stale silently — and a stale snippet reads exactly as well as a working one. This module puts every Java fence in the READMEs and guides through javac against the real artifacts, and then runs it against a real tmux server.
$ ./gradlew :docs:testOne case per snippet, named for the file and line it came from, so a failure says
where to look. One tmux server per case, from
libtmux-junit5, so a snippet that makes a session gets a
server nobody else is using.
By default: it must compile and it must run. Compiling proves the API has the shape the document describes; running proves the document is right about what happens, which is what a reader depends on and what a compiler cannot check.
Say otherwise with an HTML comment directly above the fence:
| directive | means |
|---|---|
| (none) | compiles, and runs against live tmux |
<!-- snippet: throws: IllegalArgumentException --> |
runs, and must fail with exactly that |
<!-- snippet: does-not-compile: cannot find symbol --> |
the compiler must reject it, with a diagnostic containing that phrase |
<!-- snippet: compile-only: <reason> --> |
compiles; not run, for the stated reason |
<!-- snippet: skip: <reason> --> |
not checked, for the stated reason |
The comment has to sit directly above the fence, with nothing between them. An unrecognised directive fails the build: a snippet nobody is checking, because of a typo in the thing that says how to check it, is the state this exists to prevent.
throws: names the exception's simple name, not its package — the comparison is
against getClass().getSimpleName(), so IllegalArgumentException matches and
java.lang.IllegalArgumentException does not.
A block that declares a type — a class, record, interface or enum — is
compiled and never run, whatever its directive says, because a declaration has
nothing to execute. Statements are wrapped in a method body, with whatever the
fence's own Given: line asked for (below) in scope; a type is compiled as it
stands and never sees one.
does-not-compile earns its keep: it is what keeps
Pane_.index().startsWith("2") an error. A README claiming the compiler rejects
something would otherwise survive the day it stopped being true. The phrase is
what makes the rejection the documented one: without it, a typo elsewhere in
the block would pass for the error the prose describes.
Every snippet that runs has 15 seconds. A snippet that hangs fails on its own line instead of hanging the build.
A line ending in an arrow is an assertion:
// Given: Server server, Session session
session.name(); // → demo
server.sessions().size(); // → 2
server.hasSession("demo"); // → truePython's doctest is why the sibling library's README can show what every call
returns and still be trusted. Java has no doctest, so this is one: the value after
the arrow is compared against String.valueOf(…) of the expression above it, and
a README cannot claim a value the library does not produce.
Comparing as text means one rule covers a string, a number, a boolean and a list
without a comment having to contain Java literals — what you see after the arrow
is exactly what toString gave.
Two consequences worth knowing:
- Everything after the arrow is the expected value, so prose cannot trail it. Put the explanation on its own comment line above.
- The value is trimmed, so one with a leading or trailing space cannot be expressed this way. Assert it in a test instead.
- The expression has to fit on the line the arrow is on. A call split across lines leaves the rewriter with a fragment, which fails to compile rather than failing quietly — put the value in a local first.
Documentation shows the interesting line, not the ones before it that made a server — but leaning on one of those without saying so is a snippet a reader cannot paste and run, whatever it proves to this build. So the harness offers nothing by default. A snippet that needs one declares it, visibly, as the first line inside the fence:
// Given: Server server
Session session = server.newSession("demo");Given: is not an HTML comment above the fence like the directives below — it
is inside it, in the language the fence is written in, so it is part of what a
reader sees and copies, not part of the machinery checking it. The names on
offer are server (Server), config (ServerConfig), session (Session),
window (Window), pane (Pane), options (Options), socket (Path),
directory (Path), timeout (Duration) and yamlString (String); several
go on one line, comma-separated: // Given: Server server, Session session. A
snippet declaring its own server shadows the supplied one, which is what a
reader copying it would get anyway.
The declaration is held to exactly what the snippet uses, in both directions:
- Uses a name it did not declare fails to compile as an ordinary "cannot find symbol" — nothing of that name exists on the harness the snippet asked for.
- Declares a name it never reads fails too, for that reason specifically.
Java has no "declared and not used" error for a field the way some languages
do for a local, so this half is checked textually: comments are stripped
(the
Given:line itself included) and the rest is searched for the name as a whole word. A name mentioned only in prose does not count as used, and a name inside a string this check cannot tell from code would be a false negative it does not try to catch — keep aGiven:line to real bindings and this does not come up.
A fence with no Given: line gets nothing and must be self-contained.
Consequently a fence cannot depend on a variable another fence declared, a
harness field it never asked for, or on being read in the order it prints —
and neither can a reader who copies just that fence.
SnippetCompilerTest
pins this mechanism directly, the way go's own doc-generator pins the same
property for its regions: a binding declared and used compiles, one used but
not declared fails as "cannot find symbol", and one declared but not used fails
as unused — with no tmux server needed for any of the three, since compiling a
snippet never starts one.
This module reads Java. The Kotlin fences in the root README, libtmux-kotlin's
README and the Kotlin guide are checked a different way: libtmux-kotlin has a
generateDocumentationSnippets task that turns each one into a test function, and
the ordinary Kotlin compilation and test run do the checking.
Generating a source file rather than running the Kotlin compiler in-process is the same guarantee by a shorter road — and because the generated file is the documentation, the two cannot drift.
$ ./gradlew :libtmux-kotlin:testA Kotlin fence gets server and nothing else unless its first line asks, the
same rule as the Java Given: line: // Given: config: ServerConfig or
// Given: session: Session, window: Window, pane: Pane. The names on offer are
config, session, window, pane and socket, and a name outside them fails
the task. Only one direction is checked: a Kotlin fence that uses a name it did
not ask for fails to compile, but one that asks for a name it never reads is not
caught, because the generated file suppresses unused-variable warnings.
A snippet is executed, so it cannot lie. A version in an install block, or a list of what the platform manages, is prose — and prose is what is still wrong six months later, in the one place every reader starts. Those are checked too:
| what is checked | where it looks |
|---|---|
| Every coordinate names the version this build would publish | the root README, libtmux-bom's, every published module's, the Kotlin and Scala guides, and RELEASING.md |
libtmux-bom's README lists exactly what the platform constrains |
that README against libtmux-bom/build.gradle.kts |
| Every published module's README names it first and states its coordinate | each published module's README |
| A fence in a source language nothing here builds carries a directive saying so | every reader-facing document |
| The contract tests the parity documents cite are unwritten or really declared | docs/parity/python-api.md, docs/parity/test-map.md |
| Those documents keep saying "planned parity" while those tests are unwritten | the same two |
The last two are why this module reads documents it takes no snippets from.
docs/parity/ holds no Java, and RELEASING.md is not a place snippets come
from, but a coordinate in either is a claim like any other.
The snippet suite also asserts a floor on how much it found. A filter or a rename can reduce a parameterised suite to nothing without failing anything, and a suite that discovers nothing passes loudly.
Snippets come from README.md, MIGRATION.md, every package's README.md, and
every guide under docs/guide/. The checks above that are not about snippets read more than
that, and each row says where it looks.
Not docs/decisions or docs/internals: those are dated or historical
records. Holding them to today's API would either break the build or quietly
rewrite history, and neither is what a record is for.