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
16 changes: 15 additions & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,25 @@ toolchain and `options.release` in
version matrix in the CI workflow, which builds on 25 and 27, and the claim
`README.md` makes under `Requirements`.

**tmux 3.2a through 3.7c is the supported range**, and it is not a claim: the
**tmux 3.3a through 3.7c is the supported range**, and it is not a claim: the
whole real-tmux suite runs against every one of those releases, and each lane
checks it really ran the tmux it is named after. Moving the range means moving
`workflows/tmux-matrix.yml` and the README together.

Releases below the floor have rejection lanes that run only the version check
against the real binary. Each lane also puts a 3.3a client in front of the old
daemon through `libtmux.tmux.client`, because a client as old as 3.0a or 3.1c
only ever rejects `-N`, and the daemon's own answers reach the library no other
way. The lane replaces a daemon the library already knows with the old one on
the same socket. Each lane passes the release it names (`libtmux.tmux.expected`)
and whether it is a rejection lane (`libtmux.tmux.rejection`), and the version
check fails when the binary reports another release or the wrong side of the
floor; the workflow step that prints each version fails the same way. A module
lists the classes its rejection lanes run in `libtmuxRejectionTests`, and the
lane fails unless every one of them produced a result. The 3.8 release
candidates are preview lanes built from the commit their tag named, since
upstream removed the tags.

**The API carries no compatibility guarantee.** This project is alpha: releases
carry an `-alpha` prerelease tag, any release may change or remove exported
identifiers without a deprecation period, and only the newest release is
Expand Down
4 changes: 2 additions & 2 deletions .github/WRITING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Four things near the top are compatibility claims, and they move together:
exported identifiers without a deprecation period, pin an exact version, not
recommended for production. `CHANGELOG.md` and the `Support and status`
section say the same thing in the same words.
- **The requirements are JDK 25 or newer, and tmux 3.2a through 3.7c.** The tmux
- **The requirements are JDK 25 or newer, and tmux 3.3a through 3.7c.** The tmux
range is not a claim — the matrix runs every lane of it — so it may not drift
from `workflows/tmux-matrix.yml`.
- **Coordinates are group `io.github.libtmux`, imported through
Expand Down Expand Up @@ -142,7 +142,7 @@ Order it capability, then consequence, then compatibility:

> 0.0.1-alpha.7 adds streaming capture, and rejects `windowId` at pane scope
> rather than ignoring it. Pass `scope: window` to read at window scope. JDK 25
> and tmux 3.2a through 3.7c are unchanged.
> and tmux 3.3a through 3.7c are unchanged.

The title is plain — the version, optionally preceded by `libtmux for Java`.
Never "we are excited to announce"; state what shipped.
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ jobs:
run: |
python3 -m venv "$RUNNER_TEMP/workspace-python"
"$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
git -C "$RUNNER_TEMP/python-libtmux-core" checkout --quiet --detach \
c27dd929fc25c3a95648759d8c0537a07ef60258
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__)' \
"$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"

# Runs every check task instead of stopping at the first failure, so one run names them all.
Expand Down
96 changes: 90 additions & 6 deletions .github/workflows/tmux-matrix.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: tmux matrix

# The README claims tmux 3.2a through 3.7c, and says that range is not a claim because the suite runs
# The README claims tmux 3.3a through 3.7c, and says that range is not a claim because the suite runs
# against every one of them. This is where that stops depending on someone running it locally.

on:
Expand All @@ -27,7 +27,7 @@ jobs:
lanes:
# Preview lanes carry their own label: this is the only place a reader sees which entries the
# supported range does not include, since the README and the matrix property list only the rest.
name: tmux ${{ matrix.tmux }}${{ matrix.experimental && ' (preview, not supported)' || '' }}
name: tmux ${{ matrix.tmux }}${{ matrix.experimental && ' (preview, not supported)' || '' }}${{ matrix.rejection && ' (must be refused)' || '' }}
runs-on: ubuntu-latest
timeout-minutes: ${{ matrix.tmux == 'master' && 25 || 15 }}
continue-on-error: ${{ matrix.experimental == true }}
Expand All @@ -38,13 +38,34 @@ jobs:
# (its `lanes` list), which is the source of truth. The agree step below fails if they drift
# apart. This is also what DocumentationFactsTest reads the README's claimed range from, so a
# preview lane belongs in `include` below and never here.
tmux: ['3.2a', '3.3', '3.3a', '3.4', '3.5', '3.6', '3.7', '3.7a', '3.7b', '3.7c']
tmux: ['3.3a', '3.4', '3.5', '3.6', '3.7', '3.7a', '3.7b', '3.7c']
# Ahead of the supported range, matching the plugin's `previewLanes` list: 3.8-rc2 is a
# candidate for a release not yet cut, and master is upstream's development branch, not a
# version. Both may fail and neither moves the README's claimed range.
include:
# Below the floor, matching the plugin's `rejectionLanes`: the real binary must refuse
# every call and leave a sentinel session, option and buffer untouched. Each also runs
# as the daemon behind a 3.3a client, the only way a 3.0a or 3.1c answer reaches the library.
- tmux: '3.0a'
rejection: true
client: '3.3a'
- tmux: '3.1c'
rejection: true
client: '3.3a'
- tmux: '3.2a'
rejection: true
client: '3.3a'
- tmux: '3.3'
rejection: true
client: '3.3a'
- tmux: '3.8'
experimental: true
- tmux: '3.8-rc2'
experimental: true
commit: 17d32ae40d28f1548d4d75b32161ffdc69305482
- tmux: '3.8-rc3'
experimental: true
commit: 331611b632ae3b9306d57dc44f21ca05f567b878
- tmux: master
experimental: true
steps:
Expand All @@ -69,7 +90,7 @@ jobs:
# master has no release tarball to build from a tag, so it is checked out and autoreconf'd
# instead. autoconf and automake are needed only for that.
- name: Build dependencies for a git checkout
if: matrix.tmux == 'master'
if: matrix.tmux == 'master' || matrix.commit
run: sudo apt-get install --no-install-recommends -y autoconf automake

# Keyed by version alone: a released tarball does not change, so a hit is the same binary the
Expand All @@ -84,7 +105,7 @@ jobs:
key: tmux-${{ matrix.tmux }}-${{ runner.os }}-${{ runner.arch }}

- name: Build tmux ${{ matrix.tmux }}
if: matrix.tmux != 'master' && steps.tmux-cache.outputs.cache-hit != 'true'
if: matrix.tmux != 'master' && !matrix.commit && steps.tmux-cache.outputs.cache-hit != 'true'
env:
LANE: ${{ matrix.tmux }}
run: |
Expand All @@ -98,6 +119,48 @@ jobs:
make -j"$(nproc)"
make install

# The 3.8 release candidates have no tarball and their tags are gone upstream, so each is
# fetched by the commit it was tagged at.
- name: Build tmux ${{ matrix.tmux }} from its commit
if: matrix.commit && steps.tmux-cache.outputs.cache-hit != 'true'
env:
LANE: ${{ matrix.tmux }}
COMMIT: ${{ matrix.commit }}
run: |
set -eux
git init "$HOME/tmux-src-$LANE"
cd "$HOME/tmux-src-$LANE"
git fetch --depth 1 https://github.com/tmux/tmux.git "$COMMIT"
git checkout --detach FETCH_HEAD
sh autogen.sh
./configure --prefix="$HOME/tmux-builds/$LANE"
make -j"$(nproc)"
make install

# The daemon of a 3.0a or 3.1c lane is reached by this client.
- name: Restore tmux ${{ matrix.client }} for the client
if: matrix.client
id: client-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ~/tmux-builds/${{ matrix.client }}
key: tmux-${{ matrix.client }}-${{ runner.os }}-${{ runner.arch }}

- name: Build tmux ${{ matrix.client }} for the client
if: matrix.client && steps.client-cache.outputs.cache-hit != 'true'
env:
CLIENT: ${{ matrix.client }}
run: |
set -eux
work="$(mktemp -d)"
curl -fsSL -o "$work/tmux.tar.gz" \
"https://github.com/tmux/tmux/releases/download/$CLIENT/tmux-$CLIENT.tar.gz"
tar -xzf "$work/tmux.tar.gz" -C "$work"
cd "$work/tmux-$CLIENT"
./configure --prefix="$HOME/tmux-builds/$CLIENT"
make -j"$(nproc)"
make install

# No release, no tag even: master is a moving branch, so it is built the way a contributor
# without a dist tarball would, generating the files a tarball normally ships pre-built.
- name: Build tmux master from a git checkout
Expand All @@ -114,8 +177,20 @@ jobs:
- name: The lane runs the tmux it is named after
env:
LANE: ${{ matrix.tmux }}
CLIENT: ${{ matrix.client }}
run: |
"$HOME/tmux-builds/$LANE/bin/tmux" -V
check() {
reported="$("$HOME/tmux-builds/$1/bin/tmux" -V)"
echo "$1: $reported"
if [ "$1" = master ]; then
case "$reported" in "tmux next-"*) ;; *) echo "::error::$1 reports $reported"; exit 1 ;; esac
elif [ "${reported#* }" != "$1" ]; then
echo "::error::the $1 build reports $reported"
exit 1
fi
}
check "$LANE"
if [ -n "$CLIENT" ]; then check "$CLIENT"; fi

- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6
with:
Expand All @@ -132,6 +207,15 @@ jobs:
run: |
python3 -m venv "$RUNNER_TEMP/workspace-python"
"$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
git -C "$RUNNER_TEMP/python-libtmux-core" checkout --quiet --detach \
c27dd929fc25c3a95648759d8c0537a07ef60258
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__)' \
"$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"

# One lane rather than testTmuxMatrix: each version builds and runs on its own runner, so the
Expand Down
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,47 @@ production.

## Unreleased

### Added

- **`TmuxVersion.MINIMUM` and `UnsupportedFeatureException.belowFloor()`.**
`MINIMUM` is the oldest supported tmux, 3.3a. `belowFloor()` is true when the
exception reports a tmux older than that and false when one optional feature
is missing from a supported tmux, so a handler that skips optional features
can still surface a floor refusal. (#69)
- **`CommandRequest` can discard standard output.** `discardStdout` sends it to
the null device for a caller that reads only the exit status and standard
error; the version probe uses it, because tmux 3.0 and 3.1 daemons hold the
client's standard output open after it exits. The four-argument constructor
is unchanged. (#69)
- **The MCP server states the tmux floor.** Its instructions and
`tmux://capabilities` (`minimumTmux`) name 3.3a. Against an older tmux,
startup prints one `libtmux-mcp:` line and exits non-zero without a stack
trace. (#69)
- **`tmux-workspace` reports a tmux older than 3.3a as `tmux_unsupported`.**
The code appears in JSON output beside the message, which says to upgrade
tmux or use the previous release. (#69)

### Changed

- **tmux 3.3a is the minimum across the JVM modules and products.** Upgrade the
running daemon as well as the client. Commands refuse older daemons with
`UnsupportedFeatureException` before their requested effects, and the message
says to upgrade tmux and restart any running server, or use the previous
libtmux release. This includes 3.0 and 3.1, which cannot name their version. A
version that names no release, such as `master`, fails with
`MalformedResponseException`. The Python bridge also requires compatible
Python libtmux. See [migration notes](MIGRATION.md). (#69)
- **The workspace CLI's attached handoff starts no daemon.** `workspace load`
passes `-N` to the `attach-session` it runs. (#69)
- **`TmuxVersion` orders release candidates below their release and parses
strictly.** `3.8-rc < 3.8`, `compareTo` agrees with `equals`, and text such
as `3.3a garbage`, `3.3a-master`, `next-3.9a` or `3.8-rc0` is rejected.
`atLeast` follows the same order, so a candidate or `next-` build no longer
meets a gate on the release it names. (#69)
- **JSON layouts are gated on tmux 3.8-rc2.** The release candidates already
write them, so the gate names the earliest build measured to; `3.8-rc` and
`next-3.8` are still refused. (#69)

### Documented

- **Every README opens with a quick start and is sectioned by task, with the
Expand Down
67 changes: 67 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,73 @@ API changes that require updates to calling code are recorded here. See
A breaking type is named on its own `api-break:` line. Mentioning the type in
the prose is not that line.

### tmux 3.3a is the minimum

Upgrade the tmux daemon to 3.3a or newer. Installing a newer client alone does
not upgrade a daemon that is already running. Finish or save its sessions,
stop that daemon, then start the new tmux binary on the same endpoint.

Commands through Java, Kotlin and Scala, including MCP tools and native
workspace loading, refuse an older daemon with `UnsupportedFeatureException`
before sending the requested operation. Raw commands and batches follow the
same rule. The message says to upgrade tmux and restart any running server, or
to use the previous libtmux release. A daemon older than 3.2 cannot answer the
version probe and is refused the same way.

`UnsupportedFeatureException.belowFloor()` is true for this refusal and false
when one optional feature is missing from a supported tmux. A handler that
skips optional features should test it first, so that a tmux below the floor
is surfaced and not skipped.

Standalone `cmd("-V")` and `cmd("-h")` remain available for executable
diagnostics; their results do not authorize a daemon operation. A version
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.

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
daemon remains harmless. Optional features retain their own version
requirements.

The MCP server names the floor in its instructions and in
`tmux://capabilities` as `minimumTmux`. Against an older tmux it prints one
`libtmux-mcp:` line and exits non-zero at startup, and a tool call answered by
an older daemon mid-session returns the `UNSUPPORTED_FEATURE` error code. The
workspace CLI reports the refusal with the machine code `tmux_unsupported`.

Python extensions and the Python shell use `Server.requireCompatibleTmux()`
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.

### Version ordering and parsing

`TmuxVersion.compareTo` now orders `next-X.Y < X.Y-rc < X.Y-rcN < X.Y < X.Ya`,
so it agrees with `equals`; a candidate no longer compares equal to its
release. `atLeast` follows that order: `3.8-rc` and `next-3.8` no longer meet a
gate on `3.8`, which the 0.0.1-alpha.14 notes below describe the other way
round. Gate a behaviour that a candidate already ships on the earliest build
measured to have it, such as `new TmuxVersion(3, 8, "", false, "rc2")` for JSON
layouts and an empty dead-pane pid. Use `sameRelease` for a gate on one
release's own defect.

`TmuxVersion.parse` accepts only what tmux has reported (`3.3a`, `3.8-rc2`,
`next-3.9`), with numbers of up to four digits, and throws
`IllegalArgumentException` for anything else, such as `3.3a garbage`,
`next-3.9a` or `3.8-rc0`.

## 0.0.1-alpha.16

### `CommandResult.stdout()` keeps carriage returns
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ $ tmux-workspace load \
named module `io.github.libtmux`, and the jars carry no OSGi bundle headers.
- **Linux, including WSL.** CI runs on Ubuntu. A macOS job runs the same checks
and reports without gating a merge.
- **tmux 3.2a through 3.7c.** The whole real-tmux suite runs against every one
- **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:

```console
Expand Down
Loading
Loading