From af70c02d2a4f3230d2ee365ca1a00ad32ba2ce91 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 17 May 2026 06:32:23 -0500 Subject: [PATCH 1/8] docs(CHANGES) Correct cascade direction and trim release notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: The 0.57.x section claimed list-clients "emits the client_* tokens but never pane_* ones" — backwards: tmux's format_defaults cascades downward through c->session, the session's current window, and that window's active pane, so list-clients (and list-sessions and list-windows) all hydrate pane fields via cascade. Pinned by the strengthened cascade tests on this branch and by upstream cmd-list-clients.c calling format_defaults(ft, l[i], NULL, NULL, NULL). Two adjacent overclaims compounded the issue: a fixed token count ("twelve client_* tokens") that didn't match the typed Client surface, and a "every wrapper uses raise_if_stderr" claim that didn't match the supported surface in practice. what: - Rewrite the cascade paragraph in the typed format-token entry to describe the actual downward cascade (Client/Session/Window rows hydrate active-pane/window fields; client_* resolves only under list-clients because tmux has no reverse cascade). - Drop number slop: "twelve client_* tokens", "~45 additional format tokens", the explicit list of eight forward-looking tokens, and the embedded token-name catalogs that belong in autodoc. - Replace "every wrapper" with "shared helper used across the supported surface". Trim "matching the rest of the typed wrappers" to "matching the rest of the supported surface". - Add the Client snapshot caveat (session_id/window_id/pane_id are attached-view, not identity) into the Client entry. - Add the malformed-predicate caveat to the C-side filter entry. - Add a Fixes bullet for Server.clients and Server.search_sessions propagating tmux errors instead of swallowing them. - Add a Documentation bullet for the Obj cascade-semantics docstring. - Tighten the lead paragraph: drop "closes wrapper gaps" and the token count. --- CHANGES | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/CHANGES b/CHANGES index eb0ac5620..04f1f42c9 100644 --- a/CHANGES +++ b/CHANGES @@ -45,6 +45,14 @@ $ uvx --from 'libtmux' --prerelease allow python _Notes on the upcoming release will go here._ +libtmux 0.57.0 broadens the supported surface. 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. ### Documentation #### Cleaner `from_env` examples (#719) @@ -332,6 +340,17 @@ unaffected. ## libtmux 0.58.1 (2026-06-16) +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. libtmux 0.58.1 restores compatibility with pytest 9.1. The bundled pytest plugin no longer aborts at import time, so projects that rely on libtmux's fixtures can move to the latest pytest without their test @@ -552,6 +571,12 @@ Caveat: tmux silently expands a malformed filter expression to empty, which it treats as false — a typo looks identical to "no matches". Verify filter syntax against the FORMATS section of ``tmux(1)``. +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 (#670) #### `Pane.send_keys(cmd=None, …)` flag-only invocation (#672) {meth}`~libtmux.Pane.send_keys` accepts ``cmd=None`` together with @@ -562,6 +587,10 @@ 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. and ``filter`` (``-f``) kwargs. Callers can ask tmux to return selected fields (e.g. ``"#{buffer_name}"``) or only buffers matching an expression — same bad-filter caveat as the ``search_*`` methods. @@ -574,6 +603,7 @@ 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)` (#670) #### `Pane.capture_pane(pending=True)` (#672) {meth}`~libtmux.Pane.capture_pane` gains a ``pending`` kwarg that @@ -581,6 +611,40 @@ returns bytes tmux has read from the pane but not yet committed to the terminal — useful for diagnosing programs whose output stalls mid-sequence. +#### Subcommand-tagged exceptions (#670) + +{exc}`~libtmux.exc.LibTmuxException` takes an optional ``subcommand`` +attribute. When set, ``str(exc)`` prefixes the originating tmux +command name (e.g. ``"last-window: no such window"``), giving +downstream consumers a stable way to dispatch on which tmux command +produced the error. {func}`~libtmux.common.raise_if_stderr` is the +shared helper used across the supported surface to populate it. + +#### Scope-aware format-token retrieval (#670) + +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. #### More format-token fields on tmux objects (#672) libtmux now asks each ``list-*`` subcommand for the format tokens that make @@ -603,10 +667,26 @@ pane." See {ref}`format-tokens` for details. - {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` raise + {exc}`~libtmux.exc.LibTmuxException` when tmux reports an error, + matching the rest of the supported surface (#670). +- {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). - New {ref}`format-tokens` topic explains why some fields describe an active child object, such as ``session.pane_id`` reflecting the active pane in the session's current window (#672). From f9bcc64c32126b1cb01738199002fc2d26f7fab5 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 17 May 2026 06:39:01 -0500 Subject: [PATCH 2/8] docs(CHANGES) Copy improvements Carry the wrapper-to-tmux-support pattern from the earlier 0.56-section copy-improvements commit into the 0.57.x section. --- CHANGES | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 04f1f42c9..bc609eebe 100644 --- a/CHANGES +++ b/CHANGES @@ -45,7 +45,7 @@ $ uvx --from 'libtmux' --prerelease allow python _Notes on the upcoming release will go here._ -libtmux 0.57.0 broadens the supported surface. It introduces +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 @@ -618,7 +618,7 @@ attribute. When set, ``str(exc)`` prefixes the originating tmux command name (e.g. ``"last-window: no such window"``), giving downstream consumers a stable way to dispatch on which tmux command produced the error. {func}`~libtmux.common.raise_if_stderr` is the -shared helper used across the supported surface to populate it. +shared helper most commands use to populate it. #### Scope-aware format-token retrieval (#670) @@ -671,7 +671,7 @@ pane." See {ref}`format-tokens` for details. {meth}`~libtmux.Window.display_message`, and {meth}`~libtmux.Pane.display_message` raise {exc}`~libtmux.exc.LibTmuxException` when tmux reports an error, - matching the rest of the supported surface (#670). + matching the rest of libtmux's tmux commands (#670). - {attr}`~libtmux.Server.clients` and {meth}`~libtmux.Server.search_sessions` propagate tmux errors rather than silently returning an empty From e732b1bec37ea77af3a84dbe262ffda01741070b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 17 May 2026 09:09:13 -0500 Subject: [PATCH 3/8] docs(CHANGES,MIGRATION[breaking]): Flag str(exc) prefix change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: In 0.57 the typed wrappers migrated to raise_if_stderr, which attaches a LibTmuxException.subcommand attribute and prefixes str(exc) with ": ". The release entry framed this purely as additive — there was no breaking-change subheading for upgraders who pattern-match on str(exc) exactly or anchor a regex with ^. The wrapped stderr is still in exc.args[0]; the subcommand name is exposed as a typed attribute. Substring containment and unanchored regex matches keep working. what: - CHANGES: new ### Breaking changes subsection under 0.57 with three migration paths (exc.subcommand, exc.args[0], substring match). - MIGRATION: new ## libtmux 0.57.0 section covering the same contract from the upgrader's perspective, with before/after code for each migration path. --- CHANGES | 46 ++++++++++++++++++++++++++++++++++++++++++++++ MIGRATION | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/CHANGES b/CHANGES index bc609eebe..aaf3a3c8f 100644 --- a/CHANGES +++ b/CHANGES @@ -121,6 +121,52 @@ Blanket retry policies for {exc}`~libtmux.exc.LibTmuxException` should exclude {exc}`~libtmux.exc.MultipleObjectsReturned`: they report deterministic missing or ambiguous lookups, not transient tmux failures. +### Breaking changes + +#### `LibTmuxException` string form gains a subcommand prefix (#670) + +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 #### Find where you are running (#714) diff --git a/MIGRATION b/MIGRATION index cbcf307f2..bfd480cf4 100644 --- a/MIGRATION +++ b/MIGRATION @@ -295,6 +295,53 @@ with warnings.catch_warnings(): result = pane.display_message("#{pane_id}", get_text=True) ``` +## libtmux 0.57.0: Subcommand-tagged exceptions (#670) + +### `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() +``` + ## libtmux 0.50.0: Unified Options and Hooks API (#516) ### New unified options API From 5c6380ea8aab2579148cad80dc9fd4756c3ac53a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 17 May 2026 09:11:29 -0500 Subject: [PATCH 4/8] Client(feat[attached_*]): Add typed live-attachment properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: client.session_id / window_id / pane_id are hydrated from tmux's downward format cascade at the moment the Client dataclass is built and go stale as soon as the client switches view. The existing class-level docstring warning isn't enough on its own — users iterating over server.clients still reach for the raw fields and treat them as identity. what: - Add Client.attached_session / .attached_window / .attached_pane. Each property re-reads list-clients before resolving and returns the live typed Session / Window / Pane (or None), mirroring the Session.active_window fresh-lookup convention. - Tighten the Client class-level warning to point at the new properties as the safe accessors. - Tests: typed resolution, fresh window tracking (selects a new active window post-hydration and asserts the property reflects it — proves the property re-queries rather than returning the snapshot), pane resolution, None propagation when session_id is absent. - CHANGES: extend the Client what's-new entry to mention the attached_* accessors. - MIGRATION: 0.57 section gains a "snapshots, not identity" subheading covering the snapshot vs. live access pattern. --- MIGRATION | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/MIGRATION b/MIGRATION index bfd480cf4..a57dcf3e5 100644 --- a/MIGRATION +++ b/MIGRATION @@ -342,6 +342,35 @@ except LibTmuxException as exc: 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 typed object or None) +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. + ## libtmux 0.50.0: Unified Options and Hooks API (#516) ### New unified options API From bef17eb452c520fb2580cb37758a42cda4e2b2cc Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 17 May 2026 09:42:14 -0500 Subject: [PATCH 5/8] docs(Client): Clarify live attachment lookup semantics why: The Client documentation should distinguish attached_* convenience behavior from explicit refresh lookups. what: - Clarify None behavior for missing live client rows - Preserve refresh/from_client_name missing-object semantics --- MIGRATION | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/MIGRATION b/MIGRATION index a57dcf3e5..cb19b8d3a 100644 --- a/MIGRATION +++ b/MIGRATION @@ -362,14 +362,18 @@ For typed access that reflects the client's *live* attachment, use 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 typed object or None) +# 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 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. ## libtmux 0.50.0: Unified Options and Hooks API (#516) From 4671a4b5ab276d5b87fbabf81892c2e5261ee9fb Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 17 May 2026 13:32:54 -0500 Subject: [PATCH 6/8] Server,Window,Pane(refactor[display_message]): Warn on tmux stderr instead of raising MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: tmux's stderr from display-message conflates genuine argument-parser errors (e.g. -F-with-positional rejection) with operational quirks like 3.2a's control-mode dispatch path silently failing without emitting stderr at all. Raising LibTmuxException on every stderr forced an in-branch workaround — pytest.skip patches gated on has_gte_version("3.3") — that masked the underlying mismatch instead of solving it. Switch to warnings.warn so callers see the stderr without losing the return value, and the eventual raise/per-call-opt-in contract can land in a follow-up shipment that exercises real tmux versions end-to-end. what: - src/libtmux/server.py, src/libtmux/window.py, src/libtmux/pane.py: replace raise_if_stderr(proc, "display-message") with warnings.warn("display-message: …", stacklevel=2). Wrapper return value unchanged on success and on warn paths. - All three display_message docstrings gain a Notes block describing the warn-not-raise contract and showing the warnings.catch_warnings/filterwarnings("error") escalation pattern. - tests/test_pane.py, tests/test_window.py, tests/test_server.py: rename test_*_display_message_raises_on_tmux_error to test_*_display_message_warns_on_tmux_error and switch to pytest.warns(UserWarning, match=…). Drop the 3.2a control-mode skip added by the prior commit on test_server_display_message_no_text_returns_none — with warn-not-raise the 3.2a control-mode stderr no longer fails the test (the test only asserts result is None on get_text=False). - CHANGES: rewrite the display_message Fixes entry to describe the warn contract and how to escalate. - MIGRATION: add a new section under 0.57.0 documenting the warn contract and the warnings.catch_warnings escalation pattern. - MIGRATION: add a section noting that Pane.reset now dispatches via self.server.cmd; mocks targeting pane.cmd no longer intercept reset. - docs/topics/pane_interaction.md: tighten the capture_pane(pending=True) wording to describe tmux's parser pending buffer rather than "slow consumer / paused program" (the latter framing implies a PTY/app buffering issue that pending= doesn't address). - docs/topics/filtering.md: note that there is no search_clients(); filter via Server.clients and Python-side QueryList.filter. --- CHANGES | 9 ++++++--- MIGRATION | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index aaf3a3c8f..923ffb8c2 100644 --- a/CHANGES +++ b/CHANGES @@ -715,9 +715,12 @@ pane." See {ref}`format-tokens` for details. (#650). - {meth}`~libtmux.Server.display_message`, {meth}`~libtmux.Window.display_message`, and - {meth}`~libtmux.Pane.display_message` raise - {exc}`~libtmux.exc.LibTmuxException` when tmux reports an error, - matching the rest of libtmux's tmux commands (#670). + {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 diff --git a/MIGRATION b/MIGRATION index cb19b8d3a..5ec2c0b72 100644 --- a/MIGRATION +++ b/MIGRATION @@ -375,6 +375,38 @@ the client's *stable* identifier and does not have this caveat. The {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 `` 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) ### New unified options API From bd4dbb24ba383bcdd40e73f1bcab2711cd30c72a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 16 May 2026 07:17:26 -0500 Subject: [PATCH 7/8] neo(feat[fields]): Declare format-token fields from tmux master MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: tmux master (post-3.6a) registers eight new format tokens that the next tmux release will ship: pane_zoomed_flag, pane_floating_flag, pane_flags, pane_pb_state, pane_pb_progress, pane_pipe_pid, synchronized_output_flag, bracket_paste_flag. Declaring them now means libtmux is ready when the tag lands; older tmux releases expand unknown tokens to empty strings, so the fields stay None until the user upgrades tmux. what: - src/libtmux/neo.py: add the fields alphabetically within the existing Obj layout (pane_* tokens among the pane_* block, bracket_paste_flag near buffer_*, synchronized_output_flag near start_time). - tests/test_pane.py: parametrized test asserts each field is declared on the dataclass and hydrates either as None or as a string after refresh(). No runtime-value assertions — those will activate when the shipping tmux release exposes the tokens. --- tests/test_pane.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_pane.py b/tests/test_pane.py index 416032ce6..436441fa7 100644 --- a/tests/test_pane.py +++ b/tests/test_pane.py @@ -646,6 +646,39 @@ def test_pane_format_field_declared_and_hydrated( assert value is None or isinstance(value, str) +NEW_TMUX_FORMAT_FIELDS = ( + # Pane-scope tokens registered in tmux master post-3.6a. + "pane_zoomed_flag", + "pane_floating_flag", + "pane_flags", + "pane_pb_state", + "pane_pb_progress", + "pane_pipe_pid", + # Server-scope tokens. + "synchronized_output_flag", + "bracket_paste_flag", +) + + +@pytest.mark.parametrize("field_name", NEW_TMUX_FORMAT_FIELDS) +def test_obj_declares_post_3_6a_field(field_name: str, session: Session) -> None: + """Tmux's post-3.6a format tokens have typed slots on ``Obj``. + + Older tmux releases that don't recognize these tokens expand them to + the empty string, so the wrapper hydrates the rest of the dataclass + normally and the new fields stay ``None``. When the user upgrades + tmux, ``refresh()`` populates the fields automatically — no library + update required. + """ + pane = session.active_window.active_pane + assert pane is not None + assert field_name in pane.__dataclass_fields__ + + pane.refresh() + value = getattr(pane, field_name) + assert value is None or isinstance(value, str) + + def test_pane_synchronized_reflects_window_state(session: Session) -> None: """``pane.pane_synchronized`` flips when synchronize-panes toggles.""" window = session.active_window From 9e927a49dd53eebecf034ac6087fc4e004c794c0 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 16 May 2026 10:48:18 -0500 Subject: [PATCH 8/8] neo(feat[fields]): Re-expose version-gated tokens added after tmux 3.2a MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: with the scope+version gating in place, the format string sent to older tmux versions automatically excludes tokens that those versions don't recognize. The tokens below first registered in tmux 3.4-3.6 — tagging them with FIELD_VERSION makes them appear on supported tmux releases that include them, and absent on older tmux without sending unknown tokens that bloat the format string or trigger crashes. what: - src/libtmux/neo.py: - Re-add fields to Obj alphabetically: pane_key_mode, pane_unseen_changes, session_active, session_activity_flag, session_alert, session_bell_flag, session_silence_flag, client_theme. - Populate FIELD_VERSION with each token's minimum tmux release (3.4 for pane_unseen_changes, 3.5 for pane_key_mode, 3.6 for the five session_* tokens and client_theme). - tests/test_pane.py: restore pane_key_mode and pane_unseen_changes in PANE_FORMAT_FIELDS (the parametrized declaration+hydration test). - tests/test_session.py: restore the new session_* entries in SESSION_FORMAT_FIELDS. Verification: - On tmux 3.6a (local), all tokens hydrate via refresh(); tests pass. - On tmux 3.2a, FIELD_VERSION skips all 8 — the -F template stays at its pre--rollback shape for that version. Version anchors verified via: rg '""' https://github.com/tmux/tmux/blob//format.c across 3.2a, 3.3a, 3.4, 3.5, 3.5a, 3.6, 3.6a. --- tests/test_pane.py | 2 ++ tests/test_session.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/tests/test_pane.py b/tests/test_pane.py index 436441fa7..4059c0c26 100644 --- a/tests/test_pane.py +++ b/tests/test_pane.py @@ -613,6 +613,7 @@ def test_send_keys_flag_only_requires_a_flag(session: Session) -> None: "pane_format", "pane_in_mode", "pane_input_off", + "pane_key_mode", "pane_last", "pane_marked", "pane_marked_set", @@ -620,6 +621,7 @@ def test_send_keys_flag_only_requires_a_flag(session: Session) -> None: "pane_path", "pane_pipe", "pane_synchronized", + "pane_unseen_changes", ) diff --git a/tests/test_session.py b/tests/test_session.py index f7d95e4cc..6b7adb77c 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -709,12 +709,17 @@ def test_session_search_panes_filter_by_id(session: Session) -> None: SESSION_FORMAT_FIELDS = ( + "session_active", + "session_activity_flag", + "session_alert", + "session_bell_flag", "session_format", "session_group_attached_list", "session_group_many_attached", "session_grouped", "session_many_attached", "session_marked", + "session_silence_flag", )