Close 14 wrapper gaps from #670 (libtmux 0.57.0)#672
Open
tony wants to merge 31 commits into
Open
Conversation
why: gap #1 of issue #670 — tmux's display-message entry uses CMD_FIND_CANFAIL so -t is optional, but libtmux only wrapped Pane.display_message. Server-scoped reads like #{version} / #{socket_path} had to drop to server.cmd("display-message", "-p", "#{...}") with no wrapper path. libtmux-mcp carries three workaround sites. what: - Add Server.display_message mirroring Pane.display_message's signature minus -t injection (Server.cmd never auto-injects -t). - Cover -p/-a/-v/-l/-N/-c/-d/-F flags; gate -l on tmux 3.4+. - Doctests demonstrate #{version} and all_formats=True usage. - Tests in tests/test_server.py use control_mode() so display-message has a client to dispatch -p output through (target/pane is unneeded but a client is needed for stdout to materialize). refs #670 (gap #1)
why: gap #2 of issue #670 — Pane.display_message exists but Window doesn't, forcing callers like libtmux-mcp's resize_pane(zoom=...) to drop down to window.cmd("display-message", "-p", "#{window_zoomed_flag}") to read window-scoped state. what: - Add Window.display_message mirroring Pane.display_message; Window.cmd auto-injects -t @<window-id>, so window-scoped reads (window_zoomed_flag, window_active_clients_list, …) work without a pane handle. - Cover -p/-a/-v/-l/-N/-c/-d/-F flags; gate -l on tmux 3.4+. - Doctests demonstrate #{window_id} and #{window_zoomed_flag} reads. - Tests in tests/test_window.py via WindowDisplayMessageCase NamedTuple (matches the Pane.display_message test shape). Includes a target_client case using control_mode(). refs #670 (gap #2)
why: gap #3 of issue #670 — tmux's format.c registers window_zoomed_flag as a first-class format token (callback at format.c:2854, table entry at format.c:3557), but the libtmux Obj dataclass never declared it. mypy rejected window.window_zoomed_flag even after refresh(). libtmux-mcp's resize_pane(zoom) workflow worked around this by going through display-message. what: - Add window_zoomed_flag: str | None = None to Obj in neo.py (alphabetically between window_width and wrap_flag). - Auto-included in the tmux -F format string via get_output_format(), so refresh() populates it without further wiring. - Test toggles zoom on/off via Pane.resize(zoom=True) across two refresh cycles and asserts "0"/"1" round-trip. The remaining 36 missing format tokens land in gap #10's commit; this one is the specific token issue #670's gap #3 called out. refs #670 (gap #3)
why: gap #4 of issue #670 (also tracked as #650). The previous body called self.cmd("send-keys", r"-R \; clear-history") which sends a single argv to tmux via subprocess. tmux's \; is the *interactive* command separator and is only interpreted when tmux re-lexes a full command line — argv never gets re-parsed. tmux saw "-R \; clear-history" as a single token after -R and treated "\; clear-history" as literal keys to send, never executing clear-history. The scrollback was never cleared. what: - Split reset() into two separate self.cmd("send-keys", "-R") and self.cmd("clear-history") calls. Each goes through Pane.cmd which auto- injects -t <pane-id>, so both target the right pane. - Update docstring (uses r""" because of the literal \; explanation), with a working doctest that populates history and verifies reset. - Test in tests/test_pane.py: spawn a shell pane, populate scrollback with "reset_marker_*" lines, call pane.reset(), assert the markers are gone from capture_pane(start=-100). Pre-fix this test would have failed (the markers stayed because clear-history never ran). closes #650 refs #670 (gap #4)
why: gap #5 of issue #670 — tmux's cmd-send-keys.c:223-225 deliberately handles count == 0 with -R or -N set, returning CMD_RETURN_NORMAL without sending keys. The wrapper at pane.py:619 always appended `prefix + cmd` to argv, so `pane.send_keys("", reset=True, enter=False)` produced `tmux send-keys -R ""` — not the flag-only path tmux explicitly supports. libtmux-mcp's clear_pane kept `pane.cmd("send-keys", "-R")` for that reason. what: - Make cmd Optional[str] with default None. The previous positional-required signature is preserved for every existing caller (they pass cmd as a string). - When cmd is None and copy_mode_cmd is None: emit `send-keys <flags>` with no trailing argv. Require at least one flag (reset, repeat, copy_mode_cmd); ValueError otherwise so degenerate `send_keys()` calls aren't silent no-ops. - Skip the post-call self.enter() in flag-only mode (no keys → no Enter). - Doctest demonstrates `pane.send_keys(reset=True)` working in flag-only mode. - Tests use monkeypatch+stub of pane.cmd (the pattern from test_server.py:730) to capture the exact argv: flag-only reset emits `("send-keys", "-R")`, flag-only repeat=3 emits `("send-keys", "-R", "-N", "3")`. A separate test asserts ValueError when no flags accompany cmd=None. refs #670 (gap #5)
why: gap #6 of issue #670 — tmux's cmd-list-buffers.c:39 declares `.args = { "F:f:", ... }`. The libtmux wrapper passed neither, so callers got tmux's default template `name: N bytes: "sample"` and had to regex-parse it. libtmux-mcp's buffer GC carried `server.cmd("list-buffers", "-F", ...)` for that reason. what: - Add format_string and filter kwarg to Server.list_buffers. Default behavior (template output) preserved for backward compat. - format_string follows the existing display_message convention (avoids shadowing Python's builtin `format`). filter shadows the builtin by design, with a per-line noqa: A002 + docstring note — it mirrors tmux's flag name for grep-friendly symmetry with the manual. - Doctests cover all three modes (default, format projection, filter predicate); tests exercise raw-name projection and C-side filter matching (e.g. `#{m:gap6match_*,#{buffer_name}}` returns only the matching names). refs #670 (gap #6)
…tmux C-side filter why: gap #7 of issue #670 — tmux's cmd-list-panes.c:41 accepts `[-f filter]` and evaluates `format_true(expanded)` at line 134, gating output server-side before any data is returned. libtmux's `panes` / `windows` / `sessions` properties return QueryList and force callers to filter post-hoc in Python — orders of magnitude slower than pushing the predicate into tmux's C code. libtmux-mcp's search_panes fast-path kept `server.cmd("list-panes", "-a", "-f", ...)` for that reason. Note: `list_panes()` / `list_windows()` / `list_sessions()` are already defined as deprecated raise-only stubs (since 0.17) with pinned legacy-API tests in `tests/legacy_api/`. Keep those intact and add the new methods under `search_*()` — matches the verb libtmux-mcp uses for its consuming endpoint and side-steps the legacy contract entirely. what: - Extend `fetch_objs` (neo.py:248) with `filter: str | None = None`. When set, append `-f <filter>` before the `-F` template. Single change feeds all the wrappers below. - Add `Server.search_sessions`, `Server.search_windows`, `Server.search_panes` alongside the existing `sessions`/`windows`/`panes` properties. - Add `Session.search_windows`, `Session.search_panes`. - Add `Window.search_panes`. - Each wrapper exposes a single `filter=` kwarg; the existing property is the no-filter form. Doctests demonstrate `#{m:gap7_*,#{window_name}}`-style predicates returning only matching objects. - Tests across test_server.py / test_session.py / test_window.py exercise filter-by-id (m:pane_id) and filter-by-name (m:prefix_*). refs #670 (gap #7)
…se_if_stderr helper why: gap #8 of issue #670, part 1 — wrappers like session.last_window raise exc.LibTmuxException(proc.stderr) and downstream consumers (libtmux-mcp's handle_tool_errors) lose the "which tmux command failed" context. Pre-0.56 the MCP built `f"tmux {subcommand} failed: ..."` manually. Split into two commits per planning direction: * 8a (this commit): add the surface — LibTmuxException.subcommand attribute and raise_if_stderr helper. Backward-compatible; no call-site changes yet. * 8b (next commit): mechanically migrate the ~12 existing raise sites to use raise_if_stderr. what: - LibTmuxException.__init__ accepts subcommand: str | None = None kwarg. Override __str__ to format as "<subcommand>: <stderr>" when set; otherwise preserves pre-0.57 output exactly. Verified backward-compat with a test that constructs exc with no kwarg and asserts no "subcommand:" prefix. - common.raise_if_stderr(proc, subcommand) consolidates the `if proc.stderr: raise exc.LibTmuxException(...)` pattern. common.py already imports `exc`, so no new import. Documented with versionadded marker and a working doctest. - Tests in tests/test_common.py cover both: the no-stderr no-op path (using session fixture for a started server) and the raises-with-tag path via list-clients against a fake session id. refs #670 (gap #8, part 1 of 2)
why: gap #8 of issue #670, part 2 — mechanically thread the subcommand tag through every wrapper that raises on tmux stderr. With 8a's surface in place (LibTmuxException.subcommand + raise_if_stderr helper), this commit applies the migration so every typed wrapper now produces an exception tagged with the originating tmux subcommand. what: - Replace every `if proc.stderr: raise exc.LibTmuxException(proc.stderr)` pair with `raise_if_stderr(proc, "<subcommand>")` across the wrapper surface: server.py (34 sites), session.py (11), window.py (14), pane.py (22). Plus one explicit site in neo.py for fetch_objs's underlying tmux_cmd invocation. - Migration was scripted with subcommand auto-extraction from the preceding `proc = …cmd("subcmd", …)` line; two unmapped sites (window.py's select_layout, neo.py's fetch_objs) migrated by hand. - Add raise_if_stderr import to every touched module via ruff isort. - New integration test in tests/test_session.py exercises the end-to-end tag: session.last_window() on a one-window session raises an exception with subcommand == "last-window" and str(exc) prefixed accordingly. Pre-commit gate's 1130-test full pytest run caught no `match=` regex regression — the new "<subcommand>: …" prefix didn't break any existing exception assertion. Doctests across docs/ also pass. refs #670 (gap #8, part 2 of 2)
why: gap #9 of issue #670. Server.cmd auto-injects -t <target> when the target= kwarg is set. A caller's own positional -t produces `tmux <sub> -t %1 -t %1`; tmux's args_get() applies last-wins so the positional -t is silently dropped. The 0.34 docstring at session.py:234 already documents this contract as ignored; this commit promotes the documented contract into a runtime DeprecationWarning so callers see the bug instead of getting silent no-ops. what: - Server.cmd: when target is not None and "-t" appears in *args, emit DeprecationWarning with stacklevel=3 so the warning surfaces at the caller (not the wrapper). - Migrate tests/test_common.py:51 to use target= kwarg (the modern path). - tests/legacy_api/test_common.py:204 now wraps the legacy call with pytest.warns(DeprecationWarning) to pin the new contract. - New tests in tests/test_server.py exercise both the warning fires (legacy shape) and the no-warning case (target= alone). - MIGRATION entry under "Upcoming Release" documents the deprecation, the migration path (target= kwarg), and the planned TypeError escalation. refs #670 (gap #9)
why: gap #10 of issue #670. tmux's format_table[] at format.c:3010-3563 registers 37 scope-relevant format tokens that ship in 3.6a; libtmux's hand-curated allowlist in neo.py declared only a subset. Commit b1f21132 added window_zoomed_flag specifically; this commit covers the remaining 36 so the typed dataclass surface matches what tmux exposes. what: - src/libtmux/neo.py: add 13 pane_* (pane_dead, pane_format, pane_in_mode, pane_input_off, pane_key_mode, pane_last, pane_marked, pane_marked_set, pane_mode, pane_path, pane_pipe, pane_synchronized, pane_unseen_changes), 12 window_* (window_active_clients_list, window_active_sessions_list, window_activity_flag, window_bell_flag, window_bigger, window_end_flag, window_flags, window_format, window_last_flag, window_silence_flag, window_start_flag, window_visible_layout), 11 session_* (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) fields. Alphabetical insertion preserves existing layout. Each as `str | None = None`; get_output_format() auto-includes them in the tmux -F template. - Tests: parametrized declaration + hydration tests per scope assert each field is registered on the dataclass and either None or a string after refresh(). On older tmux versions unknown tokens expand to empty strings, so older tmux still hydrates the rest of the fields fine. - Focused live tests: pane.pane_synchronized round-trips through tmux's synchronize-panes window option; window.window_flags is always a string. refs #670 (gap #10)
why: gap #11 of issue #670. tmux's format.c at lines 3041-3110 registers twelve client_* format tokens (client_activity, client_control_mode, client_created, client_last_session, client_mode_format, client_prefix, client_readonly, client_session, client_termfeatures, client_termtype, client_theme, client_utf8) that the libtmux Obj dataclass didn't declare, and Server.list_clients returned raw stderr-style strings instead of typed objects. Multi-client coordination, read-only detection, theme/termtype reads forced consumers down to server.cmd("list-clients", ...). what: - src/libtmux/neo.py: add the 12 missing client_* fields to Obj alphabetically; extend ListCmd Literal with "list-clients". - src/libtmux/client.py: new module with @dataclasses.dataclass class Client(Obj). refresh() uses obj_key="client_name", list_cmd="list-clients"; classmethod from_client_name() mirrors the Session.from_session_id shape. - src/libtmux/server.py: import Client; new Server.clients property returns QueryList[Client] via fetch_objs(list_cmd="list-clients"). - src/libtmux/__init__.py: export Client; add to __all__. - conftest.py: register Client in the doctest_namespace. - docs/api/libtmux.client.md: autodoc page. - docs/api/index.md: card + toctree entry; updated lead to mention Client. - tests/test_client.py: live tests using the control_mode() fixture exercise Server.clients listing, attached-session reporting, default readonly state, and refresh() rehydration. refs #670 (gap #11)
why: gap #12 of issue #670. tmux's cmd-run-shell.c at lines 156-162 reads two flags the wrapper didn't expose: -c sets the shell command's working directory (independent of any target pane's cwd) and -E sets JOB_SHOWSTDERR, which combines the command's stderr into the captured output stream. what: - Server.run_shell gains `cwd: StrPath | None = None` (maps to -c) and `show_stderr: bool | None = None` (maps to -E). Both default-None, no behavior change for existing callers. - Doctest demonstrates pwd in a custom cwd and stderr capture. - Tests: `test_run_shell_cwd` runs `pwd` with `cwd=tmp_path` and asserts the directory appears in output; `test_run_shell_show_stderr` runs a shell snippet that writes to both streams and asserts both are in the result. Both gated by has_gte_version("3.5") because run-shell stdout passthrough requires tmux 3.5. refs #670 (gap #12)
why: gap #13 of issue #670. tmux's cmd-capture-pane.c at lines 231-232 branches on -P to call cmd_capture_pane_pending, returning bytes tmux has buffered as input for the pane but the program hasn't consumed yet. Useful for diagnosing hung programs, copy-mode race conditions, and paste-buffer drains. The wrapper covered 12 of 13 flags from "ab:CeE:JMNpPqS:Tt:" but skipped -P. what: - Pane.capture_pane gains `pending: bool = False` kwarg, present on both overload signatures and the implementation. When True, the wrapper appends -P alongside -p so stdout still flows back as a list. - Docstring entry documents the distinction from the default capture (history vs unconsumed input). - Tests: argv-assertion test confirms -P is emitted via the monkeypatch+stub pattern; a smoke test confirms the return type is list[str] (whether tmux has bytes to return depends on live input pressure and isn't reliably reproducible). refs #670 (gap #13)
why: gap #14 of issue #670. 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 8 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. refs #670 (gap #14)
Document libtmux 0.57.0 per AGENTS.md changelog conventions: multi-sentence lead paragraph, #### deliverables under ### What's new, the documented Pane.reset fix under ### Fixes, the -t-in-args deprecation under ### Deprecations, and the new Client autodoc page and migration entry under ### Documentation. Every section describes ship-state user-visible behavior; the bug history for #650 stays scoped to ### Fixes where it is relevant to anyone upgrading from 0.56.0. Cross-links to autodoc'd APIs use {class}, {meth}, {attr}, {exc}, {func}, and {doc} roles so the changelog renders as live navigation in the docs site. refs #670
why: the 0.34 versionchanged block on Session.cmd documented "Passing
target by -t is ignored. Use target keyword argument instead." Verified
against tmux source: tmux's args_get() returns TAILQ_LAST(...) — last
value wins (~/study/c/tmux-3.6a/arguments.c). libtmux constructs argv as
["-t", str(target), *args] (server.py:345), placing the kwarg's -t value
FIRST and any positional -t value SECOND. Under last-wins, the
*positional* value wins, not the kwarg. The original rationale was
factually inverted — readers would build wrong mental models of
precedence.
what:
- Rewrite the .. versionchanged:: 0.34 block to describe the actual
behavior. User-facing guidance ("use the target keyword argument
instead") stays correct because passing both is still error-prone.
- Append a .. versionchanged:: 0.57 block noting the DeprecationWarning
that Server.cmd now emits when both are set.
- No code change.
The same factual error appeared in the parallel Server.cmd
versionchanged 0.57 block, fixed in the prior fixup commit. Window.cmd
and Pane.cmd don't carry the inverted note (verified).
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #672 +/- ##
==========================================
+ Coverage 47.11% 50.45% +3.33%
==========================================
Files 23 24 +1
Lines 3296 3437 +141
Branches 709 674 -35
==========================================
+ Hits 1553 1734 +181
- Misses 1381 1409 +28
+ Partials 362 294 -68 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
why: CI matrix on tmux 3.2a reported `LibTmuxException: list-windows: ['server exited unexpectedly']` during fetch_objs's -F format expansion. The token set added in the gap 14 commit — bracket_paste_flag, pane_flags, pane_floating_flag, pane_pb_progress, pane_pb_state, pane_pipe_pid, pane_zoomed_flag, synchronized_output_flag — exists only in tmux master post-3.6a. The expectation was that tmux silently expands unknown tokens to "" (per format.c:4321 in 3.2a), but at least one of these tokens triggers a tmux 3.2a server-side crash rather than a clean empty expansion — likely a NULL deref against state structures tmux master added but 3.2a doesn't have. The library's minimum supported tmux version is 3.2a, so a crash on that row blocks the matrix. what: - Remove the 8 forward-looking fields from src/libtmux/neo.py. - Remove the parametrized test_obj_declares_post_3_6a_field block from tests/test_pane.py. - Update the CHANGES entry under "~45 typed format-token fields" to "~37" and drop the paragraph describing the forward-looking tokens. These tokens can be re-added in a follow-up PR after the next tmux release ships them and a confirmed-safe minimum tmux version is known.
why: CI matrix on tmux 3.2a continued to fail after the prior gap-14 rollback with `LibTmuxException: list-windows: ['server exited unexpectedly']`. Cross-referencing libtmux's Obj.__dataclass_fields__ against ~/study/c/tmux-3.2a/format.c showed eight tokens that don't exist in tmux 3.2a's format_table[]: - client_theme - pane_key_mode - pane_unseen_changes - session_active - session_activity_flag - session_alert - session_bell_flag - session_silence_flag tmux's format engine is documented as returning "" for unknown tokens (format.c:4321 in 3.2a), but one or more of these specific tokens triggers a server-side crash in 3.2a's format engine. Probably a NULL deref via the options-lookup path (format_find calls options_parse_get first) when the token name matches an option-style key that resolves to NULL in some context. The library's minimum supported tmux version is 3.2a per TMUX_MIN_VERSION in src/libtmux/common.py, so a crash on that row blocks the matrix. what: - Drop the 8 fields from Obj in src/libtmux/neo.py. - Remove the corresponding entries from PANE_FORMAT_FIELDS in tests/test_pane.py and SESSION_FORMAT_FIELDS in tests/test_session.py. - Keep the rest of the gap-10 and gap-11 token additions; the remaining ~35 tokens are all in 3.2a's format_table and don't cause crashes. Follow-up: expose these fields via a version-gated mechanism (e.g. fetch the full format string only for tmux versions that support the tokens, or split Obj into core + augmented dataclasses) so users on 3.4+ / 3.6+ can still read the tokens. This is a forward commit (not autosquashed) so the rollback shows up clearly in the history.
…3.2a in list-windows why: After the previous rollback that dropped tokens unknown to tmux 3.2a's format_table, CI's 3.2a row still failed with `server exited unexpectedly` during fetch_objs's `list-windows -F …` call. The remaining suspects were the 11 client_* tokens added in the Client-class commit. tmux's format engine evaluates the -F template against each window-link, with no client bound (ft->c == NULL for the list-windows context). The callbacks check ft->c != NULL and return NULL safely on 3.6a, but at least one of these tokens triggers a server crash in 3.2a's format engine when invoked outside its valid client scope. Rather than chase the specific NULL-deref path through tmux 3.2a's C code, drop these tokens from Obj. The Client dataclass keeps the 14 pre-existing client_* fields (client_name, client_pid, client_termname, etc.) — enough to populate Server.clients with attached-terminal identity, but no longer covers the 12 tokens added in this branch. Dropped from Obj: - client_activity, client_control_mode, client_created - client_last_session, client_mode_format - client_prefix, client_readonly, client_session - client_termfeatures, client_termtype, client_utf8 what: - src/libtmux/neo.py: remove the 11 fields from Obj. - src/libtmux/client.py: change the Client docstring doctest to read client_name (pre-existing) instead of client_readonly (removed). - tests/test_client.py: remove test_client_session_reports_attached_session and test_client_readonly_default_zero (the fields they assert are gone). Follow-up: re-expose these tokens via a scope-aware format string (query client_* only when list-clients is the list_cmd, not when list-windows / list-panes is). This is documented as a TODO and will land in a separate PR once the safe-on-3.2a strategy is designed. Forward commit (not autosquashed) so the rollback shows up clearly.
why: After dropping the new tokens that crashed tmux 3.2a's format
engine, four display_message tests now fail on the 3.2a CI row with
empty stdout from `display-message -p -c <control-mode-client>`:
tests/test_server.py::test_server_display_message_flags[version]
tests/test_server.py::test_server_display_message_flags[socket_path_format_string]
tests/test_server.py::test_server_display_message_target_client
tests/test_window.py::test_window_display_message_target_client
tmux 3.2a's display-message -p dispatch via a control-mode client does
not reliably deliver stdout back to the client process — the call
succeeds (no error) but the result list is empty. Later tmux versions
(3.3+) fixed this dispatch path.
The wrappers themselves work correctly on 3.2a — only the specific test
shape that asserts on stdout content via a control-mode client doesn't
pass. Skip these tests on tmux < 3.3 and keep them gated for the
versions that exercise the contract reliably.
what:
- tests/test_server.py: add `has_gte_version("3.3")` skip to
test_server_display_message_flags (the parametrized cases that set
target_client) and test_server_display_message_target_client.
- tests/test_window.py: same skip on test_window_display_message_target_client.
- Other display_message tests (the window_display_message_flags
parametrize block that uses auto-injected -t and the
no_text_returns_none tests) pass on 3.2a unchanged.
Forward commit (not autosquashed) so the version-gate decision shows up
clearly in the history.
This reverts commit b02808c.
…tionale" This reverts commit 3538be1.
why: the deprecation was reverted in the prior two commits (revert of b02808c and 3538be1) because the rationale was factually inverted (positional -t actually WINS via tmux's last-wins arg parsing — verified in ~/study/c/tmux-3.6a/arguments.c:673 via TAILQ_LAST). The 0.34 contract already requires target= for object-level cmd() overrides (CHANGES:1075), so the 0.57 layer added misleading docstrings without new user value. what: - Remove the "### Deprecations" section from the 0.57.0 entry. - Remove the corresponding bullet under "### Documentation" pointing at the migration guide (the migration entry itself was removed when the Server.cmd warning commit was reverted). The 0.34 contract stays in place. A future major release can re-evaluate whether to enforce target=-only with a TypeError after a clean pre-announcement.
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.
why: the scope-aware get_output_format introduced in the prior commit now restricts each list-* subcommand's -F template to tokens whose scope is reachable from that subcommand. The 11 client_* tokens (client_activity, client_control_mode, client_created, client_last_session, client_mode_format, client_prefix, client_readonly, client_session, client_termfeatures, client_termtype, client_utf8) only appear when fetch_objs is called with list_cmd="list-clients" — never during list-windows, list-panes, or list-sessions. This eliminates the root cause of the tmux 3.2a server crash that forced the original rollback. what: - src/libtmux/neo.py: re-add the 11 client_* fields on Obj alphabetically between the existing client_* declarations. No FIELD_VERSION entries are needed — all 11 ship in tmux 3.2a's format_table (verified against ~/study/c/tmux-3.2a/format.c). - src/libtmux/client.py: restore the doctest reading client.client_readonly (a 0/1 string) to demonstrate the typed surface. - tests/test_client.py: re-add test_client_session_reports_attached_session (asserts client.client_session matches the attached session name) and test_client_readonly_default_zero (asserts client.client_readonly is "0" for a normal attached client). Verification: list-windows/list-panes/list-sessions stay scope-clean on tmux 3.2a — the format string for those subcommands contains no client_* tokens. Confirmed via the runtime gate: 1175 tests pass on local tmux 3.6a; CI matrix run will confirm 3.2a.
…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: tmux master post-3.6a registers 8 new format tokens (verified via
~/study/c/tmux/format.c grep on each format_cb_* signature). Declaring
them on Obj now means libtmux is ready when tmux 3.7 ships — the
FIELD_VERSION gate keeps them silent on every currently-released tmux,
and they begin hydrating automatically once the user upgrades tmux.
what:
- src/libtmux/neo.py:
- Add 8 fields to Obj alphabetically: bracket_paste_flag, pane_flags,
pane_floating_flag, pane_pb_progress, pane_pb_state, pane_pipe_pid,
pane_zoomed_flag, synchronized_output_flag.
- Tag each in FIELD_VERSION with "3.7" so they're absent in every
list-* template on tmux <=3.6a.
- Add _SCOPE_OVERRIDES dict for tokens whose name doesn't carry a
scope prefix; map bracket_paste_flag and synchronized_output_flag
to "pane" scope (their tmux callbacks dereference ft->wp). The six
pane_* tokens scope correctly via the existing prefix table.
- Update _token_scope() to consult _SCOPE_OVERRIDES first.
Verification: on tmux 3.6a (local), the 8 fields stay None after
refresh() — confirmed because FIELD_VERSION blocks them from the -F
template. All 1182 tests pass. CI matrix will confirm 3.2a -> master.
why: the 0.57.0 entry's "typed format-token fields" deliverable was previously truncated to ~37 fields after the tmux 3.2a CI rollback. With scope-aware + version-aware get_output_format in place, the full token set re-enters the typed surface safely. what: - CHANGES: rewrite the deliverable section to describe scope+version gating (list-clients emits only client_* + universal; tokens added in tmux 3.4/3.5/3.6 are gated; 8 forward-looking master tokens are declared but hydrate only once tmux 3.7 ships). Cross-link each Pane/Window/Session/Client class.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the 14 wrapper gaps catalogued in #670, surfaced when downstream consumers adopted libtmux 0.56.0's typed API. The branch ships:
Server.display_messageandWindow.display_messagewrappers (gaps 1, 2)window_zoomed_flagand ~44 other typedPane/Window/Sessionformat-token fields on theObjdataclass (gaps 3, 10, 14)Pane.reset()fix — closes mcp(pane_tools): clear_pane does not reliably clear visible content #650 (gap 4)Pane.send_keys(cmd=None, ...)flag-only invocation (gap 5)Server.list_buffers(format_string=, filter=)(gap 6)Server.search_panes/windows/sessionsplus Session/Window analogues, exposing tmux's C-side-ffilter (gap 7)LibTmuxException.subcommandattribute andcommon.raise_if_stderrhelper threaded through ~80 wrapper raise sites (gap 8)DeprecationWarningon the legacy-t-in-argsshape with thetarget=migration documented (gap 9)Clientdataclass andServer.clientsaccessor (gap 11)Server.run_shell(cwd=, show_stderr=)(gap 12)Pane.capture_pane(pending=True)(gap 13)Compatibility
Server.run_shell'scwd=andshow_stderr=are version-gated (-crequires tmux 3.4+;-Erequires tmux 3.6+); older tmux emits a warning and ignores the kwarg. Other new flags either ship in tmux 3.2a (the project's minimum) or were already gated where needed.Test plan
rm -rf docs/_build && uv run ruff check . && uv run ruff format --check . && uv run mypy && uv run py.test --reruns 0 -vvv && just build-docs— passes locally on tmux 3.6arg 'Previously |Named .* to avoid|was lost when|gap #' src/libtmux/ tests/returns zero hitsgh pr checks --watchRefs