Increase tmux coverage: Client, typed fields, Native filtering#672
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #672 +/- ##
==========================================
+ Coverage 47.02% 51.26% +4.23%
==========================================
Files 23 25 +2
Lines 3296 3482 +186
Branches 709 686 -23
==========================================
+ Hits 1550 1785 +235
- Misses 1384 1403 +19
+ Partials 362 294 -68 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
why: PR #672's CI matrix on tmux 3.2a crashed when the -F template included tokens that don't apply to the calling list-* subcommand or don't exist in the running tmux's format_table. The empirical crashers were 11 client_* tokens queried during list-windows (no client context) and several post-3.2a tokens that contributed cumulative risk. what: - src/libtmux/neo.py: - Add SCOPES_BY_LIST_CMD dict mapping each list-* to the set of token scopes its format engine can resolve (e.g. list-windows reaches universal + session + window; list-clients reaches universal + session + client). - Add FIELD_VERSION dict (initially empty) mapping field name → min tmux version; fields absent from the dict default to the project's floor (3.2a). - Add _SCOPE_PREFIXES table and _token_scope() helper that derive a token's scope from its name prefix (pane_*, window_*, session_*, client_*, buffer_*, etc.). Runtime-only tokens (mouse_*, cursor_*, selection_*, copy_cursor_*, popup_*) resolve to "event" and are excluded from all list-* templates. - Add _UNIVERSAL_TOKENS frozenset for cross-scope tokens without a scope prefix (pid, version, host, host_short, socket_path, etc.). - Add _normalize_tmux_version() helper that treats tmux master as a sentinel "newer than any tagged release" for comparison. - Rewrite get_output_format() to take (list_cmd, tmux_version) and filter the field set accordingly. Cached via @functools.cache on the small number of (list_cmd, version) combinations a process sees. - Rewrite parse_output() to take the same args so it reads the same filtered field order. - Thread the live tmux version through fetch_objs() via get_version(server.tmux_bin) before calling get_output_format(), pass through to parse_output() per line. - Doctests on the helpers and on get_output_format / parse_output demonstrate the new contracts. No Obj field changes in this commit. The 27 fields rolled back during the prior CI bisect remain absent — they re-enter in follow-up commits that exercise the new scope/version gating.
…3.2a
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 8 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 8 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 5 new session_* entries in
SESSION_FORMAT_FIELDS.
Verification:
- On tmux 3.6a (local), all 8 tokens hydrate via refresh(); 1182 tests
pass.
- On tmux 3.2a, FIELD_VERSION skips all 8 — the -F template stays at
its pre-PR-#672-rollback shape for that version.
Version anchors verified via:
rg '"<token>"' ~/study/c/tmux-3.<N>a/format.c
across 3.2a, 3.3a, 3.4, 3.5, 3.5a, 3.6, 3.6a.
why: PR #672's CI matrix on tmux 3.2a crashed when the -F template included tokens that don't apply to the calling list-* subcommand or don't exist in the running tmux's format_table. The empirical crashers were 11 client_* tokens queried during list-windows (no client context) and several post-3.2a tokens that contributed cumulative risk. what: - src/libtmux/neo.py: - Add SCOPES_BY_LIST_CMD dict mapping each list-* to the set of token scopes its format engine can resolve (e.g. list-windows reaches universal + session + window; list-clients reaches universal + session + client). - Add FIELD_VERSION dict (initially empty) mapping field name → min tmux version; fields absent from the dict default to the project's floor (3.2a). - Add _SCOPE_PREFIXES table and _token_scope() helper that derive a token's scope from its name prefix (pane_*, window_*, session_*, client_*, buffer_*, etc.). Runtime-only tokens (mouse_*, cursor_*, selection_*, copy_cursor_*, popup_*) resolve to "event" and are excluded from all list-* templates. - Add _UNIVERSAL_TOKENS frozenset for cross-scope tokens without a scope prefix (pid, version, host, host_short, socket_path, etc.). - Add _normalize_tmux_version() helper that treats tmux master as a sentinel "newer than any tagged release" for comparison. - Rewrite get_output_format() to take (list_cmd, tmux_version) and filter the field set accordingly. Cached via @functools.cache on the small number of (list_cmd, version) combinations a process sees. - Rewrite parse_output() to take the same args so it reads the same filtered field order. - Thread the live tmux version through fetch_objs() via get_version(server.tmux_bin) before calling get_output_format(), pass through to parse_output() per line. - Doctests on the helpers and on get_output_format / parse_output demonstrate the new contracts. No Obj field changes in this commit. The 27 fields rolled back during the prior CI bisect remain absent — they re-enter in follow-up commits that exercise the new scope/version gating.
…3.2a
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 8 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 8 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 5 new session_* entries in
SESSION_FORMAT_FIELDS.
Verification:
- On tmux 3.6a (local), all 8 tokens hydrate via refresh(); 1182 tests
pass.
- On tmux 3.2a, FIELD_VERSION skips all 8 — the -F template stays at
its pre-PR-#672-rollback shape for that version.
Version anchors verified via:
rg '"<token>"' ~/study/c/tmux-3.<N>a/format.c
across 3.2a, 3.3a, 3.4, 3.5, 3.5a, 3.6, 3.6a.
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. Scope: commit Verified across 5 review passes (CLAUDE.md compliance, shallow bug scan, git history of the scope gate, prior PR comments on 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. Scope: commit Verified across 5 review passes (CLAUDE.md compliance, shallow bug scan, git history of One scoping nuance was considered (autouse fixture flushes cache for all tests in 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
why: PR #672's CI matrix on tmux 3.2a crashed when the -F template included tokens that don't apply to the calling list-* subcommand or don't exist in the running tmux's format_table. The empirical crashers were 11 client_* tokens queried during list-windows (no client context) and several post-3.2a tokens that contributed cumulative risk. what: - src/libtmux/neo.py: - Add SCOPES_BY_LIST_CMD dict mapping each list-* to the set of token scopes its format engine can resolve (e.g. list-windows reaches universal + session + window; list-clients reaches universal + session + client). - Add FIELD_VERSION dict (initially empty) mapping field name → min tmux version; fields absent from the dict default to the project's floor (3.2a). - Add _SCOPE_PREFIXES table and _token_scope() helper that derive a token's scope from its name prefix (pane_*, window_*, session_*, client_*, buffer_*, etc.). Runtime-only tokens (mouse_*, cursor_*, selection_*, copy_cursor_*, popup_*) resolve to "event" and are excluded from all list-* templates. - Add _UNIVERSAL_TOKENS frozenset for cross-scope tokens without a scope prefix (pid, version, host, host_short, socket_path, etc.). - Add _normalize_tmux_version() helper that treats tmux master as a sentinel "newer than any tagged release" for comparison. - Rewrite get_output_format() to take (list_cmd, tmux_version) and filter the field set accordingly. Cached via @functools.cache on the small number of (list_cmd, version) combinations a process sees. - Rewrite parse_output() to take the same args so it reads the same filtered field order. - Thread the live tmux version through fetch_objs() via get_version(server.tmux_bin) before calling get_output_format(), pass through to parse_output() per line. - Doctests on the helpers and on get_output_format / parse_output demonstrate the new contracts. No Obj field changes in this commit. The 27 fields rolled back during the prior CI bisect remain absent — they re-enter in follow-up commits that exercise the new scope/version gating.
…3.2a
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 8 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 8 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 5 new session_* entries in
SESSION_FORMAT_FIELDS.
Verification:
- On tmux 3.6a (local), all 8 tokens hydrate via refresh(); 1182 tests
pass.
- On tmux 3.2a, FIELD_VERSION skips all 8 — the -F template stays at
its pre-PR-#672-rollback shape for that version.
Version anchors verified via:
rg '"<token>"' https://github.com/tmux/tmux/blob/<TAG>/format.c
across 3.2a, 3.3a, 3.4, 3.5, 3.5a, 3.6, 3.6a.
3b8f6e2 to
0312572
Compare
why: Every 0.57.0 deliverable in CHANGES (and the MIGRATION header) was tagged `(#670)`. Verified upstream: PR #670 does not exist on tmux-python/libtmux. The actual branch PR is #672 ("Increase tmux coverage: Client, typed fields, C-side filter"). The `(#670)` refs appear to be from an earlier draft of the branch that never opened. what: - `sed 's/(#670)/(#672)/g'` across CHANGES and MIGRATION. Verified by `gh pr view` that #672 is open on the upstream and matches the branch's scope; #670 was a 404. The two pre-existing `(#672)` refs under `### Fixes` are unchanged.
why: Every 0.57.0 deliverable in CHANGES (and the MIGRATION header) was tagged `(#670)`. Verified upstream: PR #670 does not exist on tmux-python/libtmux. The actual branch PR is #672 ("Increase tmux coverage: Client, typed fields, C-side filter"). The `(#670)` refs appear to be from an earlier draft of the branch that never opened. what: - `sed 's/(#670)/(#672)/g'` across CHANGES and MIGRATION. Verified by `gh pr view` that #672 is open on the upstream and matches the branch's scope; #670 was a 404. The two pre-existing `(#672)` refs under `### Fixes` are unchanged.
why: Every 0.57.0 deliverable in CHANGES (and the MIGRATION header) was tagged `(#670)`. Verified upstream: PR #670 does not exist on tmux-python/libtmux. The actual branch PR is #672 ("Increase tmux coverage: Client, typed fields, C-side filter"). The `(#670)` refs appear to be from an earlier draft of the branch that never opened. what: - `sed 's/(#670)/(#672)/g'` across CHANGES and MIGRATION. Verified by `gh pr view` that #672 is open on the upstream and matches the branch's scope; #670 was a 404. The two pre-existing `(#672)` refs under `### Fixes` are unchanged.
Code reviewFound 1 issue:
🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
why: Every 0.57.0 deliverable in CHANGES (and the MIGRATION header) was tagged `(#670)`. Verified upstream: PR #670 does not exist on tmux-python/libtmux. The actual branch PR is #672 ("Increase tmux coverage: Client, typed fields, C-side filter"). The `(#670)` refs appear to be from an earlier draft of the branch that never opened. what: - `sed 's/(#670)/(#672)/g'` across CHANGES and MIGRATION. Verified by `gh pr view` that #672 is open on the upstream and matches the branch's scope; #670 was a 404. The two pre-existing `(#672)` refs under `### Fixes` are unchanged.
Code reviewFound 1 issue:
🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Carry the wrapper-to-tmux-support pattern from the earlier 0.56-section copy-improvements commit into the 0.57.x section.
why: In 0.57 the typed wrappers migrated to raise_if_stderr, which attaches a LibTmuxException.subcommand attribute and prefixes str(exc) with "<subcommand>: ". 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.
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.
why: The live attachment helpers promise None when a stored client no longer resolves through tmux list-clients. what: - Translate missing client refreshes to None for attached_session - Cover real control-mode detach behavior for attached_* properties
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
…e for 0.57.0 surface
why: The autodoc layer documents the new public symbols, but Client (view vs. identity), scope+version-gated typed fields, and native filter predicates each introduce a mental model that needs a topic-page home.
what:
- Add docs/topics/clients.md covering the Client view-vs-identity distinction, attached_session/window/pane live lookup, and None-on-detach semantics
- Add docs/topics/format-tokens.md explaining the two gates (scope and tmux version), the downward format_defaults cascade, the per-release compatibility table, and how to introspect via get_output_format
- Expand docs/topics/filtering.md with a (native-filtering)= section covering Python-side vs native trade-offs, predicate shapes, the silent-zero-match diagnostic recipe, and when to prefer which
- Add Client to docs/topics/architecture.md hierarchy diagram, table, core objects, and module map
- Add cross-links from docs/topics/traversal.md to native-filtering and from docs/topics/pane_interaction.md to capture_pane(pending=True) and send_keys(cmd=None)
- Wire the two new pages into docs/topics/index.md grid and toctree
- Rename autodoc anchor (clients)= to (api-clients)= in docs/api/libtmux.client.md so the conceptual page owns the readable {ref}\`clients\`
… docs why: The 0.57.0 breaking-change docs in CHANGES and MIGRATION promise str(exc) renders as "<subcommand>: <stderr>" and that exc.args[0] holds the raw stderr string. raise_if_stderr passed proc.stderr (a list[str]) directly to LibTmuxException, so Exception.__str__ rendered the list's repr — yielding "last-window: ['no last window']" instead of the documented "last-window: no last window". The documented migration code `exc.args[0] == "can't find window"` was always False because exc.args[0] was the list, not the string. what: - Pass "\n".join(proc.stderr) to LibTmuxException so the message is a string. Multi-line tmux stderr renders as a multi-line string, matching how Python typically surfaces subprocess errors. - Add test_raise_if_stderr_str_shape_exact that asserts the FULL str(exc) and exc.args shapes (no startswith, no substring) for a wrapper flowing through raise_if_stderr, so future drift surfaces as a test failure rather than a docs lie.
why: The two-call form raced under busy pane writers: send-keys -R
clears the visible grid (verified at ~/study/c/tmux/cmd-send-keys.c:225
→ input.c:923 → screen-write.c:335) and any output landing between
the two subprocess.Popen invocations could scroll into history via
scroll-on-clear (tmux's default), then be wiped by the second call's
clear-history. That destroyed output the caller produced after the
reset point — `reset()` should wipe state at reset time, not whatever
happens to be in the grid when clear-history runs.
A naïve one-call form `self.cmd("send-keys", "-R", ";",
"clear-history")` doesn't work either: Pane.cmd auto-injects
`-t <pane_id>` only before the first subcommand, so the `;` separator
leaves clear-history routed to tmux's cmdq default pane — empirically
verified on tmux 3.6a to clear the wrong pane.
what:
- Route through self.server.cmd to bypass Pane.cmd's auto-target, and
pass `-t self.pane_id` explicitly on both subcommands so the `;`
separator can't misroute clear-history.
- Update the docstring to describe the single-IPC semantics and why
the explicit double-targeting is necessary.
- Add test_pane_reset_targets_non_active_pane that calls reset() on a
non-active pane and asserts history_size goes to 0 on the target
while the active sibling pane's history_size is preserved. Under
the misroute bug, clear-history would have hit the active sibling
instead of the target.
…rror why: `Client` inherits `client_name: str | None` from `Obj`. The `assert isinstance(self.client_name, str)` line vanishes under `python -O`, letting `None` flow into `_refresh` and surfacing as a less-clear downstream error. Keep the failure loud regardless of optimization level. what: - Replace the assertion with an explicit `if self.client_name is None: raise ValueError(...)`, with the message documented in the Raises section of the docstring. - Add test_client_refresh_raises_when_client_name_is_none asserting the explicit raise.
why: The fall-through `return "universal"` was fail-open — a future `Obj` field added without a matching prefix, override, or known-token entry would silently classify as universal and ship under every `list-*` -F template. That defeats the scope-gating machinery on exactly the case it's meant to protect: a future token whose class hasn't been mapped, where emitting on tmux 3.2a may crash the format engine or hydrate as nonsense. Pre-flight confirmed: every currently-declared `Obj` field maps to a known scope, so the flip is a no-op for the runtime template but turns future drift into a deterministic test failure. what: - Change the final return in `_token_scope` from `"universal"` to `"unknown"`. `"unknown"` is absent from every SCOPES_BY_LIST_CMD entry, so an unclassified field is excluded from every list-cmd template. - Document the fail-closed default in the docstring and show what it returns for an unrecognized name. - Add test_token_scope_unknown_for_unclassified_field asserting the default and that `"unknown"` isn't in any allowed scope set. - Add test_every_obj_field_classifies_to_known_scope as a guard: any new field added to Obj without classification breaks this test with a message pointing to the right table to update.
…s the triple
why: Code that needs all three of session/window/pane for a client
("where is this client attached *now*") naturally reads
client.attached_session, then client.attached_window, then
client.attached_pane. Each property re-reads tmux on access, so the
sequence costs three list-clients refreshes for one conceptual
read. The new helper shares a single refresh across the triple and
returns the three values together.
The helper catches NoActiveWindow and falls back to
(session, None, None). MultipleActiveWindows propagates — that
indicates a tmux invariant violation that callers should surface,
not absent attachment.
what:
- Add internal Client._resolve_attached returning
tuple[Session | None, Window | None, Pane | None], with documented
contract for the (None, None, None), (session, None, None), and
full-triple cases.
- Update the class docstring to point readers at the helper for
all-three access.
- Add three regression tests: live attachment → full triple,
detach → (None, None, None), and a monkeypatch-driven
NoActiveWindow → (session, None, None).
- attached_session / attached_window / attached_pane stay unchanged
so per-access live semantics are preserved.
why: The 0.57.0 entry covered the LibTmuxException subcommand prefix + subcommand attribute under both `### Breaking changes` and `### What's new` -> `#### Subcommand-tagged exceptions`. The breaking-changes subsection already documents the behavior, migration path, and rationale; the duplicate `####` heading restates the same content without adding new information. Per CLAUDE.md's "deliverable test," each `####` heading should be a distinct deliverable in user vocabulary — this failed it. what: - Remove the `#### Subcommand-tagged exceptions (#670)` block from `### What's new`. The breaking-changes section at CHANGES:59-101 is unchanged and remains the canonical reference for this deliverable.
why: MIGRATION's "Upcoming Release" header sat above an already-drafted 0.57.0 section without comment-bracket delimiters, so future-release content didn't have an unambiguous insertion point. CHANGES uses `<!-- KEEP THIS PLACEHOLDER -->` / `<!-- END PLACEHOLDER -->` to mark where new entries land; MIGRATION should match for the same reason. what: - Wrap the placeholder body in matching HTML comment brackets, mirroring the CHANGES convention. - New release content for the upcoming version lands below the END marker.
why: The doctest sent a key sequence then immediately read the pane's scrollback to assert the marker landed. Without `retry_until`, this trusted that the shell echoed before capture — a coin-flip under parallel-test load. The dedicated functional test in tests/test_pane.py::test_pane_reset_clears_history_and_sends_reset already exercises the same path with retry_until; the doctest's responsibility is to demonstrate the API, not to re-test timing. what: - Drop the send_keys + immediate capture_pane lines from the doctest. - Keep the call + return-value check, which is timing-independent.
why: The filtering topic doc already covers when to pick `search_*()` (native push-down) over `QueryList.filter()` (Python-side, post-fetch) with a comparison table and "When to prefer which" guidance, but the six `search_*` API entry points don't reference it. A caller landing on `Server.search_panes` from autodoc has no path to discover the comparison or the unfiltered `panes` attribute. what: - Add a See Also section to each `search_*` method (Server x3, Session x2, Window x1). Each block cross-links to (a) the matching unfiltered `panes` / `windows` / `sessions` attribute and (b) the `native-filtering` ref label in docs/topics/filtering.md.
…ic helper why: The typed wrappers (`Server.search_*`, `Session.search_*`, `Window.search_panes`, `Server.list_buffers`) all carry a warning that tmux silently expands a malformed `-f` predicate to empty — indistinguishable from "no matches". `fetch_objs` is the documented public surface those wrappers route through, but its docstring didn't carry the same caveat. A caller using `fetch_objs(filter=...)` directly missed the warning. what: - Copy the malformed-filter warning into the `fetch_objs(filter=)` parameter docstring with the same wording as the typed wrappers. - Cross-link to the `native-filtering` topic doc for the broader context.
why: Every 0.57.0 deliverable in CHANGES (and the MIGRATION header) was tagged `(#670)`. Verified upstream: PR #670 does not exist on tmux-python/libtmux. The actual branch PR is #672 ("Increase tmux coverage: Client, typed fields, native filter"). The `(#670)` refs appear to be from an earlier draft of the branch that never opened. what: - `sed 's/(#670)/(#672)/g'` across CHANGES and MIGRATION. Verified by `gh pr view` that #672 is open on the upstream and matches the branch's scope; #670 was a 404. The two pre-existing `(#672)` refs under `### Fixes` are unchanged.
…stead of raising
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.
why: Server.new_session() called get_output_format() with no args, which
defaults to ("list-panes", "3.2a") and gates out every typed Obj field
whose FIELD_VERSION entry exceeds 3.2a. On tmux 3.3+ the returned Session
silently missed pane_dead_signal and pane_dead_time, and any 3.4+ tokens
that join FIELD_VERSION in a follow-up shipment would have the same
gratuitous gap. Match the pattern used by fetch_objs: thread the live
tmux version through, and use list-sessions as the scope (the format
context for tmux's new-session -P -F is the freshly created session).
what:
- src/libtmux/server.py:
- Import get_version from libtmux.common.
- new_session() now derives tmux_version via get_version(tmux_bin=…)
and passes ("list-sessions", tmux_version) to both get_output_format
(template build) and parse_output (output parse). Pair must be
identical or the field order goes out of sync.
Verified: a new_session() on tmux 3.3+ now hydrates pane_dead_signal and
pane_dead_time on the returned Session, where master returned None
unconditionally.
why: With Server.clients and Server.search_sessions now propagating tmux errors via raise_if_stderr instead of swallowing them via try/except: pass, leaving Server.sessions on the legacy silent-empty path is internally inconsistent. A real list-sessions failure (subprocess crash, malformed output) should surface the same way for all three accessors — and code that does ``if not server.sessions:`` should be able to distinguish "no sessions" from "tmux crashed." what: - src/libtmux/server.py: drop the try/except wrapper around fetch_objs in Server.sessions; let errors propagate via the raise_if_stderr already inside fetch_objs. Match the list-comprehension shape used by Server.clients and Server.windows on this branch. - src/libtmux/server.py: add a Raises clause to the Server.sessions docstring documenting the new contract. - tests/test_server.py: add test_server_sessions_propagates_errors mirroring the existing clients/search_sessions tests. - CHANGES: fold Server.sessions into the breaking-changes entry so all three accessors are advertised together.
…ssion why: Client.refresh() was already converted to survive python -O (which strips assert statements). Pane.refresh(), Window.refresh(), and Session.refresh() carry the same pattern and are vulnerable to the same -O strip: under optimization the assert goes away and None flows into _refresh, surfacing as a less-clear downstream error. what: - src/libtmux/pane.py: replace assert isinstance(self.pane_id, str) with explicit if self.pane_id is None: raise ValueError(msg). Add Raises clause to the docstring. - src/libtmux/window.py: same for self.window_id. - src/libtmux/session.py: same for self.session_id. - tests/test_pane.py, tests/test_window.py, tests/test_session.py: add per-class test_<class>_refresh_raises_when_<id>_is_none mirroring test_client_refresh_raises_when_client_name_is_none. The Pane.window and Window.session property navigation asserts are unchanged — they're in relation-navigation paths, not refresh(), and out of scope for this targeted -O-safety fix.
why: The recent error-propagation work on Server.{sessions, clients,
search_sessions} raised LibTmuxException on every non-empty tmux
stderr — including tmux's "no server running on <socket>" and
"error connecting to <socket> (No such file or directory)" signals,
which are bootstrap states (socket exists but no daemon / socket
file missing), not error conditions. That regressed the historic
contract that a fresh Server can be safely introspected via
.sessions, .windows, .panes, .clients, .search_sessions before the
tmux daemon is up — surfaced by test_no_server_sessions,
test_no_server_attached_sessions, and the context_managers.md
doctest after the trim.
Real tmux errors (subprocess crash, malformed output,
version-incompatible flags) still propagate.
what:
- src/libtmux/server.py: add module-level _fetch_or_empty helper that
wraps fetch_objs and returns [] when the LibTmuxException's stderr
contains either of two daemon-not-up markers ("no server running",
"error connecting to"). Other LibTmuxException paths re-raise.
- src/libtmux/server.py: route all five Server collection accessors
(sessions, windows, panes, clients, search_sessions, plus
search_windows / search_panes for symmetry) through
_fetch_or_empty.
- src/libtmux/server.py: update Server.sessions Raises docstring to
note that no-daemon stays empty.
- CHANGES: extend the breaking-changes entry to explicitly call out
the no-daemon-stays-empty carve-out.
…ANGES
why: "C-side" leaks tmux's implementation language (C) into prose
about an architectural distinction — filter evaluated in tmux's
own format engine vs. re-filtered in Python. "Native" reads
cleanly as "tmux's own", parallels "Python-side", and avoids the
language conflation.
what:
- Rename the MyST anchor (c-side-filtering)= to (native-filtering)=
in docs/topics/filtering.md; update the 9 {ref} cites across
server.py, session.py, window.py, neo.py, clients.md,
traversal.md.
- Replace "tmux's C-side <noun>" with "tmux's native <noun>" in 7
docstring locations + 1 test docstring.
- Rename the filtering.md section heading "C-Side Filtering" to
"Native Filtering" and the comparison subsection "Python-side
vs. C-side" to "Python-side vs. native".
- Rename the CHANGES section heading and lead-paragraph mention to
match.
Net zero line count (24 inserted, 24 deleted) — pure prose rename.
No functional or type changes; pytest, ruff, ty, and the docs
build are unchanged from pre-rename.
why: PR #672 should describe shipped user behavior without branch-internal jargon, and real tmux connection failures must not be hidden as empty listings. Behavior: - Treat only missing/not-yet-started tmux sockets as empty listing results. Real connection failures (permission denied, ECONNRESET on a live socket, malformed daemon response) now propagate as LibTmuxException instead of being swallowed as an empty QueryList. - Regression coverage for the missing-socket versus permission-denied listing paths. Prose: - Replace "C-side filter" with "tmux-native filtering" in release notes, MIGRATION, topic docs, and API docstrings. - Replace "format-token hydration" with "format-token fields" in the same locations.
why: Tighten the internal implementation and documentation for daemon-not-up states. what: - src/libtmux/server.py: In-line missing-socket marker and simplify _is_daemon_not_up_error logic. - src/libtmux/server.py: Compress _fetch_or_empty and _is_daemon_not_up_error docstrings for clarity.
why: "hydration" is an internal mental model — users see the result, not the query mechanic. what: Replace 'hydration query' with 'initialization query' in the 0.53.1 entry.
Summary
libtmux 0.57.0 broadens tmux coverage. It introduces
Clientas a first-class typed object, threads tmux's C-side-ffilter through the typed listing methods so callers can push predicates into the tmux server, adds typed access to many more format tokens with scope-and-version-gated-Ftemplates, and propagates subcommand context throughLibTmuxExceptionso downstream tools can dispatch on which tmux command produced an error.What's new
Clientobject andServer.clientsaccessor. New typed dataclass for attached terminals.client_nameis the stable identifier;session_id/window_id/pane_idare attached-view snapshots hydrated via tmux's downwardformat_defaultscascade.attached_session/attached_window/attached_panere-readlist-clientsbefore resolving;attached_paneis session-scope (cascade), not the client'sCLIENT_ACTIVEPANEfocus.Server.display_messageandWindow.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. All threedisplay_messagewrappers (including the existingPane.display_message) surface tmux stderr viawarnings.warnrather than dropping it silently. Callers that want to escalate can wrap inwarnings.catch_warningswithfilterwarnings("error").-ffilter on typed listing methods.Server.search_panes/search_windows/search_sessionsplus theSessionandWindowanalogues take afilter=kwarg routed to tmux's-fflag. tmux evaluates the predicate and drops non-matching rows 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".-Ftemplates. The format string libtmux sends eachlist-*subcommand is now scope- and version-aware. ASessionrow hydrates active-window and active-pane fields via tmux's downward cascade; aClientrow likewise hydrates the client's attached session, window, and active pane. Tokens introduced after tmux 3.2a are gated throughFIELD_VERSIONso the-Ftemplate stays safe on the project's minimum supported tmux. Unknown tokens stayNoneon the typed surface — no crash, no warning.Pane/Window/Session/Clientformat-token fields for the 3.2a set. 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 client view (client_session,client_readonly,client_termtype…).Server.run_shell(cwd=, show_stderr=). New kwargs map to tmux's-c(3.4+) and-E(3.6+) flags. Older tmux warns and ignores the kwarg instead of erroring.Pane.send_keys(cmd=None, …)flag-only invocation. Passcmd=Nonetogether withreset=Trueorrepeat=Nto invoke tmux's flag-onlysend-keys -R/send-keys -N <n>form without any trailing key argument.Server.list_buffers(format_string=, filter=). Project a chosen-Ftemplate or push a buffer-name match expression through tmux's format engine.Pane.capture_pane(pending=True). Return bytes tmux has read from the pane but not yet committed to the terminal — useful for diagnosing programs whose output stalls mid-sequence.Fixes
Pane.reset()now clears pane scrollback. The history clear silently no-op'd in 0.56.0, leaving the scrollback intact (mcp(pane_tools): clear_pane does not reliably clear visible content #650).Breaking changes
LibTmuxExceptionstring form gains a subcommand prefix.str(exc)now begins with the originating tmux subcommand name followed by": ". An error fromSession.last_window()used to render as"can't find window"and now renders as"last-window: can't find window". The wrapped-stderr type also changed:exc.args[0]is nowstr(joined with"\n"when tmux emitted multiple stderr lines); previously it waslist[str]. Substring matches and unanchoredre.searchpatterns continue to work unchanged.Server.sessions,Server.clients, andServer.search_sessionsraise on tmux errors. Previously, a tmux command failure was swallowed by a bareexcept Exception: pass, returning an emptyQueryListindistinguishable from "no sessions/clients" or "filter matched nothing". The wrappers now letLibTmuxExceptionpropagate. Genuine empty results still return an emptyQueryList;"no server running"is still treated as an empty result, preserving the contract that a freshServercan be introspected before its daemon is up.Deferred to follow-up shipments
Objfields for tokens tmux added in 3.4 / 3.5 / 3.6 and the forward-looking set from tmux master. Held until tmux 3.7 reaches a tagged release.CLIENT_ACTIVEPANEflag.Client.attached_panecurrently follows the cascade (session's current window's active pane); the two diverge once a client has usedselect-pane -Pto set its own active pane.display_message(version-gated raise, per-callraise_on_stderrkwarg, or kept-permanent warn).Test plan
rm -rf docs/_build && uv run ruff format . && uv run ruff check . && uv run mypy src tests && uv run pytest --reruns 0 -vvv && just -f docs/justfile html— passes locally on tmux 3.6agh pr checks --watchRefs
Pane.resetscrollback)