Skip to content

Example style guide, 80-column examples, tested Javadoc code - #55

Draft
tony wants to merge 59 commits into
masterfrom
example-style
Draft

tony wants to merge 59 commits into
masterfrom
example-style

Conversation

@tony

@tony tony commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor
  • A stdlib Python check holds examples to 80; palantir-java-format, this repository's formatter, has no width option.
  • The Javadoc <pre>{@code} blocks run in the docs snippet suite (none were compiled).

Merge after libtmux/docs#101 deploys: it makes 80 columns fit a libtmux.org code block from 1024 px up.

What changed

Policy

  • .github/WRITING.md gains an ## Examples section: the shared text plus an "In this repository" block (palantir 120 as the hard limit, Scala 80, Kotlin unformatted, the <!-- snippet: ... --> directives, // → lines never break).
  • AGENTS.md routes code examples to it through "Additional guidance" and links the #examples heading.

Tooling

  • tools/check_example_width.py and .github/example-width.toml read every **/*.md page, the example sources (Java, Kotlin, Scala, *.gradle.kts, examples/api/gradle.properties and run.sh), and <pre>{@code} blocks in library sources. One [[exclude]] with a reason: examples/api/manifest.json, a machine-read index whose long paths cannot wrap. One [[allow]] entry: the deliberately wide Bad line in WRITING.md's port block.
  • CI runs the self-test and the check before the Gradle check.
  • Documentation.javaSources and snippetsInJavadoc extract each <pre>{@code} block from */src/main/java and feed DocumentationSnippetsTest. A guard test counts every opening against the blocks taken. 14 blocks run, 4 compile only and 9 skip with a stated reason.

Examples

  • Typical changes extract a run() helper, name a callback or constant, and break a chain at the outermost call.

Evidence

  • Spotless 8.9.0 runs palantir-java-format 2.97.0, a 120-column formatter (README) whose Spotless config takes only a style and formatJavadoc (JavaExtension). Javadoc is left alone because DEFAULT_FORMAT_JAVADOC = false. Kotlin has no formatter and Markdown fences are never formatted.
  • Run against origin/master, the checker and config report 276 lines over 80 in 66 files; the commits fix all of them.
  • Rejected Checkstyle LineLength, which cannot read Markdown fences or Javadoc blocks.
  • Javadoc snippets: planting pane.runn( in the Pane.await Javadoc made :docs:test fail at Pane.java:474 with a compile error; the clean tree passes. Rejected JEP 413 {@snippet}: javadoc compiles no snippet; an external one, found through --snippet-path, is a source file the build must compile separately, which moves every example out of its Javadoc.

How to verify

$ python3 tools/check_example_width.py --self-test
$ python3 tools/check_example_width.py
$ ./gradlew :docs:test
$ ./gradlew spotlessCheck

Not in this PR

  • Javadoc prose in library source is not wrapped at 80 (example programs rewrap theirs); the shared rule covers code in doc comments and comments inside examples. The 120-column palantir limit still governs library source.

Related

@tony
tony force-pushed the example-style branch 26 times, most recently from bea7809 to b0f3b89 Compare October 6, 2026 03:05
tony added 4 commits October 10, 2026 06:18
why: 12 lines ran past 80 columns, the widest at 114: the exit-report
switch, the Javadoc prose and the run signature. A formatter rejoins any
line that fits 120, so only a change of shape keeps them short.

what:
- Move the outcome switch into a private report(ran) helper so its
  cases sit at one indent level
- Name the joined output lines once instead of rebuilding them in two
  cases
- Make run package-private, which the suite in the same package still
  calls, so its signature and throws clause fit on one line
- Hold the socket default in DEMO and describe the parameters in
  @PARAM tags
- Shorten the timed-out and server-gone messages and rewrap comments at
  80
why: 17 lines ran past 80 columns, the widest at 103. Four nested try
levels put the subscription, the deadline timer and the read loop deep
enough that no line break could hold them, since the formatter rejoins
what fits 120.

what:
- Split run into watch, closeAfter and collect so each body sits at one
  or two levels
- Name the target session, the timer executor and the control-sequence
  pattern
- Bind the subscription with var and import NANOSECONDS statically
- Rewrap Javadoc prose and comments at 80
why: 12 lines ran past 80 columns, the widest at 109. The read loop sat
five levels deep, which pushed the event wait and the Delivery unwrap
out of range, and the formatter rejoins any break that fits 120.

what:
- Split run into watch and collect, and name the remaining time as left
- Name the watched window once in NEW_WINDOW and test events through a
  named helper
- Add an own-line comment on the callback parameter so the signature
  breaks at the outermost level
- Rewrap Javadoc prose and comments at 80
why: 8 lines ran past 80 columns, the widest at 101. The session lookup
and the window builder calls sat inside two nested blocks, and the
formatter rejoins any break that fits 120.

what:
- Lift the lookup into sessionNamed and the layout steps into layOut
- Use a short lambda parameter for the window spec
- Name the window count before building the report string
- Rewrap Javadoc prose and comments at 80
tony added 25 commits October 11, 2026 18:46
why: The Kotlin README fences ran to 98 columns on the DispatchException
branch and the control.output callback.

what:
- Break the DispatchException branch after the arrow
- Name the target session before the control.output callback
why: The MCP README printed the run result as one JSON line 84 columns
wide. The pretty-printed form has the same fields, one per line.

what:
- Pretty-print the run result JSON, one field per line
why: One session-renamed filter in the Cats README ran to 90 columns.

what:
- Break the session-renamed filter at the && operator
why: The Workspace README ran to 100 columns on an inline YAML document
and the invalid-layout text.

what:
- Name the YAML text before parsing it
- Break the invalid-layout YAML at a word boundary
why: The library README ran to 94 columns on the endpoint comment and
the session lookup.

what:
- Move the endpoint comment above its line
- Name the session stream before the exactlyOne filter
why: The await example ran to 89 columns.

what:
- Name the five-second limit and shorten the predicate parameter
why: The expand example ran to 83 columns on its second format.

what:
- Bind the second format to a variable and keep #{pane_index}, so the
  result still names a pane; the call then fits in 80 columns
why: The newSession example ran to 95 columns with the Dimensions value
inline.

what:
- In Server.java, name the Dimensions value before the call
why: The paneFields example ran to 99 columns with the field list
inline.

what:
- Name the field list before the call
why: The within example ran to 92 columns reading the first pane through
the scoped server.

what:
- Name the scoped server before reading its first pane
why: The newWindow example ran to 89 columns on the journalctl -f
command.

what:
- Run top instead of journalctl -f, which keeps the window just as long
- Name the window top and bind it to top, so the name, the variable and
  the command agree
why: The class example ran to 85 columns on the journalctl -f command.

what:
- Run top instead of journalctl -f
why: The class example ran to 91 columns with the Dimensions value
inline.

what:
- In SessionSpec.java, name the Dimensions value before the call
why: The stream example ran to 88 columns on a long lambda parameter.

what:
- Shorten the lambda parameter
why: The switch example ran to 112 columns on its record-pattern
bindings.

what:
- Shorten the record pattern bindings and break after the arrow
why: The history-limit example ran to 88 columns on a declaration.

what:
- Drop the kept binding; the // → assertion reads the expression value,
  which a declaration would hide
why: The capture assertion ran to 89 columns.

what:
- Show a shorter build line so the // → assertion fits
why: The pgrep loop in the reaping steps ran to 85 columns.

what:
- Continue the one-line for loop over three lines with backslashes, so
  the block still pastes and runs as one command
why: The gh secret set line ran to 85 columns. The version order block
had no language tag, so a reader could not tell output from a command.

what:
- Continue gh secret set before && with a backslash
- Tag the version order block text
why: The sbtw command ran to 101 columns.

what:
- Continue the command before the task list with a backslash, keeping
  one pasteable command
why: The module build file is one a reader opens to see how the examples
are wired. Its comments ran to 108 columns, and a Gradle file has no
formatter to wrap them.

what:
- Wrap the header comment and the libtmux-mcp comment at 80 columns
- Drop the dash that the wrap would have stranded at a line end
why: The Cats example's build file copied into a reader's project held
one dependency line at 84 columns. The -SNAPSHOT version is part of the
coordinate and cannot be dropped.

what:
- Put the libtmux-scala-cats coordinate on its own line inside
  implementation( ), unchanged
why: The launcher a reader runs held one tmux call at 81 columns. A
shell script has no formatter to break it.

what:
- Continue the first new-session call after its -f config flag with a
  backslash, leaving the command unchanged
why: The manifest test and the launcher it drives held 44 lines over 80
columns. palantir-java-format joins a wrapped call back onto one line
whenever it fits 120, so breaking lines by hand did not survive
spotlessApply. Named pieces stay short.

what:
- ApiProgramsTest: lift the program names, source walk and setup files
  into locals and helpers, and give the parameterized case a named
  helper
- MainsTest: put the JVM command in a Launch record, split the timeout
  helpers and process shutdown into methods, and name the fixture
  socket check
- Leave every assertion, expected string and deadline as it was
- ApiProgramsTest calls MainsTest, so both files change together
why: The suite that runs every example held 27 lines over 80 columns.
palantir-java-format joins a wrapped call back onto one line whenever
it fits 120, so the fix is shorter names.

what:
- Wrap the class comment and two inline comments at 80 columns
- Move the pane command prefix test and the exit status call into
  helpers that turn InterruptedException into an unchecked failure, so
  test signatures fit one line
- Name the socket path, deadline and failure message in locals
- Rename three tests to shorter names; assertions are unchanged
tony added 4 commits October 11, 2026 21:08
why: Example code had no written rule, so each author chose a width and
a break style, and a width check would have had no policy to cite. The
section is the shared text, plus how this repository meets it: Spotless
runs palantir-java-format for Java and scalafmt for Scala, Kotlin has no
formatter, and Spotless leaves Javadoc alone.

what:
- Add ## Examples after ## Code blocks with Width, Reaching 80 and What
  never breaks, byte-identical to the shared section in libtmux/docs#101
- Add ### In this repository in the shared four-bullet shape: the hard
  limit and its config, the surfaces no formatter touches, the throws,
  does-not-compile, compile-only and skip directives, and the compared
  output with the test commands
- Point at the width config and formatter keys, not their values, so the
  text cannot drift from them
- Show a Bad and Good pair from RunACommand.java: the 90 column socket
  default against the same line with the default held in a constant
why: An agent changing an example program, Javadoc code or a Markdown
fence would not open WRITING.md from the existing prose-only trigger,
and so would miss the width rule.

what:
- Extend the WRITING.md routing line with any code example (example
  programs, Javadoc code, Markdown code blocks) and link the Examples
  section
why: Java examples have no enforced width. Spotless 8.9.0 runs
palantir-java-format 2.97.0, a 120 column formatter whose Spotless
config takes only a style and formatJavadoc, so it cannot be told to
wrap at 80; Spotless leaves Javadoc alone because DEFAULT_FORMAT_JAVADOC
is false; Kotlin has no formatter; Markdown fences are never formatted.
libtmux.org renders code in a box that scrolls sideways, so 80 columns
is the target a reader sees. Run against origin/master, this checker and
config report 276 lines over 80 in 66 files, across Markdown pages,
example programs, example tests and build files, and Javadoc; the
example commits fix all of them. A libtmux.org code block holds 78
columns at 1024 px (Firefox, WebKit, CI's Chrome) and 99 at 1440 px
(Firefox, WebKit); libtmux/docs#101 sizes the code font so 80 columns
fit from 1024 px up.

Sources:
120 column formatter:
https://github.com/palantir/palantir-java-format/blob/2.97.0/README.md
PalantirJavaFormatConfig: style, formatJavadoc:
https://github.com/diffplug/spotless/blob/gradle/8.9.0/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java#L284-L308
DEFAULT_FORMAT_JAVADOC = false:
https://github.com/diffplug/spotless/blob/gradle/8.9.0/lib/src/main/java/com/diffplug/spotless/java/PalantirJavaFormatStep.java#L33

Rejected: a formatter width, because palantir has none and Kotlin has no
formatter; Checkstyle LineLength, because it cannot read Markdown fences
or Javadoc blocks; and a Gradle task, because the checker is one script
shared across ports.

what:
- Add tools/check_example_width.py, the shared checker, with a
  --self-test that proves it can fail
- .github/example-width.toml checks every Markdown file (**/*.md), every
  Java, Kotlin and Scala file under examples/src (programs and their
  tests), the Gradle build files, gradle.properties and run.sh under
  examples/, and <pre>{@code} blocks in library sources
- Exclude only examples/api/manifest.json, a machine-read index of
  example paths and expected output that ApiProgramsTest checks against
  the sources; its paths cannot be wrapped
- List one [[allow]] entry, the wide Bad example in WRITING.md, with
  allow_ceiling set to the entry count
- Record checker_digest, which must equal --digest, so an edited
  checker fails until every port changes together
- Record shared_digest, the --digest of WRITING.md's shared section, so
  a local edit to it fails the check
- Run the self-test and the check in CI before the Gradle check; python3
  is on the runner
- Document the command beside the other gates in CONTRIBUTING.md
why: A Javadoc <pre>{@code} block is code a reader copies, but nothing
compiled it. Spotless leaves Javadoc alone (DEFAULT_FORMAT_JAVADOC =
false) and a snippet that stops compiling stays published until a reader
reports it. The docs module already compiles and runs every Markdown
fence through DocumentationSnippetsTest, so the library sources join
that suite instead of a second one. Planting pane.runn( in the
Pane.await Javadoc made :docs:test fail at Pane.java:474 with a compile
error, and the unplanted tree passes. Dropping one block from the
extractor's result made the count test fail, and restoring it made it
pass.

Rejected:
- JEP 413 `{@snippet}`: javadoc compiles none, and an external snippet,
  found through --snippet-path, is a separate source file, so every
  example would leave its Javadoc
- Twin example files: a copy drifts from the Javadoc it mirrors
- A new Gradle sourceSet: the docs module already owns the snippet
  harness and its tmux fixture

Sources: DEFAULT_FORMAT_JAVADOC = false:
https://github.com/diffplug/spotless/blob/gradle/8.9.0/lib/src/main/java/com/diffplug/spotless/java/PalantirJavaFormatStep.java#L33
JEP 413 {@snippet} (includes snippets; does not compile them):
https://openjdk.org/jeps/413

what:
- Documentation.javaSources and snippetsInJavadoc extract each
  <pre>{@code ...}</pre> block from */src/main/java, reuse Snippet,
  SnippetCompiler and the // Given: fixtures, and feed
  DocumentationSnippetsTest
- A guard test counts every <pre>{@code opening against the blocks
  taken, so a block with a malformed close fails instead of going
  unchecked
- The same test sums the openings across all sources, requires the sum
  to be above zero so a broken source scan fails, and requires
  javadocSnippets() to hold exactly that many entries, so no floor
  goes stale
- Directives reuse the Markdown syntax on the line above <pre>:
  <!-- snippet: skip: reason --> or compile-only
- A snippet the library refuses with UnsupportedFeatureException on the
  lane's tmux is skipped there with that reason and runs on the lanes
  whose tmux has the feature: on tmux 3.2a the Server and SessionSpec
  examples (a detached session's size needs tmux 3.3) are skipped, and
  on tmux 3.7c none are
- Add // Given: lines and directives to the existing Javadoc blocks, and
  skip the shell command lines in the example program headers
- Say in WRITING.md's port block that Javadoc blocks take the same
  markers above <pre>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant