Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
45b25e1
Core(fix): Check for tmux 3.3a before commands
tony Oct 17, 2026
61f0b90
Core(refactor): Use the daemon feature baseline
tony Oct 17, 2026
7837b14
Docs(docs): Align support and CI on tmux 3.3a
tony Oct 17, 2026
1c1d091
Core(fix): Reject undated version reports
tony Oct 17, 2026
8bb4995
Core(fix): Prevent unchecked daemon restarts
tony Oct 17, 2026
52eadfd
Core(fix): Tell -N apart from option values
tony Oct 17, 2026
49b2c33
Formats(fix): Bound numbered dev builds' fields
tony Oct 17, 2026
23a721e
Tests(fix): Wait for the break-pane naming command
tony Oct 17, 2026
9ecc840
Core(fix): Keep version refusals through lookups
tony Oct 17, 2026
0095c8d
Docs(fix): Await real output in marker example
tony Oct 17, 2026
379f18d
Core(fix): Accept a renamed executable's banner
tony Oct 17, 2026
67c0f2d
Core(fix): Probe the daemon version hook-free
tony Oct 17, 2026
e2d5e6a
Support(docs): Describe the hook-free probe
tony Oct 17, 2026
f5f3677
Core(fix): Parse versions as tmux reports them
tony Oct 17, 2026
1ba5265
Core(fix): Pick optional flags by daemon release
tony Oct 17, 2026
69d5816
Core(perf): Reuse each endpoint's checked release
tony Oct 17, 2026
addb94f
Support(docs): Describe per-endpoint version reuse
tony Oct 17, 2026
54f0658
Core(feat): Add a small version-check surface
tony Oct 17, 2026
6f27117
Docs(feat): Document the internal support package
tony Oct 17, 2026
e1d41e1
Tests(feat): Refuse real 3.2a and 3.3 daemons
tony Oct 17, 2026
52aa796
CI(test): Run the native rejection lanes
tony Oct 17, 2026
db9bae6
CI(test): Add the tmux 3.8 lane
tony Oct 17, 2026
6fc4222
Hooks(feat): Track the tmux 3.8 hook table
tony Oct 17, 2026
c747540
Pane(fix[new_pane]): Expect 3.8 border footprint
tony Oct 17, 2026
f302319
tests(fix[popup]): Attach popup clients on a pty
tony Oct 17, 2026
2c14822
Server(fix): Pickle a Server without its cache
tony Oct 17, 2026
28a4e77
Support(fix): Read the daemon in server_version
tony Oct 17, 2026
2a517cc
Core(refactor[version]): Deprecate -V helpers
tony Oct 17, 2026
0a514b6
tests(refactor[legacy_api]): Move -V helper tests
tony Oct 17, 2026
08609ab
tests(feat[legacy_api]): Assert -V helpers warn
tony Oct 17, 2026
b1fc5a7
Docs(refactor[doctest]): Gate on daemon release
tony Oct 17, 2026
1d6c64b
Docs(refactor[topics]): Gate on daemon release
tony Oct 17, 2026
f2b46c8
tests(refactor[tmux_client]): Add -V test gate
tony Oct 17, 2026
f82c0bb
tests(refactor[tmux_client]): Use client_gte
tony Oct 17, 2026
e291d2e
Development(feat[pytest]): Fail own -V helper use
tony Oct 17, 2026
c27dd92
Changelog(docs): Record the tmux 3.3a minimum
tony Oct 17, 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
40 changes: 39 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
fail-fast: false
matrix:
python-version: ['3.15']
tmux-version: ['3.2a', '3.3a', '3.4', '3.5', '3.6', '3.7a', '3.7b', 'master']
tmux-version: ['3.3a', '3.4', '3.5', '3.6', '3.7a', '3.7b', '3.8', 'master']
steps:
- uses: actions/checkout@v7

Expand Down Expand Up @@ -90,7 +90,45 @@
with:
token: ${{ secrets.CODECOV_TOKEN }}

unsupported-versions:
# Real 3.2a and 3.3 daemons must be refused with no effects.
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Install uv
uses: astral-sh/setup-uv@v10.2.0
with:
enable-cache: true

- name: Set up Python 3.14
run: uv python install 3.14

- name: Install dependencies
run: uv sync --all-extras --dev

- name: Build supported and rejected native endpoints
run: |
sudo apt-get update
sudo apt-get install -y libevent-dev libncurses-dev bison
mkdir -p "$RUNNER_TEMP/tmux-versions"
for version in 3.2a 3.3 3.3a; do
git clone --quiet --depth=1 --branch "$version" https://github.com/tmux/tmux.git "$RUNNER_TEMP/tmux-versions/$version"
cd "$RUNNER_TEMP/tmux-versions/$version"
sh autogen.sh
./configure --prefix="$RUNNER_TEMP/tmux-versions/$version/install"
make -j2
make install
done
echo "LIBTMUX_TEST_MINIMUM_TMUX=$RUNNER_TEMP/tmux-versions/3.3a/install/bin/tmux" >> "$GITHUB_ENV"
echo "LIBTMUX_TEST_UNSUPPORTED_TMUX=$RUNNER_TEMP/tmux-versions/3.2a/install/bin/tmux" >> "$GITHUB_ENV"
echo "LIBTMUX_TEST_UNPATCHED_TMUX=$RUNNER_TEMP/tmux-versions/3.3/install/bin/tmux" >> "$GITHUB_ENV"

- name: Refuse older daemons
run: uv run pytest tests/test_versions.py --reruns=0 -q

release:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium test

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
Expand Down
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ be stated twice, the file listed above is the one that governs.
- A passing gate is evidence only once it has been shown capable of
failing. Pair a new test with a deliberate break that proves it bites.

tmux >= 3.2a is the compatibility floor (see `tests.yml`'s build
tmux >= 3.3a is the compatibility floor (see `tests.yml`'s build
matrix). `Server.sessions`, `Server.clients`, and
`Server.attached_sessions` return an empty `QueryList` rather than
raising when the underlying tmux list command fails for any reason —
Expand All @@ -61,6 +61,9 @@ list-shaped accessors are lenient by default; `Server.is_alive()` and
`src/libtmux/AGENTS.md` for the full contract and this package's
logging conventions.

An unsupported tmux version raises `VersionTooLow`, including from lenient
list accessors and query traversal. Version refusals are not empty results.

## References

- Documentation: https://libtmux.git-pull.com/
Expand Down
56 changes: 56 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,56 @@ $ uvx --from 'libtmux' --prerelease allow python
_Notes on the upcoming release will go here._
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### Breaking changes

#### tmux 3.3a is the minimum (#779)

Minimum `tmux>=3.3a` (was `>=3.2a`). libtmux now checks the release of the
tmux server it talks to, not the `tmux` executable on `PATH`, and raises
{exc}`~libtmux.exc.VersionTooLow` before sending anything to an older server,
including 3.3 without its patch letter. Upgrading the executable does not
upgrade a server that is already running: restart it after saving its
sessions. See {doc}`project/compatibility` for the releases each feature needs.

#### Executable version helpers are deprecated (#779)

{func}`~libtmux.common.get_version`, {func}`~libtmux.common.get_version_str`,
{func}`~libtmux.common.has_version`, {func}`~libtmux.common.has_gt_version`,
{func}`~libtmux.common.has_gte_version`, {func}`~libtmux.common.has_lte_version`,
{func}`~libtmux.common.has_lt_version`,
{func}`~libtmux.common.has_minimum_version`, `TMUX_MIN_VERSION` and
`TMUX_MAX_VERSION` now emit a {exc}`DeprecationWarning`. They read the `tmux`
executable, which can be a different release from the server a
{class}`~libtmux.Server` talks to. They keep working.

```python
# Before
from libtmux.common import has_gte_version

has_gte_version("3.4")

# After
from libtmux._internal.support import is_at_least, server_version

is_at_least(server_version(server), "3.4")
```

The replacement lives in the private `libtmux._internal.support` module for
now, so its names can still change.

### Fixes

#### Optional flags follow the server's release (#779)

Commands now pick their optional flags from the tmux server's release, not
the `tmux` executable's, so a newer executable talking to an older server no
longer sends flags that server rejects.

#### Hooks from tmux 3.8 are read (#779)

{meth}`~libtmux.hooks.HooksMixin.show_hook` knows the hooks tmux 3.8 adds, and
a hook a newer tmux adds no longer stops the hook table from being read.

### Documentation

#### Cleaner `from_env` examples (#719)
Expand All @@ -57,6 +107,12 @@ environment-setup plumbing, so each example leads with the constructor call it
demonstrates instead of the socket-path and `$TMUX` boilerplate needed to run
it.

#### Completion example waits for real output (#779)

The command-completion example in {doc}`topics/automation_patterns` runs a
real command and waits for its exact output line, so it no longer matches the
echoed input or misses output that arrives late.

### Development

#### CI actions updated to current majors
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ libtmux is a typed Python API over [tmux], the terminal multiplexer. Stop shelli

## Requirements & support

- tmux: >= 3.2a
- tmux: >= 3.3a
- Python: >= 3.10 (CPython and PyPy)

Maintenance-only backports (no new fixes):
Expand Down
17 changes: 17 additions & 0 deletions docs/internals/api/libtmux._internal.support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Internal Support

The {mod}`libtmux._internal.support` package holds the tmux version check that
every command runs against the connected daemon.

:::{warning}
These APIs are private and are **not** covered by version policies. They can
break or be removed between minor versions.
:::

```{eval-rst}
.. automodule:: libtmux._internal.support
:members:

.. automodule:: libtmux._internal.support.versions
:members:
```
7 changes: 7 additions & 0 deletions docs/internals/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ Internal format strings and tmux constants.
Sparse array data structure for tmux format parsing.
:::

:::{grid-item-card} Support
:link: api/libtmux._internal.support
:link-type: doc
The tmux version check every command runs against the connected daemon.
:::

::::

```{toctree}
Expand All @@ -47,6 +53,7 @@ api/libtmux._internal.dataclasses
api/libtmux._internal.query_list
api/libtmux._internal.constants
api/libtmux._internal.sparse_array
api/libtmux._internal.support
```

## Environmental variables
Expand Down
50 changes: 46 additions & 4 deletions docs/project/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,52 @@

## tmux

- **Minimum**: tmux 3.2a
- **Tested**: latest stable tmux release
- libtmux uses tmux's format system and control mode -- older tmux versions
may lack required format variables
Core operations require tmux **3.3a or newer**, including the patch letter.
Tmux 3.3 is below the minimum. The CI matrix includes 3.3a and later releases;
`master` is a development lane.

Commands check the connected daemon, which can differ from the selected
client executable. An absent endpoint uses the executable's fresh version
report before startup. Raw commands, batches and liveness checks share this
rule. An unsupported or unreadable version raises
{exc}`~libtmux.exc.VersionTooLow` before the requested command runs.
A replacement daemon is checked again before dispatch.
{meth}`~libtmux.Server.kill` returns without dispatching when its endpoint is
absent.

Each {class}`~libtmux.Server` probes its endpoint once, bounded to five
seconds, and reuses the checked release for later commands and optional-flag
decisions. The version fence in every command catches a replaced daemon: it
refuses with no effect and the next command probes again. A vanished daemon is
detected and the next command checks the version again; {meth}`~libtmux.Server.kill` and
an absent endpoint always read the endpoint directly. The query is `run-shell -C` with a marker format, which
fires neither `after-*` hooks nor `command-error`, so a user's hooks never run
for a version check. It exits 1 with the version in its error text. If a replaced
daemon fails the fence on tmux 3.5 or newer, `command-error` fires once for
that refusal, which reports a real error. Binary diagnostics through `tmux -V` and
`tmux -h` remain available. Raw `tmux -c` shell execution is refused; use
{meth}`~libtmux.Server.run_shell` for a tmux command instead.

The version check reads the forms tmux reports: a release with an optional patch
letter (`3.3a`), a candidate (`3.8-rc`, `3.8-rc2`) and a development build
(`next-3.9`). They order as `next-3.9` < `3.9-rc` < `3.9-rc2` < `3.9` < `3.9a`,
and minor numbers compare numerically, so a candidate or development build is
supported only when its release number is above the baseline. Such builds do not
inherit stable feature qualification. Everything else is refused: an undated
`master`, a development suffix on a release (`3.3a-dev`), a patch letter on a
development build (`next-3.3a`), leading zeros and OpenBSD's own numbering. On
OpenBSD, an existing daemon must report a qualified release; an executable
without `-V` cannot start an absent endpoint.
The synthetic values returned by legacy version helpers are diagnostic values
and never authorize a command.

If a checked daemon disappears before dispatch, that command fails without
starting the configured executable. Retry against the absent endpoint to
validate the executable before creating a replacement.

Individual features can require a newer daemon. Their API documentation states
those requirements. Raising the core floor does not qualify control startup,
output synchronization or other optional behavior on every later release.

## Platforms

Expand Down
2 changes: 1 addition & 1 deletion docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ from inside a live tmux session.

## Requirements

- [tmux] 3.2a or newer
- [tmux] 3.3a or newer
- [pip] - for this handbook's examples

[tmux]: https://tmux.github.io/
Expand Down
20 changes: 9 additions & 11 deletions docs/topics/automation_patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -514,21 +514,19 @@ True

### 1. Always use markers for completion detection

Timing is a guess; a marker is a fact. Instead of sleeping long enough and hoping a
command finished, have it print an explicit marker and poll for that. Your automation
then reacts to what actually happened rather than to a clock.
Have the command print a marker when it finishes, then wait until that marker appears
as a complete output line. Starting the command with the window avoids matching an
echo of typed input. The shell keeps the pane open so its output remains available.

```python
>>> bp_window = session.new_window(window_name='best-practice', attach=False)
>>> bp_window = session.new_window(
... window_name='best-practice', attach=False,
... window_shell="printf '%s\\n' 'work complete' '__DONE__'; exec /bin/sh",
... )
>>> bp_pane = bp_window.active_pane

>>> # Good: Use completion marker
>>> bp_pane.send_keys('long_command; echo "__DONE__"')

>>> # Then poll for marker
>>> import time
>>> time.sleep(0.2)
>>> '__DONE__' in '\\n'.join(bp_pane.capture_pane())
>>> from libtmux.test.retry import retry_until
>>> retry_until(lambda: '__DONE__' in bp_pane.capture_pane(), seconds=3)
True

>>> bp_window.kill()
Expand Down
28 changes: 18 additions & 10 deletions docs/topics/floating_panes.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ floating by reading its {attr}`pane_floating_flag <libtmux.Pane.pane_floating_fl
which is `"1"` when it floats:

```python
>>> from libtmux.common import has_gte_version
>>> from libtmux._internal.support import is_at_least, server_version

>>> if has_gte_version("3.7"):
>>> if is_at_least(server_version(server), "3.7"):
... floating = window.new_pane(width=20, height=5, shell="sleep 30")
... is_floating = floating.pane_floating_flag
... else:
Expand All @@ -52,17 +52,25 @@ window (tmux's `-X` / `-Y`). tmux reports the placement back through the
{attr}`pane_y <libtmux.Pane.pane_y>` fields:

```python
>>> from libtmux.common import has_gte_version
>>> from libtmux._internal.support import is_at_least, server_version

>>> if has_gte_version("3.7"):
>>> if is_at_least(server_version(server), "3.7"):
... placed = window.new_pane(width=20, height=5, x=2, y=1, shell="sleep 30")
... position = (placed.pane_x, placed.pane_y)
... else:
... position = ("2", "1")
>>> position
('2', '1')
>>> border = 1 if is_at_least(server_version(server), "3.8") else 0
>>> position == (str(2 + border), str(1 + border))
True
```

From tmux 3.8, `width`, `height`, `x` and `y` give the pane's footprint on screen
including its border, which is drawn by default. A pane created with
`width=20, height=5, x=2, y=1` then reports `pane_width` 18, `pane_height` 3,
and `pane_x` / `pane_y` of 3 and 2: the border sits on the requested column and
row, and the content one cell inside. Earlier versions size and place the
content directly.

## Styling

For the rarer cases where appearance matters, you can style a floating pane with
Expand All @@ -84,9 +92,9 @@ that stays on screen and waits for you after the command finishes instead of
vanishing:

```python
>>> from libtmux.common import has_gte_version
>>> from libtmux._internal.support import is_at_least, server_version

>>> if has_gte_version("3.7"):
>>> if is_at_least(server_version(server), "3.7"):
... held = window.new_pane(width=20, height=5, shell="sleep 30", keep=True)
... remain = held.cmd("show-options", "-p", "-v", "remain-on-exit").stdout
... else:
Expand All @@ -104,9 +112,9 @@ hold a pane — including filtering a window's {attr}`~libtmux.Window.panes` dow
to just the floats:

```python
>>> from libtmux.common import has_gte_version
>>> from libtmux._internal.support import is_at_least, server_version

>>> if has_gte_version("3.7"):
>>> if is_at_least(server_version(server), "3.7"):
... _ = window.new_pane(width=20, height=5, shell="sleep 30")
... floating = [p for p in window.panes if p.pane_floating_flag == "1"]
... found = len(floating) >= 1
Expand Down
Loading
Loading