tony · GitHub

added a commit that referenced this pull request

May 16, 2026
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.

tony added a commit that referenced this pull request

May 16, 2026
…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.

tony added a commit that referenced this pull request

May 16, 2026
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.

tony added a commit that referenced this pull request

May 16, 2026
…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.

@tony

@tony tony changed the title Close 14 wrapper gaps from #670 (libtmux 0.57.0) Close 13 of 14 wrapper gaps from #670 (libtmux 0.57.0)

May 16, 2026

@tony tony changed the title Close 13 of 14 wrapper gaps from #670 (libtmux 0.57.0) Increase tmux coverage: Client, typed fields, C-side filter

May 16, 2026

This was referenced

May 16, 2026

Merged

Closed

tony added a commit that referenced this pull request

May 17, 2026
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.

tony added a commit that referenced this pull request

May 17, 2026
…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.

@tony

tony added a commit that referenced this pull request

May 17, 2026
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.

tony added a commit that referenced this pull request

May 17, 2026
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.

@tony

This was referenced

May 17, 2026

Draft

Open

Open

tony added a commit that referenced this pull request

May 17, 2026
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.

@tony

@tony

tony added a commit that referenced this pull request

May 17, 2026
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: 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.

@tony

@tony

@tony

tony deleted the parity-pt-2 branch

May 18, 2026 02:36

tony added a commit to tmux-python/tmuxp that referenced this pull request

May 23, 2026
…e test
why: Verify libtmux's 0.57.0 parity branch doesn't regress tmuxp
before #672 merges. First sync caught a real regression:
cursor_x/y were filtered out of the list-panes -F template by
an overzealous prefix rule, causing _wait_for_pane_ready to
return True instantly. Fixed upstream in libtmux@78f425ab; this
PR pins to that commit.
what:
- Add [tool.uv.sources] pointing libtmux at parity-pt-2
- Regenerate uv.lock

tony added a commit to tmux-python/libtmux-mcp that referenced this pull request

Jul 11, 2026
…e test
why: Verify libtmux's 0.57.0 parity branch doesn't regress
libtmux-mcp before #672 merges. The branch's CHANGES entry for
LibTmuxException.subcommand specifically calls out
handle_tool_errors as a downstream consumer; this pin exercises
the wider 0.57 surface against our test matrix.
what:
- Add [tool.uv.sources] pointing libtmux at parity-pt-2
- Regenerate uv.lock

Read the original on github.com ↗