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
10 changes: 5 additions & 5 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ answerable for how the whole library is tested.

## Building

You need JDK 25 or newer and tmux on `PATH`. Nothing else — Gradle provisions
the toolchain, and the library has no runtime dependencies. The workspace CLI
adds its own runtime dependencies; its terminal regression tests also require
Python 3 for the standard-library PTY driver and Bash on `PATH` for interactive
completion tests. On macOS, install Bash with Homebrew; the system Bash lacks
You need JDK 25 or newer and tmux on `PATH`. Gradle provisions the toolchain,
and the library has no runtime dependencies. Native terminal test fixtures need
Python 3 for its standard-library PTY driver. The workspace CLI adds its own
runtime dependencies; its interactive completion tests also need Bash on
`PATH`. On macOS, install Bash with Homebrew; the system Bash lacks
the completion test's Readline hooks.

`./gradlew` is the only supported entry point. A locally installed `gradle` is
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@ jobs:
"$RUNNER_TEMP/workspace-python/bin/python" -m pip install 'tmuxp==1.74.0'
git init --quiet "$RUNNER_TEMP/python-libtmux-core"
git -C "$RUNNER_TEMP/python-libtmux-core" fetch --quiet --depth=1 \
https://github.com/tmux-python/libtmux.git c27dd929fc25c3a95648759d8c0537a07ef60258
https://github.com/tmux-python/libtmux.git 5df9cb3e89647577b7dd8484f844ffe1053129bb
git -C "$RUNNER_TEMP/python-libtmux-core" checkout --quiet --detach \
c27dd929fc25c3a95648759d8c0537a07ef60258
5df9cb3e89647577b7dd8484f844ffe1053129bb
PYTHONPATH="$RUNNER_TEMP/python-libtmux-core/src" "$RUNNER_TEMP/workspace-python/bin/python" -c \
'import sys, libtmux; from pathlib import Path; from libtmux.common import TMUX_MIN_VERSION; assert TMUX_MIN_VERSION == "3.3a", TMUX_MIN_VERSION; assert Path(libtmux.__file__).resolve() == Path(sys.argv[1]).resolve() / "libtmux" / "__init__.py", libtmux.__file__; print(TMUX_MIN_VERSION, libtmux.__file__)' \
'import sys, libtmux; from pathlib import Path; from libtmux.common import TMUX_MIN_VERSION, TMUX_OPTIONAL_FEATURE_POLICY_VERSION as policy; assert type(policy) is int and policy >= 2, policy; assert TMUX_MIN_VERSION == "3.3a", TMUX_MIN_VERSION; assert Path(libtmux.__file__).resolve() == Path(sys.argv[1]).resolve() / "libtmux" / "__init__.py", libtmux.__file__; print(TMUX_MIN_VERSION, policy, libtmux.__file__)' \
"$RUNNER_TEMP/python-libtmux-core/src"
echo "PYTHONPATH=$RUNNER_TEMP/python-libtmux-core/src" >> "$GITHUB_ENV"
echo "TMUX_WORKSPACE_TEST_PYTHON=$RUNNER_TEMP/workspace-python/bin/python" >> "$GITHUB_ENV"
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/tmux-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,11 @@ jobs:
"$RUNNER_TEMP/workspace-python/bin/python" -m pip install 'tmuxp==1.74.0'
git init --quiet "$RUNNER_TEMP/python-libtmux-core"
git -C "$RUNNER_TEMP/python-libtmux-core" fetch --quiet --depth=1 \
https://github.com/tmux-python/libtmux.git c27dd929fc25c3a95648759d8c0537a07ef60258
https://github.com/tmux-python/libtmux.git 5df9cb3e89647577b7dd8484f844ffe1053129bb
git -C "$RUNNER_TEMP/python-libtmux-core" checkout --quiet --detach \
c27dd929fc25c3a95648759d8c0537a07ef60258
5df9cb3e89647577b7dd8484f844ffe1053129bb
PYTHONPATH="$RUNNER_TEMP/python-libtmux-core/src" "$RUNNER_TEMP/workspace-python/bin/python" -c \
'import sys, libtmux; from pathlib import Path; from libtmux.common import TMUX_MIN_VERSION; assert TMUX_MIN_VERSION == "3.3a", TMUX_MIN_VERSION; assert Path(libtmux.__file__).resolve() == Path(sys.argv[1]).resolve() / "libtmux" / "__init__.py", libtmux.__file__; print(TMUX_MIN_VERSION, libtmux.__file__)' \
'import sys, libtmux; from pathlib import Path; from libtmux.common import TMUX_MIN_VERSION, TMUX_OPTIONAL_FEATURE_POLICY_VERSION as policy; assert type(policy) is int and policy >= 2, policy; assert TMUX_MIN_VERSION == "3.3a", TMUX_MIN_VERSION; assert Path(libtmux.__file__).resolve() == Path(sys.argv[1]).resolve() / "libtmux" / "__init__.py", libtmux.__file__; print(TMUX_MIN_VERSION, policy, libtmux.__file__)' \
"$RUNNER_TEMP/python-libtmux-core/src"
echo "PYTHONPATH=$RUNNER_TEMP/python-libtmux-core/src" >> "$GITHUB_ENV"
echo "TMUX_WORKSPACE_TEST_PYTHON=$RUNNER_TEMP/workspace-python/bin/python" >> "$GITHUB_ENV"
Expand Down
55 changes: 55 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,61 @@ production.

## Unreleased

### Added

- **Capabilities are recorded in one registry and a `capabilities.json`
manifest.** The registry names each optional tmux feature and release defect
the library gates, with the release it needs and how it refuses or repairs.
The gates read it, and the manifest at the repository root is written from it
for the capability pages on libtmux.org. (#72)
- **`TextOutcome.PANE_GONE`, `TextWait` and `Pane.watchText`.** `watchText`
waits like `Pane.awaitText` and also returns the pane's output at its last
read. (#72)

### Changed

- **Managed control now requires tmux 3.7+ and a pinned socket.** Older
daemons are refused before control starts; ordinary operations keep the 3.3a
minimum. See [migration notes](MIGRATION.md#optional-tmux-requirements). (#72)
- **Named buffer deletion, rename and managed paste require tmux 3.4+.** On
3.3a a missing name can select an unrelated buffer. (#72)
- **Invalid literal layouts are refused on tmux 3.3a.** The refusal is an
`UnsupportedFeatureException` before any command of the batch runs. (#72)
- **Literal `split-window -p` is refused on exact tmux 3.4.** Typed percentage
splits keep working. (#72)
- **`refresh-client -l` is refused below tmux 3.6a.** On 3.3a it ends the server
when a client attached from an `xterm` or `tmux-256color` terminal receives
the query, and on 3.4 to 3.6 it ends the server when the client leaves before
its terminal answers. (#72)
- **Raw and unnamed `break-pane` is refused on exact tmux 3.7.** Named breaks
retain the requested name, and single-pane breaks also work without native
`-P` output. (#72)
- **Exact tmux 3.7 refuses `.` and `:` in session and window names before
dispatch.** A batch or workspace no longer runs the commands ahead of the
one tmux would reject. (#72)
- **Python extensions require optional-policy v2.** `tmux-workspace shell` and
extension loading need a Python libtmux with
`TMUX_OPTIONAL_FEATURE_POLICY_VERSION >= 2`. No released libtmux has it yet;
it comes with tmux-python/libtmux#780, so until then install libtmux from
that source or leave `shell` and extensions alone. (#72)
- **`Pane.awaitText` answers `TextOutcome.PANE_GONE` for a pane killed
mid-wait** instead of throwing `CommandRejectedException`. (#72)
- **`Server.cmd`, `batch`, `chain` and `ControlClient.send` say that nested
command strings are not checked.** An `if-shell` body, a hook or a
`source-file` keeps tmux's own behaviour, so do not nest a command the
library refuses. (#72)

### Fixed

- **`wait_for_text` and `run_shell_command` answer for a gone pane or server.**
A pane killed mid-wait ends the call as `PANE_GONE` and a killed server as
`SERVER_GONE`, both with the output seen so far, where the tool used to
return an error or wait out the timeout. A bounded check of the pane runs
on an interval while `run_shell_command` blocks, so a killed pane ends it
within about a quarter second; the check reads through `list-panes` and
`capture-pane`, so it does not run an `after-display-message` hook. `matched_line` for a match split
across wrapped rows is now only the rows that carry it. (#72)

### Development

- **CI no longer runs macOS.** macOS stays supported on a best-effort basis, and
Expand Down
84 changes: 75 additions & 9 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ token that names no release, such as `master`, fails with
`MalformedResponseException`. A candidate for 3.3a does not satisfy the
released 3.3a minimum.

Ordinary command requests carry an atomic version guard. Control-client
startup checks the floor through its process carrier before launching `-C`;
it does not pin the endpoint between that check and attachment. These checks
require native built-in command semantics. A tmux configuration that replaces
commands such as `if-shell` or `run-shell` through `command-alias` is outside
this qualification.
Ordinary command requests carry an atomic version guard. Managed control
acquisition requires tmux 3.7 or newer and pins the socket before `-C`.
The identity is checked through that pin before attaching; replacement of the
original pathname cannot redirect initialization to a new daemon.
These checks require native built-in command semantics. A tmux configuration
that replaces commands such as `if-shell` or `run-shell` through
`command-alias` is outside this qualification.

The first session on an unused endpoint checks the configured binary before
starting a daemon. Reads leave an unused endpoint empty; cleanup of an absent
Expand All @@ -57,9 +58,69 @@ before starting Python. This validates the selected daemon or executable
without starting a daemon; later Java operations still run their own version
check. The bridge also requires Python libtmux whose declared
`TMUX_MIN_VERSION` is 3.3a or newer and whose own dispatcher enforces that
floor on each command. An older installed backend is refused before builders,
plugins or shell code run, and upgrading tmuxp alone does not supply a
command-level version check.
floor on each command, and the integer
`libtmux.common.TMUX_OPTIONAL_FEATURE_POLICY_VERSION >= 2`. It checks both
requirements in the runtime probe and again in the actual child, before
builders, plugins or shell code run. Policy version 1 is refused, and
upgrading tmuxp alone does not supply a command-level version check or the
optional guards.

No libtmux release on PyPI declares that marker yet; it arrives with
[tmux-python/libtmux#780](https://github.com/tmux-python/libtmux/pull/780).
Until it ships, `tmux-workspace shell` and extension loading refuse on every
installable Python libtmux. Either install libtmux from that source into the
interpreter named by `TMUX_WORKSPACE_PYTHON`, or leave `shell`, `plugins` and
custom builders alone: native loading and every read command need no Python.

### Optional tmux requirements

Ordinary subprocess operations, polling, MCP tools and native workspace
building keep the 3.3a core floor. The selected daemon determines eligibility,
even when the configured client is newer. A refused literal batch runs none
of its commands, including a preceding command that would otherwise succeed.

| Operation | Requirement or repair |
| --- | --- |
| Managed control, watches, mirrors and live views | Stable tmux 3.7+ and a carrier-owned pinned socket. Older daemons are refused before control initialization. |
| Exact named buffer deletion and rename | tmux 3.4+. On 3.3a, a missing name can select an unrelated buffer. |
| `Pane.paste(text)` | tmux 3.4+ before creating a temporary buffer, because failure cleanup needs exact named deletion. |
| Direct `paste-buffer -d -b name` | Retained on 3.3a; native lookup already rejects a missing name. |
| Literal `select-layout` on 3.3a | Invalid operands are refused with `UnsupportedFeatureException` before every batch effect. Presets, unique prefixes and serialized layouts remain available; `-o` requires a serialized operand. |
| Clipboard query | Literal `refresh-client -l`, under any name tmux resolves to it, is refused below tmux 3.6a: on 3.3a it ends the server when a client attached from an `xterm` or `tmux-256color` terminal receives the query, and on 3.4 to 3.6 it ends the server when the client leaves before its terminal answers. |
| Percentage splitting | Typed `SplitSpec.percent` retains `-l N%`. Literal `split-window -p` is refused on exact 3.4. |
| Session and window names on exact 3.7 | A name containing `.` or `:` is refused before dispatch (`IllegalArgumentException`), including workspace window names; 3.7a and later keep such names. |
| `break-pane` on exact 3.7 | Literal calls and unnamed typed calls are refused. Use `Pane.breakOut(name)` or upgrade to 3.7a+. |
| Single-pane `breakOut` before 3.6 | The typed call reads the pane's actual post-command context; native success does not print `-P` output there. |
| Captured `run-shell` output | Existing 3.5 floor remains; 3.3a and 3.4 lose the requested output. Effect-only shell commands remain available. |

Named `breakOut` on exact 3.7 applies the name after the pane moves, because
that release ignores `-n` in its multi-pane branch. A later rename failure can
leave the pane in its new window. An unnamed single-pane break on other
supported releases preserves the existing window name.

Custom `ControlCarrier` implementations must implement `pinSocket` in their
own execution realm. Its alias must retain the socket inode in a private
directory for the attachment's lifetime; replacing the original pathname must
not redirect it. The default method refuses control acquisition. Ordinary
commands through an existing custom transport are unaffected. The built-in
local carrier uses a short private hard link on the socket's filesystem and
refuses before `-C` if no valid alias can be created. Closing an attachment or
receiving EOF removes only its owned alias and directory; the carrier remains
borrowed. One timeout budget covers qualification and attachment.

Literal command words retain their existing encoding, including semicolons
used as data. Feature guards inspect the literal commands a batch represents.
They do not check a tmux command string nested inside one: an `if-shell` body,
a `run-shell -C` command, a hook, a brace block or a `source-file` file. They
also do not interpret shell text or configured aliases. Do not nest a command
this library refuses. The guard does not look inside it, so tmux 3.3a or 3.7
can still end the server or act on the wrong buffer. Those nested strings keep
native tmux semantics and are outside the managed optional-feature guarantee.

Control-positive matrix cases run from 3.7. Lower lanes keep ordinary
operations and explicit before-effect refusal assertions. Documentation
control examples compile on every lane and assert the refusal below that
feature's floor.

### Version ordering and parsing

Expand Down Expand Up @@ -475,6 +536,11 @@ screen wait can end a way a channel never does:
| `TIMED_OUT` | `TextOutcome.TIMED_OUT` |
| `SERVER_GONE` | `TextOutcome.SERVER_GONE` |

A pane killed mid-wait is `TextOutcome.PANE_GONE` instead of a
`CommandRejectedException` from the failed read. `Pane.watchText` takes the
same arguments and also returns the output so far as a `TextWait`. A
condition passed to `Pane.await` still raises for a pane that is gone.

`PRESENT_AT_ENTRY` says only that the text was there on the first look. It may
be output from a command that finished before the wait read, or it may have been
on the pane for an hour; a screen cannot tell those apart. What it replaces is
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ Reference: [`Flows`][scala-ox-flows] on libtmux.org and
[`Flows.liveView`][scala-ox-flows] emits the current view of a server when the
flow starts, then every newer one:

<!-- snippet: scala-sync: root-ox-live-view -->
<!-- snippet: scala-sync-control: root-ox-live-view -->
```scala
import _root_.ox.{supervised, useCloseableInScope}
import io.github.libtmux.scaladsl.{config => _, *}
Expand Down Expand Up @@ -715,8 +715,11 @@ $ tmux-workspace load \
named module `io.github.libtmux`, and the jars carry no OSGi bundle headers.
- **Linux, including WSL.** CI runs on Ubuntu. macOS is supported on a
best-effort basis, and CI coverage for it is highly limited.
- **tmux 3.3a through 3.7c.** The whole real-tmux suite runs against every one
of those releases, and each lane checks it ran the tmux it is named after:
- **tmux 3.3a through 3.7c** for ordinary operations. The whole real-tmux suite
runs against every one of those releases, and each lane checks it ran the tmux
it is named after. Managed control needs stable tmux 3.7 or newer and a
carrier that can pin its socket; the other feature requirements are in the
[migration notes](MIGRATION.md#optional-tmux-requirements):

```console
$ ./gradlew testTmuxMatrix -PlibtmuxMatrix=/path/to/tmux/builds
Expand Down
Loading
Loading