Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
e234dc3
Server(feat[display_message]): Add Server.display_message wrapper
tony May 16, 2026
899441f
Window(feat[display_message]): Add Window.display_message wrapper
tony May 16, 2026
5cefe0f
neo(feat[fields]): Add window_zoomed_flag typed field on Obj
tony May 16, 2026
9b0585d
Pane(fix[reset]): Split into two cmd calls so clear-history runs
tony May 16, 2026
1ebef2b
Pane(feat[send_keys]): Support flag-only invocation (cmd=None)
tony May 16, 2026
77b3393
Server(feat[list_buffers]): Add format_string= and filter= kwargs
tony May 16, 2026
8506df9
neo,server,session,window(feat[search]): Add search_*() methods with …
tony May 16, 2026
24714bc
exc,common(feat[subcommand]): Add LibTmuxException.subcommand and rai…
tony May 16, 2026
6b79cfc
core(refactor[exc]): Migrate stderr raises to raise_if_stderr
tony May 16, 2026
6d153ef
Server(feat[cmd]): Warn on legacy -t-in-args usage
tony May 16, 2026
a783ca3
neo(feat[fields]): Declare pane/window/session format tokens on Obj
tony May 16, 2026
b1ffc68
Client(feat): Add Client dataclass and Server.clients accessor
tony May 16, 2026
7d24f91
Server(feat[run_shell]): Add cwd= and show_stderr= kwargs
tony May 16, 2026
5b3ded5
Pane(feat[capture_pane]): Add pending= kwarg
tony May 16, 2026
a0a46d1
docs(CHANGES): Add 0.57.0 release entry
tony May 16, 2026
ca8cc90
session(docs[cmd]): Correct 0.34 versionchanged precedence rationale
tony May 16, 2026
72dcacd
neo(fix[fields]): Drop tokens missing from tmux 3.2a format_table
tony May 16, 2026
d36df4f
neo,client(fix[fields]): Drop new client_* tokens that crash tmux 3.2…
tony May 16, 2026
40ea9bf
tests(display_message): Skip control-mode dispatch tests on tmux 3.2a
tony May 16, 2026
45a2e7f
Revert "Server(feat[cmd]): Warn on legacy -t-in-args usage"
tony May 16, 2026
c6c1c62
Revert "session(docs[cmd]): Correct 0.34 versionchanged precedence ra…
tony May 16, 2026
62fd3ce
docs(CHANGES): Drop -t-in-args deprecation note
tony May 16, 2026
9979d58
neo(feat[scope-aware]): Make get_output_format scope+version aware
tony May 16, 2026
6d82ac7
neo,client(feat[fields]): Re-expose client_* fields via scope gating
tony May 16, 2026
fba9262
neo(feat[scope-gate]): Add _SCOPE_OVERRIDES dict for fields without s…
tony May 17, 2026
027abc7
docs(CHANGES): Document scope+version gated typed format-token fields
tony May 16, 2026
bcdc261
display_message(fix): Raise LibTmuxException on tmux stderr
tony May 16, 2026
4ec09a9
Pane(docs[capture_pane]): Correct -P/pending semantics
tony May 16, 2026
8cdf9ae
Server(docs[run_shell]): Note tmux chdir fallback for cwd=
tony May 16, 2026
dc6513c
tests(display_message): Skip no-text test on tmux 3.2a control-mode c…
tony May 16, 2026
128f245
neo(fix[scope-gate]): Widen SCOPES_BY_LIST_CMD to admit downward cascade
tony May 16, 2026
f1687e9
common(perf[get_version]): Memoize via @functools.cache to eliminate …
tony May 16, 2026
827c7a4
neo(fix[scope-gate]): Re-admit cursor_x/y/flag/character as pane-scope
tony May 16, 2026
9f1599e
neo(fix[scope-gate]): Re-admit mouse_*_flag and scroll_region_* as pa…
tony May 17, 2026
8f1faa2
neo(fix[scope-gate]): Reclassify universal-mislabeled tokens to true …
tony May 17, 2026
e07edef
neo(fix[field_version]): Gate pane_dead_signal/time to tmux 3.3
tony May 17, 2026
0d30ece
Server(fix[clients,search_sessions]): Propagate tmux errors
tony May 17, 2026
a244155
search,list_buffers(docs[filter]): Warn on silent zero-match
tony May 17, 2026
873c5d9
Client(docs): Warn that session/window/pane fields are attached-view …
tony May 17, 2026
d7f49ca
neo(docs[Obj],tests[cascade]): Pin downward-cascade resolution target
tony May 17, 2026
6a64c59
neo(refactor[_UNIVERSAL_TOKENS]): Add context scope for non-list tokens
tony May 17, 2026
bf5ab07
docs(CHANGES) Correct cascade direction and trim release notes
tony May 17, 2026
92f48a2
docs(CHANGES) Copy improvements
tony May 17, 2026
59bbd20
docs(CHANGES) Copy improvements
tony May 17, 2026
e3d61d1
docs(CHANGES,MIGRATION[breaking]): Flag str(exc) prefix change
tony May 17, 2026
8de240a
Client(feat[attached_*]): Add typed live-attachment properties
tony May 17, 2026
d24190a
Client(fix[attached_*]): Return None for stale detached clients
tony May 17, 2026
b53388c
docs(Client): Clarify live attachment lookup semantics
tony May 17, 2026
872ff69
docs(topics[clients,format-tokens,filtering]): Add conceptual coverag…
tony May 17, 2026
14f86fc
common(fix[raise_if_stderr]): Flatten proc.stderr so str(exc) matches…
tony May 17, 2026
415c3f1
Pane(fix[reset]): Bundle send-keys -R and clear-history as one tmux IPC
tony May 17, 2026
ce4bf97
Client(fix[refresh]): Replace -O-stripped assert with explicit ValueE…
tony May 17, 2026
349e170
neo(fix[_token_scope]): Fail-closed default for unclassified tokens
tony May 17, 2026
13a9b06
Client(feat[_resolve_attached]): Share one list-clients refresh acros…
tony May 17, 2026
b652d0c
docs(CHANGES): Drop duplicate Subcommand-tagged exceptions deliverable
tony May 17, 2026
1228ece
docs(MIGRATION): Add KEEP PLACEHOLDER bracket pattern
tony May 17, 2026
91c89ae
Pane(fix[reset doctest]): Remove timing-fragile capture from the example
tony May 17, 2026
c82c926
docs(search_*): Cross-link from each method to the filtering topic doc
tony May 17, 2026
30bce61
neo(docs[fetch_objs]): Propagate malformed-filter warning to the publ…
tony May 17, 2026
fbeede8
docs(CHANGES,MIGRATION): Correct (#670) PR refs to (#672)
tony May 17, 2026
d4106e4
Server,Window,Pane(refactor[display_message]): Warn on tmux stderr in…
tony May 17, 2026
0b4941d
Server(fix[new_session]): Hydrate Session with live tmux version
tony May 17, 2026
5332b79
neo(feat[fields]): Declare format-token fields from tmux master
tony May 16, 2026
393a937
neo(feat[fields]): Re-expose version-gated tokens added after tmux 3.2a
tony May 16, 2026
8db405d
neo(feat[fields]): Declare forward-looking tokens for tmux 3.7+
tony May 16, 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
205 changes: 199 additions & 6 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,203 @@ $ uvx --from 'libtmux' --prerelease allow python
_Notes on the upcoming release will go here._
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

libtmux 0.57.0 broadens tmux support. It introduces
{class}`~libtmux.Client` as a first-class object, threads tmux's
C-side ``-f`` filter through the typed listing methods so callers can
push predicates into the tmux server, and adds typed access to many
more format tokens — all scope- and version-gated so they're safe on
every supported tmux version. Subcommand context now flows through
{exc}`~libtmux.exc.LibTmuxException`, making it easier for downstream
tools to dispatch on which tmux command produced an error.

### Breaking changes

#### `LibTmuxException` string form gains a subcommand prefix (#672)

When {exc}`~libtmux.exc.LibTmuxException` is raised from one of the
typed command wrappers, ``str(exc)`` now begins with the originating
tmux subcommand name followed by ``": "``. For example, an error from
{meth}`~libtmux.Session.last_window` used to render as ``"can't find
window"`` and now renders as ``"last-window: can't find window"``.

The wrapped stderr is unchanged — ``exc.args[0]`` still holds the raw
tmux output, and the new {attr}`~libtmux.exc.LibTmuxException.subcommand`
attribute exposes the tmux subcommand name as a separate field.
{func}`~libtmux.common.raise_if_stderr` is the shared helper that
populates both.

This is a serialization-format change: code that pattern-matches on
``str(exc)`` exactly or anchors a regex with ``^`` against the old
shape will no longer match.

```python
# Before
try:
session.last_window()
except LibTmuxException as exc:
if str(exc) == "can't find window":
...

# After — dispatch on the typed attribute
try:
session.last_window()
except LibTmuxException as exc:
if exc.subcommand == "last-window":
...

# Or — match against the raw stderr without the prefix
try:
session.last_window()
except LibTmuxException as exc:
if exc.args and exc.args[0] == "can't find window":
...
```

Substring matches (``"can't find" in str(exc)``) and unanchored
``re.search`` patterns continue to work unchanged.

### What's new

#### `Client` object and `Server.clients` accessor (#672)

New {class}`~libtmux.Client` dataclass and
{attr}`~libtmux.Server.clients` property bring typed-ORM ergonomics
to tmux's attached-client model. Reads like ``client.client_readonly``
and ``client.client_session`` work directly on the client instead of
forcing callers down to {meth}`~libtmux.Server.cmd`.

Note that ``client.session_id`` / ``client.window_id`` /
``client.pane_id`` reflect the client's currently attached view at
hydration time — {meth}`~libtmux.Client.refresh` re-reads them after
the client switches focus. ``client.client_name`` is the client's
stable identifier.

For typed access to the live attachment, use
{attr}`~libtmux.Client.attached_session`,
{attr}`~libtmux.Client.attached_window`, and
{attr}`~libtmux.Client.attached_pane`. Each property re-reads the
client from ``list-clients`` before resolving; if tmux no longer
reports that ``client_name``, such as after the client detaches, the
property returns ``None``. Otherwise, the returned
{class}`~libtmux.Session` / {class}`~libtmux.Window` /
{class}`~libtmux.Pane` reflects where the client is attached *now*,
not where it was when the {class}`~libtmux.Client` was constructed.
Direct {meth}`~libtmux.Client.refresh` and
{meth}`~libtmux.Client.from_client_name` calls still surface missing
client lookup errors.

#### `Server.display_message` and `Window.display_message` (#672)

{meth}`~libtmux.Server.display_message` and
{meth}`~libtmux.Window.display_message` join the existing
{meth}`~libtmux.Pane.display_message`. Server reads like
``#{version}`` and ``#{socket_path}`` work without a pane handle;
window reads (``#{window_zoomed_flag}``, ``#{window_active_clients_list}``)
auto-bind to the window's id.

#### C-side filter on typed listing methods (#672)

{meth}`~libtmux.Server.search_panes`,
{meth}`~libtmux.Server.search_windows`,
{meth}`~libtmux.Server.search_sessions`, and the Session/Window
analogues take a ``filter=`` kwarg routed to tmux's ``-f`` flag. tmux
evaluates the predicate and drops non-matching objects before any
Python instance is constructed.

Caveat: tmux silently expands a malformed predicate to empty, which
the format engine treats as false — a typo looks identical to "no
matches". Verify predicate syntax against the FORMATS section of
``tmux(1)``.

#### `Pane.send_keys(cmd=None, …)` flag-only invocation (#672)

{meth}`~libtmux.Pane.send_keys` accepts ``cmd=None`` together with
``reset=True`` or ``repeat=N`` to invoke tmux's flag-only
``send-keys -R`` / ``send-keys -N <n>`` form without any trailing
key argument.

#### `Server.list_buffers(format_string=, filter=)` (#672)

{meth}`~libtmux.Server.list_buffers` gains ``format_string`` (``-F``)
and ``filter`` (``-f``) kwargs. Callers can project a chosen template
(e.g. ``"#{buffer_name}"``) or push a buffer-name match expression
into tmux's format engine — same bad-filter caveat as the
``search_*`` methods.

#### `Server.run_shell(cwd=, show_stderr=)` (#672)

{meth}`~libtmux.Server.run_shell` gains ``cwd`` (``-c``) to set the
shell command's working directory and ``show_stderr`` (``-E``) to
merge the command's stderr into the captured output. Both kwargs are
version-gated; older tmux warns and ignores the flag instead of
erroring.

#### `Pane.capture_pane(pending=True)` (#672)

{meth}`~libtmux.Pane.capture_pane` gains a ``pending`` kwarg that
returns bytes tmux has read from the pane but not yet committed to
the terminal — useful for diagnosing programs whose output stalls
mid-sequence.

#### Scope-aware format-token retrieval (#672)

The ``-F`` template libtmux sends to each ``list-*`` subcommand is now
scope- and version-aware. tmux's format engine cascades context
downward from client → session → current window → active pane, so a
``Session`` row hydrates active-window and active-pane fields via that
cascade, and a ``Client`` row likewise hydrates the client's attached
session, window, and active pane. ``client_*`` tokens resolve only
under ``list-clients`` because tmux has no reverse cascade. Tokens
introduced after tmux 3.2a are gated through ``FIELD_VERSION`` so the
format string stays compatible with the project's minimum supported
tmux. Tokens the running tmux doesn't recognize stay ``None`` on the
typed surface — no crash, no warning.

{class}`~libtmux.Pane`, {class}`~libtmux.Window`,
{class}`~libtmux.Session`, and {class}`~libtmux.Client` declare typed
dataclass fields for the scope-relevant tokens that ship in tmux 3.2a,
including pane state (``pane_dead``, ``pane_in_mode``, ``pane_marked``,
``pane_synchronized``, ``pane_path``, ``pane_pipe`` …), window state
(``window_zoomed_flag``, ``window_silence_flag``, ``window_flags`` …),
session state (``session_marked`` …), and the client view
(``client_session``, ``client_readonly``, ``client_termtype`` …). Typed
fields for tokens tmux added in 3.4 / 3.5 / 3.6 and the forward-looking
set from tmux master will land in a follow-up shipment once those
releases can be validated end-to-end.

### Fixes

- {meth}`~libtmux.Pane.reset` now clears pane scrollback. In 0.56.0
the history clear silently no-op'd, leaving the scrollback intact
(#650).
- {meth}`~libtmux.Server.display_message`,
{meth}`~libtmux.Window.display_message`, and
{meth}`~libtmux.Pane.display_message` surface tmux stderr via
:func:`warnings.warn` instead of silently returning ``[]``. tmux uses
stderr for both genuine errors and informational messages on some
versions, so the wrappers warn rather than raise; callers that want
to escalate can wrap the call in :func:`warnings.catch_warnings` with
``filterwarnings("error")`` (#672).
- {attr}`~libtmux.Server.clients` and
{meth}`~libtmux.Server.search_sessions` propagate tmux errors
rather than silently returning an empty
{class}`~libtmux._internal.query_list.QueryList`. A genuine
list-clients or list-sessions failure now surfaces instead of
looking identical to "no clients" or "filter matched nothing"
(#672).

### Documentation

- New API page: {doc}`api/libtmux.client`.
- {class}`~libtmux.neo.Obj`'s class docstring documents the
downward-cascade resolution target so readers know that, for
example, ``session.pane_id`` is the session's *current window's
active* pane — not "the session's pane" (#672).

## libtmux 0.56.0 (2026-05-10)

libtmux 0.56.0 is the tmux command-parity release. It adds more than
50 wrappers across {class}`~libtmux.Server`, {class}`~libtmux.Session`,
50 commands across {class}`~libtmux.Server`, {class}`~libtmux.Session`,
{class}`~libtmux.Window`, and {class}`~libtmux.Pane`, filling in many
commands that previously required raw {meth}`~libtmux.Server.cmd` calls.
It also adds attached-client test support so interactive tmux commands can be
Expand All @@ -58,7 +251,7 @@ covered in headless test suites.

#### Interactive tmux commands are now scriptable (#653)

libtmux now exposes Python wrappers for tmux commands that normally depend on
libtmux now exposes Python commands for tmux that normally depend on
an attached client: {meth}`~libtmux.Pane.display_popup`,
{meth}`~libtmux.Server.display_menu`,
{meth}`~libtmux.Server.command_prompt`,
Expand All @@ -80,7 +273,7 @@ The `detach-client` API is split by the same scopes tmux actually honors:
`tmux detach-client -a [-t <keep>]`. This keeps each method to one tmux flag
group and one subprocess call.

#### tmux buffer I/O has first-class wrappers (#653)
#### tmux buffer I/O has first-class support (#653)

Named tmux buffers can now be used from libtmux without hand-built commands.
{meth}`~libtmux.Server.set_buffer`,
Expand All @@ -94,7 +287,7 @@ inter-process handoff workflows.

#### Server commands cover key bindings, clients, shell execution, and access (#653)

{class}`~libtmux.Server` gains wrappers for key-binding inspection and mutation
{class}`~libtmux.Server` gains support for key-binding inspection and mutation
({meth}`~libtmux.Server.bind_key`,
{meth}`~libtmux.Server.unbind_key`,
{meth}`~libtmux.Server.list_keys`,
Expand Down Expand Up @@ -137,7 +330,7 @@ Navigation helpers fill in the surrounding topology:
{meth}`~libtmux.Session.next_window`, and
{meth}`~libtmux.Session.previous_window`.

#### Existing wrappers expose more tmux flags (#653)
#### Improvements (#653)

Several established methods now surface tmux flags that were previously only
available by dropping to raw commands. Highlights include
Expand Down Expand Up @@ -196,7 +389,7 @@ old hardcoded socket paths. Fixes #664.

#### tmux 3.7 is within the known-version range (#653)

{data}`~libtmux.common.TMUX_MAX_VERSION` is now `"3.7"`, enabling wrappers and
{data}`~libtmux.common.TMUX_MAX_VERSION` is now `"3.7"`, enabling support and
tests for version-gated tmux 3.7 flags such as
{meth}`~libtmux.Server.command_prompt` `bspace_exit` and
{meth}`~libtmux.Server.show_messages` `terminals` / `jobs`. Installations on
Expand Down
114 changes: 113 additions & 1 deletion MIGRATION
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,121 @@ sections below for detailed migration examples and code samples.

## Upcoming Release

<!-- KEEP THIS PLACEHOLDER - DO NOT REMOVE OR MODIFY THIS LINE -->
_Detailed migration steps for the next version will be posted here._
<!-- END PLACEHOLDER - ADD NEW MIGRATION ENTRIES BELOW THIS LINE -->

<!-- To the maintainers and contributors: please add migration details for the upcoming release here -->
## libtmux 0.57.0: Subcommand-tagged exceptions (#672)

### `LibTmuxException` `str()` gains a subcommand prefix

When {exc}`~libtmux.exc.LibTmuxException` is raised from one of the
typed command wrappers, ``str(exc)`` now starts with the originating
tmux subcommand name followed by ``": "``. The wrapped stderr is
unchanged — ``exc.args[0]`` still holds the raw tmux output, and the
new {attr}`~libtmux.exc.LibTmuxException.subcommand` attribute exposes
the tmux subcommand name on its own.

**Who is affected:** code that pattern-matches `str(exc)` exactly,
anchors a regex with `^` against the previous shape, or hashes the
stringified exception. Substring containment (`"can't find" in
str(exc)`) and unanchored `re.search` patterns continue to match
unchanged.

**Before (0.56.x and earlier):**

```python
try:
session.last_window()
except LibTmuxException as exc:
if str(exc) == "can't find window":
handle_missing_last_window()
```

**After (0.57.0+) — dispatch on the typed attribute:**

```python
try:
session.last_window()
except LibTmuxException as exc:
if exc.subcommand == "last-window":
handle_missing_last_window()
```

**Or — match against the raw stderr in `exc.args[0]`:**

```python
try:
session.last_window()
except LibTmuxException as exc:
if exc.args and exc.args[0] == "can't find window":
handle_missing_last_window()
```

### `Client.session_id` / `window_id` / `pane_id` are snapshots, not identity

{class}`~libtmux.Client` is new in 0.57.0, so this isn't a behavior
change — but new users of {attr}`~libtmux.Server.clients` should know
that ``client.session_id``, ``client.window_id``, and
``client.pane_id`` are hydrated from tmux's downward format cascade
(``c->session`` → ``s->curw`` → ``wl->window->active``) at the moment
the {class}`~libtmux.Client` was built. They go stale as soon as the
client switches sessions, changes window, or detaches.

For typed access that reflects the client's *live* attachment, use
{attr}`~libtmux.Client.attached_session`,
{attr}`~libtmux.Client.attached_window`, and
{attr}`~libtmux.Client.attached_pane`:

```python
# Snapshot (cheap, may be stale)
client = server.clients.get(client_name=ctl.client_name)
session_id = client.session_id # str captured at hydration time

# Live (re-reads list-clients; returns None if tmux no longer reports the client)
attached = client.attached_session # libtmux.Session | None
window = client.attached_window # libtmux.Window | None
pane = client.attached_pane # libtmux.Pane | None
```

The ``client.client_name`` field (typically the tty path on Unix) is
the client's *stable* identifier and does not have this caveat. The
``attached_*`` properties translate a missing ``list-clients`` row into
``None`` for convenience; direct {meth}`~libtmux.Client.refresh` and
{meth}`~libtmux.Client.from_client_name` calls still raise when that
client row is gone.

### `Pane.reset` now dispatches through `self.server.cmd`

{meth}`~libtmux.Pane.reset` now bundles ``send-keys -R`` and
``clear-history`` into a single tmux IPC routed through
``self.server.cmd`` (with an explicit ``-t <pane_id>`` on both
subcommands) rather than calling ``self.cmd`` twice. This closes a race
where output written to the pane between the two IPCs could land in the
scrollback that the second call then cleared.

**Who is affected:** test fixtures and downstream code that intercepts
``Pane.cmd`` (for example with ``unittest.mock.patch.object(pane,
"cmd")``) will no longer observe ``reset()``'s tmux invocation. Patch
``Server.cmd`` instead, or assert on the resulting pane state directly.

### `Server.display_message` / `Window.display_message` / `Pane.display_message` warn instead of raise

The three ``display_message`` wrappers now report tmux stderr via
:func:`warnings.warn` rather than raising
{exc}`~libtmux.exc.LibTmuxException`. tmux uses stderr for both genuine
errors and informational messages, and the right escalation depends on
tmux version and call shape; the wrappers default to warning so callers
can decide. To escalate to an exception, wrap the call in
:func:`warnings.catch_warnings` with ``filterwarnings("error")``:

```python
import warnings

with warnings.catch_warnings():
warnings.filterwarnings("error", category=UserWarning)
result = pane.display_message("#{pane_id}", get_text=True)
```

## libtmux 0.50.0: Unified Options and Hooks API (#516)

Expand Down
2 changes: 2 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from _pytest.doctest import DoctestItem

from libtmux._internal.control_mode import ControlMode
from libtmux.client import Client
from libtmux.pane import Pane
from libtmux.pytest_plugin import USING_ZSH
from libtmux.server import Server
Expand All @@ -42,6 +43,7 @@ def add_doctest_fixtures(
doctest_namespace["Session"] = Session
doctest_namespace["Window"] = Window
doctest_namespace["Pane"] = Pane
doctest_namespace["Client"] = Client
doctest_namespace["server"] = request.getfixturevalue("server")
doctest_namespace["Server"] = request.getfixturevalue("TestServer")
session: Session = request.getfixturevalue("session")
Expand Down
Loading
Loading