Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 19 additions & 2 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ Run a focused inner suite:
$ mise exec -- python scripts/check.py unit query
```

Run unit suites, Lua51 formatting, lint, generated-field drift, links and
whitespace checks:
Run unit suites, Lua51 formatting, lint, generated-field drift, links,
whitespace and example-width checks:

```console
$ mise exec -- python scripts/check.py mid
Expand All @@ -120,6 +120,15 @@ artifact builds/imports, LuaLS diagnostics and actual editor completion:
$ mise exec -- python scripts/check.py outer
```

The outer gate also runs `scripts/check_doc_fences.py`: every `lua` fence in
the README and `docs/` runs or parses according to its `<!-- lua: ... -->`
directive, against a private owned tmux server selected by `TMUX_BIN`. Run it
alone with:

```console
$ mise exec -- python scripts/check.py fences
```

Integration includes real luv and Neovim loop ownership, process cancellation,
post-exit pipe draining and metadata byte round-trips. These use the selected
Lua interpreter and its own luv ABI; Neovim uses its embedded runtime.
Expand Down Expand Up @@ -177,6 +186,14 @@ generation and drift checks are offline:
$ mise exec -- python scripts/generate_options.py
```

Examples stay within 80 columns. The width check reads
[example-width.toml](example-width.toml), proves it can fail, then scans the
files it names. The width gate runs both steps, and the mid gate runs it:

```console
$ mise exec -- python scripts/check.py width
```

Check whitespace in unstaged changes:

```console
Expand Down
98 changes: 98 additions & 0 deletions .github/WRITING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ lists and code blocks. Wrap repository prose at 80 columns, except for URLs
and other tokens that cannot be split. Do not hard-wrap issue or pull-request
body paragraphs.

### Code blocks

A shell code block is one paste-and-run action:

- Put one command in each block. An explicit `&&`, `;`, or `\` continuation
Expand All @@ -72,6 +74,102 @@ $ git log \
--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:** StyLua with Lua51 syntax, configured by `stylua.toml` and
its `column_width` key; `examples/stylua.toml` sets 80 for the example
directories, and `stylua --syntax Lua51 examples` applies it; run it again
until `stylua --check --syntax Lua51 examples` passes, since StyLua 2.4.0
rewrites its own output once for a method chain inside a call argument.
The width check is `python3 scripts/check_example_width.py`,
run by the `mid` gate. It reads `.github/example-width.toml` and checks
that `examples/stylua.toml` differs from the root config only in
`column_width`.
- **Not formatted:** StyLua does not wrap comments and never reads Markdown.
Hold comments, the fences in every Markdown file, and
`examples/api/run.sh` to the example width by hand; the width check fails on a
wider line.
- **Runs, compiles, exempt:** a program listed in `examples/api/manifest.json`
runs through `scripts/api_examples.py` and its output is compared.
Programs the package gate lists run there. Every `lua` fence in the README
and `docs/` carries `<!-- lua: run | fragment | compile-only: reason -->`
above it and an unmarked fence fails. `run` is a whole program,
`fragment` runs inside a live server with `server`, `session`, `window`,
`pane`, `snapshot` and `must` bound, and `compile-only` states why it
cannot run.
- **Compared output and copied blocks:** never wrap the `stdout` in
`manifest.json` or a line a test matches. The README snapshot fence copies
`examples/snapshot.lua` and the quickstart region between `docs:begin` and
`docs:end` is quoted by the documentation site. Neither has a sync tool;
edit the source file, then copy it.

Bad, over 80:

```lua
local split = must(editor.pane:split({ direction = "right", percent = 40 }):await())
```

Good, a named option table:

```lua
local right = { direction = "right", percent = 40 }
local split = must(editor.pane:split(right):await())
```

## Commit messages

Use `Scope(type[detail]): Description`. The detail is optional. Name the
Expand Down
24 changes: 24 additions & 0 deletions .github/example-width.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
width = 80
markdown = ["**/*.md"]
sources = ["examples/**/*.lua", "examples/api/run.sh"]
allow_ceiling = 1
shared_digest = "28b74bae1f7a06731fea5a150cbde4430179b7788b7564ea1644c3951df8531b"
checker_digest = "eb87afceb3fc8f11ef5a478cd38f901ef3950e89ff94fbe0ca281c0cfc1a0e2e"

[[allow]]
path = ".github/WRITING.md"
line = 'local split = must(editor.pane:split({ direction = "right", percent = 40 }):await())'
reason = "the Bad example in the port block; it shows the width problem the Good example fixes"

[[exclude]]
glob = "examples/stylua.toml"
reason = "a copy of stylua.toml that [[formatter_copies]] already compares"

[[exclude]]
glob = "examples/api/manifest.json"
reason = "a machine-read index; its description and stdout values are JSON strings that cannot wrap, and tests compare them"

[[formatter_copies]]
copy = "examples/stylua.toml"
root = "stylua.toml"
key = "column_width"
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ requested work. Prefer `rg`, `ag`, and `fd` for discovery.

- Setup, validation, tmux isolation, and pull requests:
[.github/CONTRIBUTING.md](.github/CONTRIBUTING.md).
- Documentation, user-facing text, comments, and commit messages:
[.github/WRITING.md](.github/WRITING.md).
- Documentation, user-facing text, comments, commit messages, and any code
example (example programs, Markdown code blocks):
[.github/WRITING.md](.github/WRITING.md), and its
[Examples](.github/WRITING.md#examples) section for code a reader sees.

Each guide is the single home for its subject. `CLAUDE.md` is a relative
symlink to this file so both entry points use the same instructions.
Expand Down
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ for exact versions and remaining platform coverage.
Connect to an existing server by its explicit socket path. This is the full
[snapshot example](examples/snapshot.lua), which prints pane and window IDs:

<!-- lua: run -->
```lua
local adapter = require("libtmux.runtime.luv")

Expand All @@ -77,13 +78,14 @@ local function must(value, err)
return value
end

local binary =
assert(os.getenv("TMUX_BIN"), "set TMUX_BIN to an absolute tmux executable")
local socket =
assert(os.getenv("TMUX_SOCKET"), "set TMUX_SOCKET to an explicit socket")

must(adapter.run(function(runtime)
local server = must(runtime
:connect({
binary = assert(os.getenv("TMUX_BIN"), "set TMUX_BIN to an absolute tmux executable"),
socket_path = assert(os.getenv("TMUX_SOCKET"), "set TMUX_SOCKET to an explicit socket"),
})
:await())
local server =
must(runtime:connect({ binary = binary, socket_path = socket }):await())
local snapshot = must(server:snapshot({ strict = true }):await())
for _, pane in ipairs(snapshot.panes) do
io.stdout:write(pane.id, "\t", pane.window_id, "\n")
Expand Down Expand Up @@ -134,6 +136,7 @@ SH
After capturing `snapshot` in the example above, filter its panes with
structured criteria or an ordinary Lua function:

<!-- lua: fragment -->
```lua
local editors = snapshot.panes:where({
current_command = { one_of = { "nvim", "vim" } },
Expand Down Expand Up @@ -165,10 +168,12 @@ $ lua examples/native_query.lua
Before closing `server` in that runtime body, create a session and split a
window. These calls use the same `must` helper:

<!-- lua: fragment -->
```lua
local work = must(server:new_session({ name = "work" }):await())
local editor = must(work.session:new_window({ name = "editor" }):await())
local split = must(editor.pane:split({ direction = "right", percent = 40 }):await())
local right = { direction = "right", percent = 40 }
local split = must(editor.pane:split(right):await())

must(split.pane:send_text("printf '%s\\n' hello"):await())
must(split.pane:send_keys({ "Enter" }):await())
Expand All @@ -191,6 +196,7 @@ $ nvim --cmd 'set runtimepath+=.'
With `TMUX_SOCKET` set to an existing server's absolute socket path, run this
Lua code. `start` uses the editor's loop and reports the result in a callback:

<!-- lua: compile-only: Neovim runtime, absent from the plain Lua gate -->
```lua
local adapter = require("libtmux.runtime.nvim")

Expand Down
1 change: 1 addition & 0 deletions docs/buffers.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Buffers hold bytes in the tmux daemon. Use an explicit name for every read,
write, deletion and paste; these methods never infer the most recent buffer.
Each live operation returns a Request through the PROCESS endpoint.

<!-- lua: fragment -->
```lua
assert(server:set_buffer("build-output", "one\000two\n"):await())
local content = assert(server:show_buffer("build-output"):await())
Expand Down
1 change: 1 addition & 0 deletions docs/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ the snapshot. Client records do not prove attachment continuity.
Inside a [runtime task](runtime.md), with a connected `server`, choose the
client and destination session by their observed names:

<!-- lua: compile-only: needs an attached client -->
```lua
local snapshot = assert(server:snapshot():await())
local observed = snapshot.clients:where({ name = "/dev/pts/7" }):one()
Expand Down
5 changes: 4 additions & 1 deletion docs/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Server methods address tmux's global environment store. Session methods address
that session's local store. Each method returns a Request; reads never evaluate
shell text. Window and Pane handles reject these operations with `invalid_scope`.

<!-- lua: compile-only: hard-codes /usr/bin/tmux and a socket path -->
```lua
local adapter = require("libtmux.runtime.luv")
local result, err = adapter.run(function(runtime)
Expand All @@ -18,7 +19,9 @@ local result, err = adapter.run(function(runtime)
assert(session:set_environment("APP_MODE", "local"):await())
local local_value = assert(session:get_environment("APP_MODE"):await())
assert(session:unset_environment("APP_MODE"):await())
local inherited = assert(session:get_environment("APP_MODE", { inherit = true }):await())
local inherit = { inherit = true }
local request = session:get_environment("APP_MODE", inherit)
local inherited = assert(request:await())
return { local_value = local_value, inherited = inherited }
end)
assert(result, tostring(err))
Expand Down
2 changes: 2 additions & 0 deletions docs/panes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ PROCESS endpoint. Each method returns a Request; await it inside a managed
runtime task or use its completion callback. Snapshot records remain plain
captured data. Obtain a handle from a creation receipt or `server:handle`.

<!-- lua: fragment -->
```lua
local capture = assert(pane:capture({ history_lines = 20 }):await())
local text = assert(capture:text())
Expand Down Expand Up @@ -130,6 +131,7 @@ The initial action subset includes cursor/word/paragraph/page/history
navigation, selection marking, rectangle modes, refresh, search and jumps.
For example:

<!-- lua: fragment -->
```lua
assert(pane:copy_mode():await())
assert(pane:copy_command("search-forward-text", { "ready" }):await())
Expand Down
4 changes: 3 additions & 1 deletion docs/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,13 @@ ordinary JSON `decode` function cannot preserve evidence of duplicate keys.
Core imports and ordinary queries require only Lua. Codec functions are
trusted synchronous code; the library does not load a codec automatically.

<!-- lua: fragment -->
```lua
local query = require("libtmux.query")
local json = require("lunajson")
local schema = { fields = { active = { type = "boolean" } } }
local text = assert(query.encode_json(schema, { active = false, AND = {} }, json))
local input = { active = false, AND = {} }
local text = assert(query.encode_json(schema, input, json))
local criteria = assert(query.decode_json(schema, text, json))
```

Expand Down
4 changes: 3 additions & 1 deletion docs/snapshots.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ Cancellation retires owned clients; it does not kill the daemon.
Run the example against an explicitly selected existing server:

```console
$ TMUX_BIN=/usr/bin/tmux TMUX_SOCKET=/tmp/example-tmux.sock lua examples/snapshot.lua
$ TMUX_BIN=/usr/bin/tmux \
TMUX_SOCKET=/tmp/example-tmux.sock \
lua examples/snapshot.lua
```

The package gate runs this file from installed core and luv outside the
Expand Down
1 change: 1 addition & 0 deletions docs/topology.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ the PROCESS endpoint. Each operation returns a Request that resolves to
explicitly to inspect the resulting state; existing records do not change
in place.

<!-- lua: fragment -->
```lua
assert(session:rename("build"):await())
assert(window:resize({ width = 120, height = 40 }):await())
Expand Down
19 changes: 13 additions & 6 deletions examples/api/capture.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,33 @@ local function must(value, err)
return value
end

local binary = assert(os.getenv("TMUX_BIN"), "set TMUX_BIN to an absolute tmux executable")
local socket = assert(os.getenv("TMUX_SOCKET"), "set TMUX_SOCKET to the private socket")
local binary =
assert(os.getenv("TMUX_BIN"), "set TMUX_BIN to an absolute tmux executable")
local socket =
assert(os.getenv("TMUX_SOCKET"), "set TMUX_SOCKET to the private socket")

must(adapter.run(function(runtime)
local server = must(runtime:connect({ binary = binary, socket_path = socket }):await())
local created = must(server:new_session({ name = "demo", argv = { "/bin/sh" } }):await())
local server =
must(runtime:connect({ binary = binary, socket_path = socket }):await())
local created = must(
server:new_session({ name = "demo", argv = { "/bin/sh" } }):await()
)
local pane = created.pane
local function quote(text)
return "'" .. text:gsub("'", "'\\''") .. "'"
end

-- Signal completion on this socket; do not guess when the shell has printed.
-- Signal completion on this socket; do not guess when the shell
-- has printed.
local command = "printf '\\nlua capture ready\\n'; "
.. quote(binary)
.. " -S "
.. quote(socket)
.. " wait-for -S example-ready"
must(pane:send_text(command):await())
must(pane:send_keys({ "Enter" }):await())
must(server:command({ "wait-for", "example-ready" }, { timeout = 1000 }):await())
local wait = { "wait-for", "example-ready" }
must(server:command(wait, { timeout = 1000 }):await())
local capture = must(pane:capture({ history_lines = 20 }):await())
local found = false
for line in must(capture:text()):gmatch("[^\r\n]+") do
Expand Down
14 changes: 10 additions & 4 deletions examples/api/connect.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ local function must(value, err)
return value
end

local binary = assert(os.getenv("TMUX_BIN"), "set TMUX_BIN to an absolute tmux executable")
local socket = assert(os.getenv("TMUX_SOCKET"), "set TMUX_SOCKET to the private socket")
local binary =
assert(os.getenv("TMUX_BIN"), "set TMUX_BIN to an absolute tmux executable")
local socket =
assert(os.getenv("TMUX_SOCKET"), "set TMUX_SOCKET to the private socket")

must(adapter.run(function(runtime)
local server = must(runtime:connect({ binary = binary, socket_path = socket }):await())
local server =
must(runtime:connect({ binary = binary, socket_path = socket }):await())
local snapshot = must(server:snapshot({ strict = true }):await())
assert(#snapshot.sessions:where({ name = "bootstrap" }) == 1, "bootstrap session is missing")
assert(
#snapshot.sessions:where({ name = "bootstrap" }) == 1,
"bootstrap session is missing"
)
print("connected")

must(server:close():await())
Expand Down
Loading
Loading