Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
3b89055
Examples(style[RunACommand]): Extract report
tony Oct 4, 2026
1e82be8
Examples(style[WatchPaneOutput]): Split run apart
tony Oct 4, 2026
d1c69b4
Examples(style[WatchWhatChanges]): Split the loop
tony Oct 4, 2026
a9597ac
Examples(style[BuildAWorkspace]): Lift the layout
tony Oct 4, 2026
a37c022
Examples(style[FindPanesRunning]): Use a for loop
tony Oct 4, 2026
83eb50b
Examples(style[ServeTmuxOverMcp]): Extract setup
tony Oct 4, 2026
3ac502d
Examples(style[java]): Move bodies into run()
tony Oct 4, 2026
113cade
Examples(style[WatchWithFlow]): Name the callbacks
tony Oct 4, 2026
66b6b1d
Examples(style[kotlin/Capture]): Add awaitShown
tony Oct 4, 2026
91812b7
Examples(style[kotlin]): Move bodies into execute
tony Oct 4, 2026
d6e041d
Examples(style[BlockingWorkspace]): Wrap comments
tony Oct 4, 2026
5ba8c1e
Examples(style[CaptureConcurrently]): Wrap note
tony Oct 4, 2026
9b3de6e
Examples(style[ObserveChanges]): Wrap import note
tony Oct 4, 2026
6c88631
Examples(style[ResourceBoundaries]): Wrap import
tony Oct 4, 2026
da6cc3c
Migration(style[fences]): Break long switch cases
tony Oct 4, 2026
f610a7c
README(style[fences]): Break chains, spread map
tony Oct 4, 2026
1fd6eb9
Docs(style[concurrency]): Shorten fan-out lambda
tony Oct 4, 2026
3994ca0
Docs(style[filtering]): Close assertion gap
tony Oct 4, 2026
0331f10
Docs(style[getting-started]): Break outer call
tony Oct 4, 2026
630e597
Docs(style[mcp]): Name the wait; tag output text
tony Oct 4, 2026
5d673e3
Docs(style[operating-a-service]): Name config
tony Oct 4, 2026
b829f05
Docs(style[options-and-hooks]): Shorten hooks
tony Oct 4, 2026
b9f3036
Docs(style[scala-getting-started]): Wrap comment
tony Oct 4, 2026
afac248
Docs(style[scala-query]): Break the filter lambda
tony Oct 4, 2026
d476670
Docs(style[snapshots-and-handles]): Wrap a comment
tony Oct 4, 2026
32d7e53
Docs(style[streaming]): Name the streaming steps
tony Oct 4, 2026
07e56ea
Examples(style[README]): Split the java command
tony Oct 4, 2026
c5705a2
Examples(style[api-README]): Hold root in $src
tony Oct 4, 2026
7cb9cb7
Jackson(style[README]): Name pane, break chain
tony Oct 4, 2026
fb0cc96
JUnit5(style[README]): Name capture and event
tony Oct 4, 2026
fcc5cea
Kotlin(style[README]): Break branch, name session
tony Oct 4, 2026
f80b8bc
MCP(style[README]): Pretty-print the run result
tony Oct 4, 2026
1a80e72
Cats(style[README]): Break filter at &&
tony Oct 4, 2026
2abe4e6
Workspace(style[README]): Name YAML, break text
tony Oct 4, 2026
387eb62
Library(style[README]): Name the session stream
tony Oct 4, 2026
2ea100e
Pane(style[await]): Name the await limit
tony Oct 4, 2026
99d344c
Pane(style[expand]): Bind the format string
tony Oct 4, 2026
946a095
Server(style[newSession]): Name Dimensions
tony Oct 4, 2026
23115b5
Server(style[paneFields]): Name the field list
tony Oct 4, 2026
21c1263
Server(style[within]): Name scoped server
tony Oct 4, 2026
7c98ef0
Session(style[newWindow]): Run top not journalctl
tony Oct 4, 2026
3c8c489
WindowSpec(style[example]): Run top in the example
tony Oct 4, 2026
74b6448
SessionSpec(style[example]): Name Dimensions
tony Oct 4, 2026
a0519b5
EventSubscription(style[stream]): Shorten lambda
tony Oct 4, 2026
52b6afc
Notification(style[example]): Shorten bindings
tony Oct 4, 2026
7b91d1d
OptionKey(style[example]): Drop the kept binding
tony Oct 4, 2026
1c9dfd7
FakeTmux(style[example]): Shorten build line
tony Oct 4, 2026
6c27d35
Contributing(style[console]): Break the reap loop
tony Oct 4, 2026
8a318e6
Releasing(style[fences]): Break secret, tag output
tony Oct 4, 2026
e57563a
ModuleTests(style[README]): Break the sbtw run
tony Oct 4, 2026
e6a76dd
Examples(style[build.gradle.kts]): Wrap comments
tony Oct 4, 2026
92584c8
Examples(style[scala-cats]): Break the dependency
tony Oct 4, 2026
93bb181
Examples(style[api-run]): Break the first session
tony Oct 4, 2026
41bf176
Examples(style[ApiProgramsTest]): Name the checks
tony Oct 4, 2026
097cc6b
Examples(style[ExamplesRunTest]): Shorten checks
tony Oct 4, 2026
e9dd512
Docs(docs[WRITING]): Add the Examples section
tony Oct 4, 2026
aa8ffcc
ai(rules[AGENTS]): Route code examples to WRITING
tony Oct 4, 2026
8d2e3bb
Examples(chore[width]): Add the width check
tony Oct 4, 2026
34df9b0
Docs(test[javadoc]): Run <pre>{@code} blocks
tony Oct 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ Then find whose it is, since a socket path exists only in the process's own
command line:

```console
$ for p in $(pgrep tmux); do tr '\0' ' ' < /proc/$p/cmdline | rg -o '\-S [^ ]+'; done
$ for p in $(pgrep tmux); do \
tr '\0' ' ' < /proc/$p/cmdline | rg -o '\-S [^ ]+'; \
done
```

Kill only sockets under this port's roots. Another port's servers are not yours
Expand Down Expand Up @@ -136,6 +138,14 @@ How a fence says what it is — the directives, the `// →` assertions, the
fixtures a snippet may assume — is in [`docs/README.md`](../docs/README.md#how-these-pages-are-tested), beside
the code that reads them.

Examples, whether programs, Javadoc blocks, or Markdown fences, stay within 80
columns. Unlike the Gradle gates this one is a script, and it runs in CI before
them:

```console
$ python3 tools/check_example_width.py
```

A green `check` that reported `UP-TO-DATE` for every task verified nothing.
Force it when that matters:

Expand Down
90 changes: 90 additions & 0 deletions .github/WRITING.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,3 +429,93 @@ Bad:
# Show the last ten commits as a graph
$ git log --max-count=10 --graph --oneline
```

## Examples

<!-- shared:examples -->

An example is code written for a reader: a program under `examples/`, code in
a doc comment or docstring, and every fenced block in a README or docs page.
Shell blocks also follow [Code blocks](#code-blocks).

The text between the shared markers is the same in every libtmux port.
Change it in all of them together.

### Width

- **Examples stay within 80 columns.** They render in fixed-width boxes that
scroll sideways, and 80 columns fits a libtmux.org code block in a
laptop-width window. Comments inside examples wrap at 80 too.
- **The width check enforces it.** It reads the tracked files that
`.github/example-width.toml` names and fails on a wider line. It measures
the whole source line, so code in a doc comment counts its indent and
comment marker. It skips output (a fence tagged `text`, and what a
`console` block prints), hidden setup lines, and a line that is only a URL;
an untagged fence counts as code.
- **A line that must stay wider is listed there with its reason.** An entry
that no longer matches a line fails the check, so no stale entry stays.
- **The formatter's width is the hard limit for all other source.** Example
directories set their formatter to 80 where the formatter takes a width.

### Reaching 80

- **Change the code, not the line breaks.** A formatter rejoins any line that
fits its width. Name a sub-expression, use a short example name, hide setup
the reader does not need, or print less.
- **Break at the outermost level when a break is still needed:** after an
opening parenthesis with one argument per line, one call per line in a
chain, one field per line in a literal.
- **Put a comment on its own line above the code it explains.** Never trail
one after code in an example, unless the repository's example runner reads
it there, as with an assertion marker.
- **Break a long string at a word boundary,** never inside a tmux format
(`#{...}`) or an escape sequence; the joined text stays the same.
- **Continue a long command in a `console` block the way its shell does:**
`\` after a `$ ` prompt, a backtick after `PS> `, one flag per continuation
line.

### What never breaks

- **Output a test compares.** Wrapping it changes what the test expects.
- **A block copied from a source file.** Fix the width in the source, then
refresh the copy with the sync command the port block names, or by hand
where it names none; never fix the copy alone.
- **Marker lines and URLs,** which tools and readers take whole.

<!-- /shared:examples -->

### In this repository

- **Hard limit:** palantir-java-format through Spotless, configured in
`build-logic/conventions/src/main/kotlin/libtmux.java-library.gradle.kts`,
which takes no width key, so Java keeps palantir's fixed 120 columns; scalafmt
reads `maxColumn` in `.scalafmt.conf`, set to the example width. Kotlin has
no formatter. The width check is `python3 tools/check_example_width.py`.
- **Not formatted:** Javadoc `<pre>{@code}` blocks, Kotlin sources, every
Markdown fence, the Gradle scripts under `examples/`, and `gradle.properties`
and `run.sh` under `examples/api/`; held to the example width by hand. Java
examples reach it through named variables and extracted helpers, because
palantir rejoins any line that fits its own width.
- **Runs, compiles, exempt:** a fence in `docs/` or a README compiles and
runs against live tmux unless a `<!-- snippet: ... -->` directive on the
line above it says otherwise: `throws` (must fail with that exception),
`does-not-compile` (the compiler must reject it), `compile-only` or
`skip`, each with its reason, as [docs/README.md](../docs/README.md)
lists. Javadoc blocks use the same markers above `<pre>`. `// →` states a
value, and `// Given:` binds a fixture.
- **Compared output and copied blocks:** `// →` lines and the text they
compare stay on one line. No block is copied from a source file, so there
is no sync command; `./gradlew :docs:test` runs the fences and
`./gradlew :examples:test` runs the programs.

Bad, over 80:

```java
Path socket = Path.of(args.length > 0 ? args[0] : "/tmp/libtmux-java-dev/demo/s");
```

Good, hold the default in a constant:

```java
Path socket = Path.of(args.length > 0 ? args[0] : DEMO);
```
23 changes: 23 additions & 0 deletions .github/example-width.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
width = 80
shared_digest = "28b74bae1f7a06731fea5a150cbde4430179b7788b7564ea1644c3951df8531b"
checker_digest = "eb87afceb3fc8f11ef5a478cd38f901ef3950e89ff94fbe0ca281c0cfc1a0e2e"
markdown = ["**/*.md"]
sources = [
"examples/src/**/*.java",
"examples/src/**/*.kt",
"examples/src/**/*.scala",
"examples/**/*.gradle.kts",
"examples/api/gradle.properties",
"examples/api/run.sh",
]
doc_comments = ["*/src/main/**/*.java", "*/src/main/**/*.kt", "*/src/main/**/*.scala"]
allow_ceiling = 1

[[allow]]
path = ".github/WRITING.md"
line = " Path socket = Path.of(args.length > 0 ? args[0] : \"/tmp/libtmux-java-dev/demo/s\");"
reason = "the Bad example in the port block; it shows the width problem the Good example fixes"

[[exclude]]
glob = "examples/api/manifest.json"
reason = "machine-read index of example paths and expected output, which ApiProgramsTest checks against the sources; its long paths cannot be wrapped"
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ jobs:
"$RUNNER_TEMP/workspace-python/bin/python" -m pip install 'tmuxp==1.74.0'
echo "TMUX_WORKSPACE_TEST_PYTHON=$RUNNER_TEMP/workspace-python/bin/python" >> "$GITHUB_ENV"

- name: Examples stay within 80 columns
run: |
python3 tools/check_example_width.py --self-test
python3 tools/check_example_width.py

# Runs every check task instead of stopping at the first failure, so one run names them all.
- run: ./gradlew :libtmux-mcp:test check --stacktrace --continue -Plibtmux.testJdk=${{ matrix.java }}

Expand Down
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ This file routes; it does not restate. Read the policy that governs the change
being made:

- Documentation and user-facing prose — `README.md`, `CHANGELOG.md`, release
notes, commit messages, CLI and help text, API documentation, and source
comments: [`.github/WRITING.md`](.github/WRITING.md)
notes, commit messages, CLI and help text, API documentation, source comments,
and any code example (example programs, Javadoc code, Markdown code blocks):
[`.github/WRITING.md`](.github/WRITING.md), and its
[Examples](.github/WRITING.md#examples) section for code a reader sees
- Contribution workflow, building, testing, and pull requests:
[`.github/CONTRIBUTING.md`](.github/CONTRIBUTING.md)
- Security-sensitive changes: [`SECURITY.md`](SECURITY.md)
Expand Down
33 changes: 22 additions & 11 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ first:
// Given: Server server
CommandResult result = server.cmd("list-sessions", "-F", "#{session_name}");
List<String> lines = result.stdout().stream()
.map(line -> line.endsWith("\r") ? line.substring(0, line.length() - 1) : line)
.map(line -> line.stripTrailing())
.toList();
```

Expand Down Expand Up @@ -204,14 +204,19 @@ final class Recovery {
return switch (failure) {
case TargetGoneException gone -> "look the handle up again";
case ServerUnavailableException down -> "start a server";
case CommandRejectedException refused -> "change the request: " + refused.errorLines();
case DispatchException failed -> failed.safeToRetry() ? "send it again" : "read tmux's state first";
case ControlEndedException ended -> "attach again and take a snapshot";
case UnsupportedFeatureException unsupported -> "do without it: " + unsupported.getMessage();
case UnencodableTextException unencodable -> "start the JVM in a UTF-8 locale";
case MalformedResponseException malformed -> "report it: " + malformed.getMessage();
case CommandRejectedException e ->
"change the request: " + e.errorLines();
case DispatchException e ->
e.safeToRetry() ? "send it again" : "read state first";
case ControlEndedException e -> "attach again and take a snapshot";
case UnsupportedFeatureException e ->
"do without it: " + e.getMessage();
case UnencodableTextException e ->
"start the JVM in a UTF-8 locale";
case MalformedResponseException e -> "report it: " + e.getMessage();
case CardinalityException.NoMatch none -> "nothing matched";
case CardinalityException.MultipleMatches many -> many.atLeast() + " matched";
case CardinalityException.MultipleMatches m ->
m.atLeast() + " matched";
};
}
}
Expand Down Expand Up @@ -324,7 +329,10 @@ io.github.libtmux.snapshot.ServerSnapshot.of(
java.time.Instant.now(),
server.snapshot().serverPid().orElseThrow(),
server.version(),
java.util.List.of(), java.util.List.of(), java.util.List.of(), java.util.List.of());
java.util.List.of(),
java.util.List.of(),
java.util.List.of(),
java.util.List.of());
```

### Key bindings are a view: `server.keys()`
Expand Down Expand Up @@ -386,7 +394,9 @@ or catch it and restore the flag.
```java
// Given: Server server
Channel ready = server.channel("ready");
ready.signal(); // tmux keeps a signal nobody was waiting for, so this wait returns at once
// tmux keeps a signal nobody was waiting for, so the wait below returns
// at once.
ready.signal();
try {
ready.await(java.time.Duration.ofSeconds(30));
} catch (InterruptedException cancelled) {
Expand Down Expand Up @@ -453,7 +463,8 @@ them.
// Given: Pane pane
pane.sendLine("./build --target release");

// Answered by what the build prints, not by the echo of the line that started it.
// Answered by what the build prints, not by the echo of the line that
// started it.
pane.awaitText("release", java.time.Duration.ofSeconds(60));
```

Expand Down
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,10 @@ traversal cannot see a half-changed server.
// Given: Server server
Window editor = server.newSession("demo").newWindow("editor");

List<String> names = server.windows().stream().map(Window::name).sorted().toList();
List<String> names = server.windows().stream()
.map(Window::name)
.sorted()
.toList();

names.contains("editor"); // → true
editor.session().name(); // → demo
Expand All @@ -164,7 +167,7 @@ editors.get(0).name(); // → editor
An expression is a value, so it can also say what it is — which a lambda cannot:

```java
Window_.name().startsWith("edit").describe(); // → window_name starts-with edit
Window_.name().startsWith("edit").describe(); // → window_name starts-with edit
```

### Find one thing
Expand All @@ -184,7 +187,8 @@ Which is how "this session, or a new one" stays a single read:

```java
// Given: Server server
Session work = server.session("work").orElseGet(() -> server.newSession("work"));
Session work = server.session("work")
.orElseGet(() -> server.newSession("work"));

work.name(); // → work
```
Expand All @@ -195,8 +199,9 @@ work.name(); // → work
// Given: Server server
server.newSession("build");

var sessions = server.sessions().stream();
Session build = Selections.exactlyOne(
server.sessions().stream().filter(Session_.name().is("build")).toList());
sessions.filter(Session_.name().is("build")).toList());

build.name(); // → build
```
Expand Down Expand Up @@ -256,7 +261,9 @@ and `TmuxEnvironment` reads them back:

```java
// Given: Path socket
Map<String, String> inside = Map.of("TMUX", socket + ",1,$0", "TMUX_PANE", "%0");
Map<String, String> inside = Map.of(
"TMUX", socket + ",1,$0",
"TMUX_PANE", "%0");

TmuxEnvironment here = TmuxEnvironment.of(inside).orElseThrow();

Expand Down
5 changes: 3 additions & 2 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ are done, so cutting the next release is a tag and someone pressing publish.

`alpha` is the lowest qualifier Maven's comparator knows. Its full order is

```
```text
alpha < beta < milestone < rc < snapshot < (release) < sp
```

Expand Down Expand Up @@ -150,7 +150,8 @@ $ gpg --armor --export-secret-keys <FINGERPRINT> > /tmp/libtmux-signing.asc
| `CENTRAL_PORTAL_PASSWORD` | the token's password half |

```console
$ gh secret set SIGNING_KEY < /tmp/libtmux-signing.asc && rm /tmp/libtmux-signing.asc
$ gh secret set SIGNING_KEY < /tmp/libtmux-signing.asc \
&& rm /tmp/libtmux-signing.asc
```

An unset secret reaches a workflow as an **empty string**, not as nothing, and
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ server.newSession("right");
int windows = 0;
try (ExecutorService threads = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<Integer>> counts = server.sessions().stream()
.map(session -> threads.submit(() -> session.refresh().windows().size()))
.map(s -> threads.submit(() -> s.refresh().windows().size()))
.toList();
for (Future<Integer> count : counts) {
windows += count.get();
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ windows are zoomed".
```java
var busy = Window_.panes().any(Pane_.command().startsWith("nv"));

busy.describe(); // → panes any (pane_current_command starts-with nv)
busy.describe(); // → panes any (pane_current_command starts-with nv)
```

This is the half a lambda cannot do, and the reason the AST is a sealed tree of
Expand Down
16 changes: 11 additions & 5 deletions docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ it with a lambda, or hand it a description you built earlier.

```java
// Given: Server server
Session build = server.newSession(s -> s.named("build").firstWindowNamed("editor"));
Session build = server.newSession(
s -> s.named("build").firstWindowNamed("editor"));
Window logs = build.newWindow(w -> w.named("logs").running("sleep", "30"));

build.name(); // → build
Expand Down Expand Up @@ -111,7 +112,9 @@ older server:
```java
// Given: Server server, Pane pane
if (!server.version().atLeast(new TmuxVersion(3, 7, ""))) {
assertThrows(UnsupportedFeatureException.class, () -> pane.split(s -> s.empty()));
assertThrows(
UnsupportedFeatureException.class,
() -> pane.split(s -> s.empty()));
}
```

Expand Down Expand Up @@ -220,10 +223,12 @@ try (ControlClient client = server.control(session);

client.send("send-keys", "-t", session.name(), "echo streamed", "Enter");

// Output arrives in frames as tmux flushes it, so one line can span several.
// 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());
var frame = output.next(Duration.ofSeconds(5)).orElseThrow();
seen.append(Delivery.kept(frame).data());
}
seen.indexOf("streamed") >= 0; // → true
}
Expand All @@ -244,7 +249,8 @@ can predict. Pin one:

```java
// Given: Path directory
Path tmuxConf = Files.writeString(directory.resolve("tmux.conf"), "set -g base-index 5\n");
Path conf = directory.resolve("tmux.conf");
Path tmuxConf = Files.writeString(conf, "set -g base-index 5\n");

ServerConfig pinned = ServerConfig.builder()
.endpoint(ServerEndpoint.socketPath(directory.resolve("pinned")))
Expand Down
5 changes: 3 additions & 2 deletions docs/guide/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ others.
So the command is framed instead. It is bracketed by two lines that print a random
nonce, and only lines strictly between them are returned:

```
```text
( \trap '/usr/bin/tmux -S /tmp/tmux.sock display-message -p lt3fa9-e:"$?"; /usr/bin/tmux -S /tmp/tmux.sock wait-for -S ch_lt3fa9; \exit 0' 0; /usr/bin/tmux -S /tmp/tmux.sock display-message -p lt3fa9-s; ( \eval 'pytest -q' ) )
```

Expand Down Expand Up @@ -185,7 +185,8 @@ try (ControlClient client = server.control(session);
EventSubscription<ControlEvent> events = client.subscribeEvents(32)) {
client.watch("names", "@*", "#{window_name}");

Delivery<ControlEvent> step = events.next(Duration.ofSeconds(2)).orElseThrow();
Duration wait = Duration.ofSeconds(2);
Delivery<ControlEvent> step = events.next(wait).orElseThrow();
ControlEvent event = Delivery.kept(step);
event.subscription(); // which watch this came from
event.windowId(); // which window, when the watch is over windows
Expand Down
Loading
Loading