Type hints for tmux 3.4+ format tokens#674
Draft
tony wants to merge 65 commits into
Draft
Conversation
why: 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).
why: 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().
why: 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.
why: 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).
why: 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.
why: 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).
…tmux C-side filter
why: tmux's cmd-list-panes.c:41 accepts `[-f filter]` and evaluates
`format_true(expanded)`, 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_*).
…se_if_stderr helper
why: 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.
why: 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 (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.
why: 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.
why: 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. An earlier commit on this branch 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.
why: 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 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.
why: 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.
why: 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).
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 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.
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 (https://github.com/tmux/tmux/blob/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).
why: CI matrix on tmux 3.2a continued to fail after the prior rollback with `LibTmuxException: list-windows: ['server exited unexpectedly']`. Cross-referencing libtmux's Obj.__dataclass_fields__ against https://github.com/tmux/tmux/blob/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 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 and token additions; the remaining ~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.
…a 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 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 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 6d153ef.
…tionale" This reverts commit f5d062d.
why: the deprecation was reverted in the prior two commits because the rationale was factually inverted (positional -t actually WINS via tmux's last-wins arg parsing — verified in tmux's last-wins arg parsing in arguments.c 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: '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 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 https://github.com/tmux/tmux/blob/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: tests pass on local tmux 3.6a; CI matrix run will confirm 3.2a.
…cope prefix why: the prefix-based scope router in _token_scope handles tokens whose names start with pane_/window_/session_/client_/buffer_, but tmux's format table contains tokens whose names don't carry a scope prefix (cursor_*, mouse_*_flag, scroll_region_*, etc.). Without an override mechanism those tokens either fall through to the wrong scope via a prefix rule or land in the fail-closed "unknown" bucket and never appear in any list-* template. Introduce _SCOPE_OVERRIDES as the targeted escape hatch: a per-token map that wins before the prefix table. Subsequent scope-gate fix commits populate it as misclassifications are audited against tmux's format_cb_* callbacks. what: - Declare _SCOPE_OVERRIDES as an empty dict in src/libtmux/neo.py. - _token_scope() consults it first, before _SCOPE_PREFIXES. - Doctest in _token_scope() documents the override path without pinning a specific token (later commits add their own).
why: the 0.57.0 entry's "typed format-token fields" deliverable was previously truncated to ~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.
why: Server/Window/Pane.display_message() called tmux then returned proc.stdout without inspecting proc.stderr. On tmux errors (e.g. -F together with positional cmd: "only one of -F or argument must be given") the wrapper silently returned [] or None, hiding the failure from callers and contradicting the rest of the typed-wrapper surface. what: - Insert raise_if_stderr(proc, "display-message") in all three display_message wrappers, matching the sibling display_popup / display_panes / display_menu wrappers in this branch. - Add a negative test per scope verifying the wrapper raises LibTmuxException(subcommand="display-message") instead of swallowing tmux's "only one of -F or argument" error. - Document the fix in CHANGES under Fixes.
why: The pending= docstring described tmux's -P as capturing "bytes tmux has buffered as input for the pane but the running program hasn't consumed". That's backwards: tmux is a terminal, it observes output from the running program, not input to it. tmux's input_pending() returns ictx->since_ground, an evbuffer that accumulates output bytes which begin an incomplete escape sequence and are still pending the parser's ground state. Same wording leaked into the CHANGES entry. what: - Rewrite the pending= parameter docstring in Pane.capture_pane() to describe parser-state semantics (incomplete escape sequences, since_ground), keeping the diagnostic-use mention. - Mirror the same correction in the CHANGES entry for the kwarg.
why: The cwd= docstring framed the kwarg as a "working directory for the
shell command", parallel to subprocess.Popen(cwd=). tmux's -c is actually a
*start directory*: tmux/job.c:142 tries chdir(cwd), then chdir(home), then
chdir("/"), only fatal()'ing if all three fail. A user relying on Python
semantics expects a failed chdir to raise, but tmux silently falls back.
Reproduced live: run_shell('pwd', cwd='/definitely/not/a/path') returns
['<HOME>'].
what:
- Extend the cwd= parameter description to document tmux's home -> /
fallback chain and contrast with subprocess.Popen(cwd=).
- Re-word the lead sentence "Working directory" to "Start directory"
to match tmux's terminology.
…lient
why: After display_message() started propagating tmux stderr (the prior display_message-fix commit), tmux 3.2a's CI surfaced a latent issue in
test_server_display_message_no_text_returns_none: the control-mode client
path emits a usage error from tmux's argument parser on 3.2a, which the test
silently absorbed before but now raises. This is the same
control-mode-on-3.2a unreliability already gated on the sibling
test_server_display_message_flags and
test_server_display_message_target_client suites with
`has_gte_version("3.3")`.
what:
- Add the same `has_gte_version("3.3")` skip to
test_server_display_message_no_text_returns_none, matching the
existing gate on its sibling tests.
why: The 0.57.x section claimed list-clients "emits the client_*
tokens but never pane_* ones" — backwards: tmux's format_defaults
cascades downward through c->session, the session's current window,
and that window's active pane, so list-clients (and list-sessions
and list-windows) all hydrate pane fields via cascade. Pinned by the
strengthened cascade tests on this branch and by upstream
cmd-list-clients.c calling format_defaults(ft, l[i], NULL, NULL,
NULL). Two adjacent overclaims compounded the issue: a fixed token
count ("twelve client_* tokens") that didn't match the typed Client
surface, and a "every wrapper uses raise_if_stderr" claim that didn't
match the supported surface in practice.
what:
- Rewrite the cascade paragraph in the typed format-token entry to
describe the actual downward cascade (Client/Session/Window rows
hydrate active-pane/window fields; client_* resolves only under
list-clients because tmux has no reverse cascade).
- Drop number slop: "twelve client_* tokens", "~45 additional format
tokens", the explicit list of eight forward-looking tokens, and
the embedded token-name catalogs that belong in autodoc.
- Replace "every wrapper" with "shared helper used across the
supported surface". Trim "matching the rest of the typed wrappers"
to "matching the rest of the supported surface".
- Add the Client snapshot caveat (session_id/window_id/pane_id are
attached-view, not identity) into the Client entry.
- Add the malformed-predicate caveat to the C-side filter entry.
- Add a Fixes bullet for Server.clients and Server.search_sessions
propagating tmux errors instead of swallowing them.
- Add a Documentation bullet for the Obj cascade-semantics
docstring.
- Tighten the lead paragraph: drop "closes wrapper gaps" and the
token count.
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 C-side 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 (c-side-filtering)= section covering Python-side vs C-side 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 c-side-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_*()` (C-side 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 `c-side-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 `c-side-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, 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.
…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: tmux master (post-3.6a) registers eight new format tokens that the next tmux release will ship: pane_zoomed_flag, pane_floating_flag, pane_flags, pane_pb_state, pane_pb_progress, pane_pipe_pid, synchronized_output_flag, bracket_paste_flag. Declaring them now means libtmux is ready when the tag lands; older tmux releases expand unknown tokens to empty strings, so the fields stay None until the user upgrades tmux. what: - src/libtmux/neo.py: add the fields alphabetically within the existing Obj layout (pane_* tokens among the pane_* block, bracket_paste_flag near buffer_*, synchronized_output_flag near start_time). - tests/test_pane.py: parametrized test asserts each field is declared on the dataclass and hydrates either as None or as a string after refresh(). No runtime-value assertions — those will activate when the shipping tmux release exposes the tokens.
why: with the scope+version gating in place, the format string sent to older
tmux versions automatically excludes tokens that those versions don't
recognize. The tokens below first registered in tmux 3.4-3.6 — tagging them
with FIELD_VERSION makes them appear on supported tmux releases that include
them, and absent on older tmux without sending unknown tokens that bloat the
format string or trigger crashes.
what:
- src/libtmux/neo.py:
- Re-add fields to Obj alphabetically: pane_key_mode,
pane_unseen_changes, session_active, session_activity_flag,
session_alert, session_bell_flag, session_silence_flag, client_theme.
- Populate FIELD_VERSION with each token's minimum tmux release
(3.4 for pane_unseen_changes, 3.5 for pane_key_mode, 3.6 for the
five session_* tokens and client_theme).
- tests/test_pane.py: restore pane_key_mode and pane_unseen_changes in
PANE_FORMAT_FIELDS (the parametrized declaration+hydration test).
- tests/test_session.py: restore the new session_* entries in
SESSION_FORMAT_FIELDS.
Verification:
- On tmux 3.6a (local), all tokens hydrate via refresh(); tests
pass.
- On tmux 3.2a, FIELD_VERSION skips all 8 — the -F template stays at
its pre--rollback shape for that version.
Version anchors verified via: rg '"<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.
why: tmux master post-3.6a registers new format tokens (verified via https://github.com/tmux/tmux/blob/master/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 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 fields stay None after refresh() — confirmed because FIELD_VERSION blocks them from the -F template. All tests pass.
This was referenced May 17, 2026
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## parity-pt-2 #674 +/- ##
===============================================
- Coverage 51.08% 50.84% -0.24%
===============================================
Files 25 25
Lines 3465 3481 +16
Branches 682 682
===============================================
Hits 1770 1770
- Misses 1400 1416 +16
Partials 295 295 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
068ce01 to
07d009f
Compare
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
Adds typed Python attributes for format tokens tmux added in releases
after 3.2a — covering 3.4 (`pane_unseen_changes`), 3.5
(`pane_key_mode`), 3.6 (`session_active`, `session_activity_flag`,
`session_alert`, `session_bell_flag`, `session_silence_flag`,
`client_theme`), and the forward-looking set expected to land with
tmux 3.7+ (`bracket_paste_flag`, `pane_flags`, `pane_floating_flag`,
`pane_pb_progress`, `pane_pb_state`, `pane_pipe_pid`,
`pane_zoomed_flag`, `synchronized_output_flag`).
Problem solved
IDE autocomplete and `mypy` awareness for newer tmux format tokens.
Today users can read these via raw `display-message -p`, but the
typed surface saves the field-name lookup and centralizes the
version gate.
Why this is its own shipment
The forward-looking tokens can't be validated in CI until tmux 3.7
reaches a tagged release. Holding the shipment lets the typed
declarations land alongside real version-checked tests instead of
declarations gated by a version number that doesn't yet exist.
Status
Draft, gated on tmux 3.7 reaching a tagged release. This branch is
parented on `parity-pt-2` (#672); once #672 merges, rebase onto
`master` and revisit. The 3.4 / 3.5 / 3.6 tokens are already
validatable today and could land sooner if needed — but the
forward-looking set drives the held-back timing.
Acceptance criteria for merge
returns the correct field set for every combination of
`list_cmd ∈ {list-sessions, list-windows, list-panes, list-clients}`
\× `version ∈ {3.2a, 3.3, 3.4, 3.5, 3.6a, 3.7}`
fields hydrate as expected
Test plan
pre-existing 3.2a control-mode skips)
after the rebase target merges
Refs
and core typed surface land in Increase tmux coverage: Client, typed fields, C-side filter #672; the forward-looking expansion
lands here.