Skip to content

Close 14 wrapper gaps from #670 (libtmux 0.57.0)#672

Open
tony wants to merge 31 commits into
masterfrom
parity-pt-2
Open

Close 14 wrapper gaps from #670 (libtmux 0.57.0)#672
tony wants to merge 31 commits into
masterfrom
parity-pt-2

Conversation

@tony
Copy link
Copy Markdown
Member

@tony tony commented May 16, 2026

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_message and Window.display_message wrappers (gaps 1, 2)
  • window_zoomed_flag and ~44 other typed Pane/Window/Session format-token fields on the Obj dataclass (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/sessions plus Session/Window analogues, exposing tmux's C-side -f filter (gap 7)
  • LibTmuxException.subcommand attribute and common.raise_if_stderr helper threaded through ~80 wrapper raise sites (gap 8)
  • DeprecationWarning on the legacy -t-in-args shape with the target= migration documented (gap 9)
  • Client dataclass and Server.clients accessor (gap 11)
  • Server.run_shell(cwd=, show_stderr=) (gap 12)
  • Pane.capture_pane(pending=True) (gap 13)

Compatibility

Server.run_shell's cwd= and show_stderr= are version-gated (-c requires tmux 3.4+; -E requires 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.6a
  • Narrative-bleed scan clean: rg 'Previously |Named .* to avoid|was lost when|gap #' src/libtmux/ tests/ returns zero hits
  • CI matrix (3.2a through 3.6a) green via gh pr checks --watch

Refs

tony added 17 commits May 16, 2026 08:57
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
Copy link
Copy Markdown

codecov Bot commented May 16, 2026

Codecov Report

❌ Patch coverage is 61.87500% with 122 lines in your changes missing coverage. Please review.
✅ Project coverage is 50.45%. Comparing base (73f5740) to head (e98f483).

Files with missing lines Patch % Lines
src/libtmux/neo.py 30.69% 69 Missing and 1 partial ⚠️
src/libtmux/server.py 77.00% 19 Missing and 4 partials ⚠️
src/libtmux/window.py 64.58% 12 Missing and 5 partials ⚠️
src/libtmux/client.py 33.33% 6 Missing ⚠️
src/libtmux/session.py 83.33% 3 Missing ⚠️
src/libtmux/exc.py 75.00% 2 Missing ⚠️
src/libtmux/common.py 66.66% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

tony added 12 commits May 16, 2026 09:15
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.
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.
tony added 2 commits May 16, 2026 10:52
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mcp(pane_tools): clear_pane does not reliably clear visible content

1 participant