Skip to content

tmux parity: add interactive command coverage and align flag semantics with tmux - #653

Merged
tony merged 69 commits into
masterfrom
tmux-parity
May 10, 2026
Merged

tmux parity: add interactive command coverage and align flag semantics with tmux#653
tony merged 69 commits into
masterfrom
tmux-parity

Conversation

@tony

@tonytony commented Mar 29, 2026

Copy link
Copy Markdown
Member

Summary

This PR expands libtmux's tmux command parity across Server, Session, Window, and Pane, with a focus on interactive and client-dependent commands. It also tightens several flag mappings and state-refresh behaviors so the high-level API matches tmux semantics more closely.

In addition to the new command coverage, this includes the three follow-up fixes from review:

  • Session.detach_client() no longer forces -s, so targeted detach behaves like tmux.
  • Window.move_window() now refreshes after successful moves, so returned objects are not stale after relative or cross-session moves.
  • ControlMode now binds client_name to the spawned client by matching client_pid, which fixes multi-client races.

Main Changes

New or expanded tmux command coverage

  • Add Server support for commands such as bind_key, unbind_key, list_keys, list_commands, start_server, lock_server, lock_client, refresh_client, suspend_client, server_access, run_shell, if_shell, source_file, buffer commands, confirm_before, command_prompt, and display_menu.
  • Add Session.detach_client() and window navigation helpers.
  • Add Window support for move_window flags, select_layout flags, last_pane, next_layout, previous_layout, rotate, swap, respawn, link, and unlink.
  • Expand Pane coverage for display_popup, capture_pane, send_keys, select, copy_mode, clock_mode, choose_*, customize_mode, display_panes, find_window, paste_buffer, clear_history, pipe, join, break_pane, move, respawn, and related flags.

tmux semantics and compatibility fixes

  • Correct several flag mappings and version gates to match tmux behavior.
  • Treat move-window -r as standalone renumbering.
  • Fix popup, prompt, paste-buffer, rotate, choose-tree, clear-history, capture-pane, and run-shell semantics where prior mappings diverged from tmux.
  • Add version annotations and parity docs where new parameters were introduced on existing APIs.

Test and tooling support

  • Add ControlMode and pytest fixture support for commands that require a real attached client.
  • Add extensive functional coverage for new command paths and regression cases.
  • Add parity-analysis support files under .claude/ and skills/tmux-parity/ to help maintain command coverage against tmux.

Testing

Ran:

  • uv run ruff check . --fix --show-fixes
  • uv run ruff format .
  • uv run mypy
  • uv run py.test --reruns 0 -vvv

Latest full run:

  • 1067 passed, 1 skipped

Notes

  • Diff vs origin/master is broad because this branch includes both the parity feature work and the review-driven correctness fixes on top.
  • The attached-client test infrastructure is intentionally part of this PR because several new tmux commands cannot be exercised correctly without a real client.

Comment threadsrc/libtmux/_internal/control_mode.py Fixed
@codecov

codecovBot commented Mar 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 48.51695% with 486 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.02%. Comparing base (0b77cd5) to head (dd3da50).

Files with missing linesPatch %Lines
src/libtmux/server.py46.19%110 Missing and 81 partials ⚠️
src/libtmux/pane.py50.39%107 Missing and 80 partials ⚠️
src/libtmux/window.py48.62%32 Missing and 24 partials ⚠️
src/libtmux/_internal/control_mode.py48.21%28 Missing and 1 partial ⚠️
src/libtmux/session.py45.16%11 Missing and 6 partials ⚠️
src/libtmux/common.py66.66%2 Missing ⚠️
src/libtmux/options.py50.00%1 Missing and 1 partial ⚠️
src/libtmux/pytest_plugin.py33.33%2 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #653 +/- ##
==========================================
+ Coverage 46.58% 47.02% +0.44% 
==========================================
Files 22 23 +1 Lines 2372 3296 +924 Branches 390 709 +319 ==========================================
+ Hits 1105 1550 +445 - Misses 1098 1384 +286 - Partials 169 362 +193 

☔ 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

tony commented Mar 29, 2026

Copy link
Copy Markdown
MemberAuthor

Code review

Found 4 issues:

  1. Pane.display_message(): no_expand maps to the wrong tmux flag. -I in display-message opens the pane for stdin input forwarding (window_pane_start_input()), not format suppression. The correct flag is -l (literal/no-expand, tmux 3.4+). Additionally, list_formats is also mapped to -l with the doc claiming it "lists format variables", but -l means suppress expansion; listing variables is done by -a (already mapped to all_formats). Both parameters are misassigned.

tmux_args+= ("-v",)
ifno_expand:
tmux_args+= ("-I",)
ifnotify:
tmux_args+= ("-N",)
iflist_formats:
ifhas_gte_version("3.4", tmux_bin=self.server.tmux_bin):
tmux_args+= ("-l",)
else:
warnings.warn(
"list_formats requires tmux 3.4+, ignoring",
stacklevel=2,

  1. Window.swap() does not call self.refresh() after the operation, leaving window_index stale. The docstring example works around this by manually calling w1.refresh()/w2.refresh(), but the method itself never refreshes. Commit 3654a36e fixed the identical issue in move_window() for the same reason ("move-window can land on an index different from the requested target… leaving the returned Window stale"); swap-window has the same behaviour.

target_id=target.window_idifisinstance(target, Window) elsetarget
tmux_args+= ("-s", str(target_id))
proc=self.cmd("swap-window", *tmux_args)
ifproc.stderr:
raiseexc.LibTmuxException(proc.stderr)

  1. Server.show_prompt_history() and Server.clear_prompt_history() have no version guard. Both commands were added in tmux 3.3; libtmux's declared minimum is 3.2a. On tmux 3.2a the calls will raise LibTmuxException with a raw "unknown command" tmux error instead of a clean version message. The pattern established in the same PR for confirm_before and command_prompt (has_gte_version("3.3") + raise) should be applied here too.

defshow_prompt_history(
self,
*,
prompt_type: str|None=None,
) ->list[str]:
"""Show prompt history via ``$ tmux show-prompt-history``.
Parameters
----------
prompt_type : str, optional
Prompt type to show (``-T`` flag). One of: ``command``,
``search``, ``target``, ``window-target``.
Returns
-------
list[str]
Prompt history lines.
Examples
--------
>>> result = server.show_prompt_history()
>>> isinstance(result, list)
True
"""
tmux_args: tuple[str, ...] = ()
ifprompt_typeisnotNone:
tmux_args+= ("-T", prompt_type)
proc=self.cmd("show-prompt-history", *tmux_args)
ifproc.stderr:
raiseexc.LibTmuxException(proc.stderr)
returnproc.stdout
defclear_prompt_history(
self,
*,

  1. display_popup docstring cross-reference uses the wrong module path: ~libtmux.test.control_mode.ControlMode — that module does not exist. The class lives at libtmux._internal.control_mode.ControlMode, which is already used correctly in server.py line 1013. This will produce a broken Sphinx link in generated docs.

libtmux/src/libtmux/pane.py

Lines 1202 to 1206 in 857384a

Requires tmux 3.2+ and an attached client. Use
:class:`~libtmux.test.control_mode.ControlMode` in tests to provide
a client.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@tony
tonyforce-pushed the tmux-parity branch 2 times, most recently from bc12f79 to 64a9ad5CompareMay 2, 2026 18:16
tony added a commit that referenced this pull request May 2, 2026
…matrix
why: PR #653 builds failed on tmux 3.2a-3.5 (3.6/master cancelled as
siblings). Investigation traced six categories: methods that need an
attached client, tests/doctests missing version guards, and a tmux
upstream regression in run-shell on 3.3a/3.4.
what:
- Server.show_messages: add target_client kwarg; cmd-show-messages.c
uses format_create_from_target without -T/-J, so a TTY-less CI
server raises 'no current client' unless -t <client> is supplied.
- Server.server_access: add 3.3+ version guard (server-access was
introduced in tmux 3.3 per CHANGES FROM 3.2a TO 3.3).
- Server.run_shell doctest: drop stdout assertion. tmux 3.3a/3.4 do
not pipe run-shell stdout back through cmdq; restored upstream in
3.5 by commit fb37d52d.
- Server.{command_prompt,confirm_before,show_prompt_history,
clear_prompt_history} doctests: gate interactive demos behind
has_gte_version so the doctest is harmless on older tmux.
- tests/test_server.py: skip test_server_access_list,
test_show_prompt_history, test_clear_prompt_history on <3.3; skip
test_run_shell_basic on <3.5; rewrite test_show_messages to spawn
a control-mode client via the existing fixture and pass
target_client.
- tests/test_pane.py: skip test_split_percentage on <3.5 since
split-window -p was broken in 3.4 (fixed per CHANGES 3.4 TO 3.5).
@tony
tony marked this pull request as ready for review May 2, 2026 19:04
tony added a commit that referenced this pull request May 3, 2026
…matrix
why: PR #653 builds failed on tmux 3.2a-3.5 (3.6/master cancelled as
siblings). Investigation traced six categories: methods that need an
attached client, tests/doctests missing version guards, and a tmux
upstream regression in run-shell on 3.3a/3.4.
what:
- Server.show_messages: add target_client kwarg; cmd-show-messages.c
uses format_create_from_target without -T/-J, so a TTY-less CI
server raises 'no current client' unless -t <client> is supplied.
- Server.server_access: add 3.3+ version guard (server-access was
introduced in tmux 3.3 per CHANGES FROM 3.2a TO 3.3).
- Server.run_shell doctest: drop stdout assertion. tmux 3.3a/3.4 do
not pipe run-shell stdout back through cmdq; restored upstream in
3.5 by commit fb37d52d.
- Server.{command_prompt,confirm_before,show_prompt_history,
clear_prompt_history} doctests: gate interactive demos behind
has_gte_version so the doctest is harmless on older tmux.
- tests/test_server.py: skip test_server_access_list,
test_show_prompt_history, test_clear_prompt_history on <3.3; skip
test_run_shell_basic on <3.5; rewrite test_show_messages to spawn
a control-mode client via the existing fixture and pass
target_client.
- tests/test_pane.py: skip test_split_percentage on <3.5 since
split-window -p was broken in 3.4 (fixed per CHANGES 3.4 TO 3.5).
@tony

tony commented May 3, 2026

Copy link
Copy Markdown
MemberAuthor

Code review

Found 1 issue:

  1. Server.command_prompt(bspace_exit=True) emits -e with no version guard, while the sibling literal parameter on the same method does guard -l with has_gte_version("3.6") and a warnings.warn fallback. The docstring acknowledges that -e was added by upstream commit 1e5f93b7 on 2026-01-14 and is "not in any tagged release at the time of writing", so any caller passing bspace_exit=True on currently-released tmux (3.2a–3.6a) will get an unknown flag -e error from tmux. The CI hides this because tests/test_server.py::test_command_prompt_extra_flags[bspace_exit] monkeypatches server.cmd, never invoking real tmux for the assertion.

ifliteral:
ifhas_gte_version("3.6", tmux_bin=self.tmux_bin):
tmux_args+= ("-l",)
else:
warnings.warn(
"literal requires tmux 3.6+, ignoring",
stacklevel=2,
)
ifbspace_exit:
tmux_args+= ("-e",)

Suggested fix: mirror the literal pattern — guard the -e append on has_gte_version (e.g. > 3.6 once a tag exists, or check tmux master) and emit a warning when the user requests bspace_exit=True on a version that lacks it.

Other items considered and dropped below the 80 confidence threshold: ControlMode _write_fd is not closed if subprocess.Popen raises before the registration loop (control_mode.py L61–L92) — real but error-path-only; and Session.detach_client(all_clients=True, target_client=...) enumerates list-clients then issues per-client detaches non-atomically — race window is microseconds and the docstring acknowledges it.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@tony

tony commented May 3, 2026

Copy link
Copy Markdown
MemberAuthor

Code review

No issues found. Checked for bugs and CLAUDE.md compliance against the 3 commits since the prior review (095758a..1704a54).

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

tony added a commit that referenced this pull request May 9, 2026
…matrix
why: PR #653 builds failed on tmux 3.2a-3.5 (3.6/master cancelled as
siblings). Investigation traced six categories: methods that need an
attached client, tests/doctests missing version guards, and a tmux
upstream regression in run-shell on 3.3a/3.4.
what:
- Server.show_messages: add target_client kwarg; cmd-show-messages.c
uses format_create_from_target without -T/-J, so a TTY-less CI
server raises 'no current client' unless -t <client> is supplied.
- Server.server_access: add 3.3+ version guard (server-access was
introduced in tmux 3.3 per CHANGES FROM 3.2a TO 3.3).
- Server.run_shell doctest: drop stdout assertion. tmux 3.3a/3.4 do
not pipe run-shell stdout back through cmdq; restored upstream in
3.5 by commit fb37d52d.
- Server.{command_prompt,confirm_before,show_prompt_history,
clear_prompt_history} doctests: gate interactive demos behind
has_gte_version so the doctest is harmless on older tmux.
- tests/test_server.py: skip test_server_access_list,
test_show_prompt_history, test_clear_prompt_history on <3.3; skip
test_run_shell_basic on <3.5; rewrite test_show_messages to spawn
a control-mode client via the existing fixture and pass
target_client.
- tests/test_pane.py: skip test_split_percentage on <3.5 since
split-window -p was broken in 3.4 (fixed per CHANGES 3.4 TO 3.5).
tony added a commit that referenced this pull request May 9, 2026
…matrix
why: PR #653 builds failed on tmux 3.2a-3.5 (3.6/master cancelled as
siblings). Investigation traced six categories: methods that need an
attached client, tests/doctests missing version guards, and a tmux
upstream regression in run-shell on 3.3a/3.4.
what:
- Server.show_messages: add target_client kwarg; cmd-show-messages.c
uses format_create_from_target without -T/-J, so a TTY-less CI
server raises 'no current client' unless -t <client> is supplied.
- Server.server_access: add 3.3+ version guard (server-access was
introduced in tmux 3.3 per CHANGES FROM 3.2a TO 3.3).
- Server.run_shell doctest: drop stdout assertion. tmux 3.3a/3.4 do
not pipe run-shell stdout back through cmdq; restored upstream in
3.5 by commit fb37d52d.
- Server.{command_prompt,confirm_before,show_prompt_history,
clear_prompt_history} doctests: gate interactive demos behind
has_gte_version so the doctest is harmless on older tmux.
- tests/test_server.py: skip test_server_access_list,
test_show_prompt_history, test_clear_prompt_history on <3.3; skip
test_run_shell_basic on <3.5; rewrite test_show_messages to spawn
a control-mode client via the existing fixture and pass
target_client.
- tests/test_pane.py: skip test_split_percentage on <3.5 since
split-window -p was broken in 3.4 (fixed per CHANGES 3.4 TO 3.5).
tony added a commit that referenced this pull request May 9, 2026
…matrix
why: PR #653 builds failed on tmux 3.2a-3.5 (3.6/master cancelled as
siblings). Investigation traced six categories: methods that need an
attached client, tests/doctests missing version guards, and a tmux
upstream regression in run-shell on 3.3a/3.4.
what:
- Server.show_messages: add target_client kwarg; cmd-show-messages.c
uses format_create_from_target without -T/-J, so a TTY-less CI
server raises 'no current client' unless -t <client> is supplied.
- Server.server_access: add 3.3+ version guard (server-access was
introduced in tmux 3.3 per CHANGES FROM 3.2a TO 3.3).
- Server.run_shell doctest: drop stdout assertion. tmux 3.3a/3.4 do
not pipe run-shell stdout back through cmdq; restored upstream in
3.5 by commit fb37d52d.
- Server.{command_prompt,confirm_before,show_prompt_history,
clear_prompt_history} doctests: gate interactive demos behind
has_gte_version so the doctest is harmless on older tmux.
- tests/test_server.py: skip test_server_access_list,
test_show_prompt_history, test_clear_prompt_history on <3.3; skip
test_run_shell_basic on <3.5; rewrite test_show_messages to spawn
a control-mode client via the existing fixture and pass
target_client.
- tests/test_pane.py: skip test_split_percentage on <3.5 since
split-window -p was broken in 3.4 (fixed per CHANGES 3.4 TO 3.5).
tony added a commit that referenced this pull request May 9, 2026
…matrix
why: PR #653 builds failed on tmux 3.2a-3.5 (3.6/master cancelled as
siblings). Investigation traced six categories: methods that need an
attached client, tests/doctests missing version guards, and a tmux
upstream regression in run-shell on 3.3a/3.4.
what:
- Server.show_messages: add target_client kwarg; cmd-show-messages.c
uses format_create_from_target without -T/-J, so a TTY-less CI
server raises 'no current client' unless -t <client> is supplied.
- Server.server_access: add 3.3+ version guard (server-access was
introduced in tmux 3.3 per CHANGES FROM 3.2a TO 3.3).
- Server.run_shell doctest: drop stdout assertion. tmux 3.3a/3.4 do
not pipe run-shell stdout back through cmdq; restored upstream in
3.5 by commit fb37d52d.
- Server.{command_prompt,confirm_before,show_prompt_history,
clear_prompt_history} doctests: gate interactive demos behind
has_gte_version so the doctest is harmless on older tmux.
- tests/test_server.py: skip test_server_access_list,
test_show_prompt_history, test_clear_prompt_history on <3.3; skip
test_run_shell_basic on <3.5; rewrite test_show_messages to spawn
a control-mode client via the existing fixture and pass
target_client.
- tests/test_pane.py: skip test_split_percentage on <3.5 since
split-window -p was broken in 3.4 (fixed per CHANGES 3.4 TO 3.5).
tony added a commit that referenced this pull request May 9, 2026
…matrix
why: PR #653 builds failed on tmux 3.2a-3.5 (3.6/master cancelled as
siblings). Investigation traced six categories: methods that need an
attached client, tests/doctests missing version guards, and a tmux
upstream regression in run-shell on 3.3a/3.4.
what:
- Server.show_messages: add target_client kwarg; cmd-show-messages.c
uses format_create_from_target without -T/-J, so a TTY-less CI
server raises 'no current client' unless -t <client> is supplied.
- Server.server_access: add 3.3+ version guard (server-access was
introduced in tmux 3.3 per CHANGES FROM 3.2a TO 3.3).
- Server.run_shell doctest: drop stdout assertion. tmux 3.3a/3.4 do
not pipe run-shell stdout back through cmdq; restored upstream in
3.5.
- Server.{command_prompt,confirm_before,show_prompt_history,
clear_prompt_history} doctests: gate interactive demos behind
has_gte_version so the doctest is harmless on older tmux.
- tests/test_server.py: skip test_server_access_list,
test_show_prompt_history, test_clear_prompt_history on <3.3; skip
test_run_shell_basic on <3.5; rewrite test_show_messages to spawn
a control-mode client via the existing fixture and pass
target_client.
- tests/test_pane.py: skip test_split_percentage on <3.5 since
split-window -p was broken in 3.4 (fixed per CHANGES 3.4 TO 3.5).
tony added a commit that referenced this pull request May 9, 2026
…matrix
why: PR #653 builds failed on tmux 3.2a-3.5 (3.6/master cancelled as
siblings). Investigation traced six categories: methods that need an
attached client, tests/doctests missing version guards, and a tmux
upstream regression in run-shell on 3.3a/3.4.
what:
- Server.show_messages: add target_client kwarg; cmd-show-messages.c
uses format_create_from_target without -T/-J, so a TTY-less CI
server raises 'no current client' unless -t <client> is supplied.
- Server.server_access: add 3.3+ version guard (server-access was
introduced in tmux 3.3 per CHANGES FROM 3.2a TO 3.3).
- Server.run_shell doctest: drop stdout assertion. tmux 3.3a/3.4 do
not pipe run-shell stdout back through cmdq; restored upstream in
3.5.
- Server.{command_prompt,confirm_before,show_prompt_history,
clear_prompt_history} doctests: gate interactive demos behind
has_gte_version so the doctest is harmless on older tmux.
- tests/test_server.py: skip test_server_access_list,
test_show_prompt_history, test_clear_prompt_history on <3.3; skip
test_run_shell_basic on <3.5; rewrite test_show_messages to spawn
a control-mode client via the existing fixture and pass
target_client.
- tests/test_pane.py: skip test_split_percentage on <3.5 since
split-window -p was broken in 3.4 (fixed per CHANGES 3.4 TO 3.5).
@tony
tonyforce-pushed the tmux-parity branch 2 times, most recently from 0fdcb8e to 78d4247CompareMay 10, 2026 00:40
tony added a commit that referenced this pull request May 10, 2026
…matrix
why: PR #653 builds failed on tmux 3.2a-3.5 (3.6/master cancelled as
siblings). Investigation traced six categories: methods that need an
attached client, tests/doctests missing version guards, and a tmux
upstream regression in run-shell on 3.3a/3.4.
what:
- Server.show_messages: add target_client kwarg; cmd-show-messages.c
uses format_create_from_target without -T/-J, so a TTY-less CI
server raises 'no current client' unless -t <client> is supplied.
- Server.server_access: add 3.3+ version guard (server-access was
introduced in tmux 3.3 per CHANGES FROM 3.2a TO 3.3).
- Server.run_shell doctest: drop stdout assertion. tmux 3.3a/3.4 do
not pipe run-shell stdout back through cmdq; restored upstream in
3.5.
- Server.{command_prompt,confirm_before,show_prompt_history,
clear_prompt_history} doctests: gate interactive demos behind
has_gte_version so the doctest is harmless on older tmux.
- tests/test_server.py: skip test_server_access_list,
test_show_prompt_history, test_clear_prompt_history on <3.3; skip
test_run_shell_basic on <3.5; rewrite test_show_messages to spawn
a control-mode client via the existing fixture and pass
target_client.
- tests/test_pane.py: skip test_split_percentage on <3.5 since
split-window -p was broken in 3.4 (fixed per CHANGES 3.4 TO 3.5).
tony added a commit that referenced this pull request May 10, 2026
…matrix
why: PR #653 builds failed on tmux 3.2a-3.5 (3.6/master cancelled as
siblings). Investigation traced six categories: methods that need an
attached client, tests/doctests missing version guards, and a tmux
upstream regression in run-shell on 3.3a/3.4.
what:
- Server.show_messages: add target_client kwarg; cmd-show-messages.c
uses format_create_from_target without -T/-J, so a TTY-less CI
server raises 'no current client' unless -t <client> is supplied.
- Server.server_access: add 3.3+ version guard (server-access was
introduced in tmux 3.3 per CHANGES FROM 3.2a TO 3.3).
- Server.run_shell doctest: drop stdout assertion. tmux 3.3a/3.4 do
not pipe run-shell stdout back through cmdq; restored upstream in
3.5.
- Server.{command_prompt,confirm_before,show_prompt_history,
clear_prompt_history} doctests: gate interactive demos behind
has_gte_version so the doctest is harmless on older tmux.
- tests/test_server.py: skip test_server_access_list,
test_show_prompt_history, test_clear_prompt_history on <3.3; skip
test_run_shell_basic on <3.5; rewrite test_show_messages to spawn
a control-mode client via the existing fixture and pass
target_client.
- tests/test_pane.py: skip test_split_percentage on <3.5 since
split-window -p was broken in 3.4 (fixed per CHANGES 3.4 TO 3.5).
…ent, key-name flags
why: send-keys has many useful flags (reset terminal, hex input, repeat count,
format expansion, copy-mode commands) that were not exposed in the Python API.
what:
- Add reset (-R), copy_mode_cmd (-X), repeat (-N), expand_formats (-F),
hex_keys (-H), target_client (-c, 3.4+), key_name (-K, 3.4+) parameters
- Version-gate target_client and key_name with has_gte_version("3.4")
- Add SendKeysCase NamedTuple parametrized tests for all new flags
tony added 21 commits May 10, 2026 05:40
…coverage
why: codecov/project failed because the new wrappers' parameter
branches were untested (patch coverage 40.97%). Server.display_menu
had no test at all; Pane.display_popup had two ad-hoc tests covering
only a handful of flags.
what:
- tests/test_pane.py: replace test_display_popup_runs_command and
test_display_popup_with_dimensions with a parametrized
test_display_popup_flags driven by DisplayPopupCase, exercising
basic, dimensions, position, start_directory, and the 3.3+ flags
(title, border_lines, style, border_style, environment). Add
test_display_popup_close_on_success for the -EE branch in
isolation, test_display_popup_mutual_exclusion for the ValueError
guard, and test_display_popup_close_existing for the -C branch.
- tests/test_server.py: add DisplayMenuCase + test_display_menu_flags
covering basic, title, position, target_pane, starting_choice,
and the 3.3+ flags (border_lines, style, border_style). Per
Server.display_menu's own docstring the wrapper cannot run under
ControlMode (tty.sy=0 makes menu_prepare return NULL and the call
hangs); the test stubs server.cmd to capture and assert the
constructed argv, the only deviation from the suite's
"use real tmux" pattern. The deviation is documented in the test
docstring per AGENTS.md guidance.
Patch coverage on server.py 62%→68%, pane.py 64%→70% (line-only;
branch coverage gains are larger since each parametrized case
exercises a distinct `if param is not None:` branch).
why: pane.py and server.py inlined `from libtmux.common import
has_gte_version` and `import warnings` ~15 times across method
bodies, with no circular-import reason. Each repetition is slop
that obscures the module's dependencies and prevents ruff from
grouping imports. AGENTS.md "code should not declare what it needs
over and over" applies in spirit.
what:
- Hoist `import warnings` and `from libtmux.common import
has_gte_version` to the top of pane.py.
- Hoist `from libtmux.common import has_gte_version` to the top of
server.py (warnings was not used there).
- Drop the inline imports inside ~12 method bodies.
- Update tests/test_pane_capture_pane.py:test_capture_pane_trim_trailing_warning
to monkeypatch `libtmux.pane.has_gte_version` instead of
`libtmux.common.has_gte_version` — `from X import Y` binds Y in
the importer's namespace, so once hoisted the pane module's
binding is what the wrapper resolves at call time.
why: tmux's display-menu accepts -H selected-style (3.4+),
-M always-mouse (3.5+), and -O stay-open (3.2+). The wrapper
omitted all three, leaving callers without a way to highlight the
selected item, force mouse mode, or keep the menu open after a
release.
what:
- Add selected_style: str | None (-H), mouse: bool | None (-M),
stay_open: bool | None (-O) parameters to Server.display_menu.
- selected_style and mouse are version-gated with warnings.warn on
unsupported tmux; stay_open ships unconditionally (3.2+ is older
than the project's minimum).
- Hoist `import warnings` into server.py top-level imports.
- Extend test_display_menu_flags with three new cases
(with_stay_open, with_selected_style_v34, with_mouse_v35) and
teach the assertion to skip bool values (which emit only the
switch, not a value).
why: tmux's display-popup accepts three flags the wrapper omitted:
- -B (3.3+) opens the popup with no border at all and overrides
-b border-lines unconditionally (cmd-display-menu.c, lines =
BOX_LINES_NONE)
- -k (3.6+) dismisses the popup on any keypress after the inner
command exits (cmd-display-menu.c, POPUP_CLOSEANYKEY)
- -N (3.6+) clears all auto-close flags so the popup is not
auto-dismissed (cmd-display-menu.c, flags = 0)
what:
- Add no_border (-B), close_on_any_key (-k), no_keys (-N) kwargs
to Pane.display_popup with the right per-flag version guards.
- Reject no_border=True + border_lines=... with ValueError, mirroring
the existing close_on_exit/close_on_success guard. tmux's -B
overrides -b regardless, so the combination is meaningless.
- Extend test_display_popup_flags with no_border_v33,
close_on_any_key_v36, no_keys_v36 cases.
- Add test_display_popup_no_border_with_border_lines_rejects.
why: tmux's command-prompt accepts three flags the wrapper omitted:
- -F (3.3+) passes the template through args_make_commands_prepare
so format strings expand
- -l (3.6+) disables splitting the prompt on commas — treat the
whole prompt as a single literal
- -e (post-3.6) closes the prompt when the user empties it via
backspace (PROMPT_BSPACE_EXIT)
what:
- Add expand_format (-F), literal (-l), bspace_exit (-e) kwargs
to Server.command_prompt with per-flag version guards.
- Add test_command_prompt_extra_flags using the monkeypatch argv
pattern from test_display_menu_flags. End-to-end behaviour for
these flags depends on tmux internals (format expansion, comma
splitting, backspace exit) that are awkward to drive headless;
the argv check confirms the wrapper emits the right flag.
Server(fix[command_prompt]): gate bspace_exit on tmux 3.7+
why: command_prompt(bspace_exit=True) emitted -e unconditionally, but
the flag was added upstream after tmux 3.6 and is not in any
tagged release (verified: tag --contains returns empty; tmux 3.6a
errors with "unknown flag -e"). The wrapper would break on every
released version. Sibling literal flag (3.6+) shows the correct
guard pattern.
what:
- bump TMUX_MAX_VERSION from "3.6" to "3.7" so master compares as
"3.7-master" >= "3.7", letting bspace_exit work on master while
still failing the gate on tagged releases 3.2a-3.6a
- guard the -e append with has_gte_version("3.7"); warn-and-ignore
on older versions, matching the literal pattern at the same site
- update test_command_prompt_extra_flags[bspace_exit] gate to "3.7"
so the case skips on every currently-released tmux
- bump test_version_parsing[next_version] fixture from "next-3.7" to
"next-3.8" since "3.7" is no longer strictly greater than the new
TMUX_MAX_VERSION
why: tmux's copy-mode accepts two flags the wrapper omitted:
- -s source-pane (3.2+) lets a pane display another pane's history
in copy mode — useful for scrolling/copying from one pane into an
editor in another
- -d (3.5+) page-down on entry if already in copy mode
what:
- Add page_down (-d, version-gated to 3.5+) and source_pane (-s,
unconditional since 3.2 is older than the project minimum) to
Pane.copy_mode.
- Add test_copy_mode_source_pane (cross-pane history exercise) and
test_copy_mode_page_down (3.5+ skipif).
why: tmux's choose-tree accepts a much larger surface than the
wrapper exposed (cmd-choose-tree.c:36, args="F:f:GK:NO:rstwyZ").
Real users need at least format/filter/sort to drive the chooser
programmatically, plus -Z to zoom while picking.
what:
- Add format_string (-F), filter_expression (-f), sort_order (-O),
reverse (-r), zoom (-Z) kwargs to Pane.choose_tree.
- Use filter_expression rather than filter to avoid shadowing the
Python builtin (ruff A002).
- Add test_choose_tree_with_flags exercising all five.
why: capture_pane hardcoded -p (pipe to caller's stdout) and offered
no way to send the capture into a named tmux buffer for later cross-
pane consumption — a real feature of cmd-capture-pane.c
(args="ab:CeE:JMNpPqS:Tt:").
what:
- Add to_buffer: str | None kwarg. When set, the wrapper omits -p,
emits -b <buffer> and returns None instead of stdout.
- Use @t.overload to keep the existing list[str] return type for the
default path; only the to_buffer-set call returns None.
- Add test_capture_pane_to_buffer that captures into a named buffer
and verifies the marker survived via show-buffer / delete-buffer.
why: tmux's show-messages takes -T (list terminal capabilities) and
-J (print job server summary) in addition to the message-log default
(cmd-show-messages.c:41 args="JTt:"). The wrapper docstring already
referenced both, but only -t was exposed. -T and -J are early-return
paths in cmd-show-messages.c that don't go through
format_create_from_target, so they work clientless — handy for
debugging tmux from a headless test run.
what:
- Add terminals (-T) and jobs (-J) bool kwargs to
Server.show_messages.
- Update the docstring to note the clientless modes.
- Add test_show_messages_terminals_jobs which exercises both modes
without spinning up a control_mode client.
…rite
why: tmux's server-access supports forcing a user to attach
read-only (-r) or allowing read-write attach (-w) — both implicitly
allow the user if not yet in the ACL (cmd-server-access.c:108-145).
The wrapper omitted both, leaving callers without the controls that
make this command useful.
what:
- Add read_only (-r) and write (-w) bool kwargs to
Server.server_access. Mutually exclusive — tmux rejects -r -w with
"cannot be used together", and the wrapper raises ValueError early
to give a clearer Python-side trace.
- Add test_server_access_read_only_write_mutex for the guard and
test_server_access_argv (monkeypatch capture) for the emitted
flags. server-access's actual side effect requires real OS users
and ACL state, so end-to-end testing is out of scope.
why: test_detach_client and test_detach_client_no_target_detaches_all_session_clients
covered the same path (no target_client → -s session_id scoping)
with identical fixture shape; the latter is strictly stronger
because it asserts on two attached clients rather than one. Keeping
both adds maintenance cost without adding signal.
what:
- Remove test_detach_client. The remaining tests (no_target_*,
target_client, all_clients_session_scoped) cover the three real
branches of Session.detach_client.
why: BufferCase already had an `append: bool | None` field but no
parametrisation case actually exercised it — the append behaviour
lived in a duplicative test_buffer_append function. The remaining
buffer tests (delete, save_load, save_append, list_buffers) test
genuinely different operations than set+show variations and stay
as their own test functions.
what:
- Add a `set_show_append` BufferCase that seeds the buffer with
"first" and appends "_second" via append=True, asserting the
concatenated content.
- Update test_buffer_set_show to interpret the existing `append`
field by seeding the buffer and passing append=True.
- Remove the standalone test_buffer_append function.
why: test_capture_pane_quiet, test_capture_pane_alternate_screen,
and test_capture_pane_mode_screen each had the same "call with one
flag, assert isinstance(result, list)" shape. Three almost-identical
3-line functions are exactly the case parametrised tests are for.
The existing CAPTURE_PANE_CASES harness is intentionally focused on
output-content assertions (run a command, check pattern in output)
— folding flag smoke-tests into it would muddle that purpose. A
small dedicated parametrise is the right fit.
what:
- Replace the three smoke functions with one parametrised
test_capture_pane_flag_smoke driving (kwargs, min_tmux_version)
cases for quiet, alternate_screen, and mode_screen (3.6+).
why: a previous commit annotated new params on existing methods but
left the entirely-new methods unmarked. After the parity branch, ~55
brand-new public wrappers ship without a "versionadded" hint, so
users can't tell from the docs which API landed in 0.45 vs an older
release. The Weave review's three reviewers all flagged this
(consensus: Suggestion → Important).
what:
- Insert `.. versionadded:: 0.45` into the docstring of every new
public method on Server, Session, Window, and Pane.
- 27 methods on server.py, 5 on session.py, 7 on window.py, 16 on
pane.py.
- Existing methods (capture_pane, display_message, Pane.select,
Window.select, …) are left alone — they predate 0.45.
why: test_show_messages_terminals_jobs assumed -T/-J short-circuit
before client lookup. That only holds on tmux >= 3.6, after the
upstream "Allow show-messages to work without a client" change
added CMD_CLIENT_CANFAIL to cmd_show_messages_entry. On
3.2a/3.3a/3.4/3.5 the command queue rejects the call with
"no current client" before cmd_show_messages_exec runs, so the
clientless codepath is unreachable.
what:
- pytest.skip on tmux < 3.6 via has_gte_version("3.6")
- replace docstring rationale with the actual upstream cause
why: parity tests deliberately exercise version-specific tmux
behaviour, so a failure on one tmux-version row should not cancel
sibling jobs. With fail-fast on, a single failing version makes
gh pr checks show all six rows as red and hides which versions
actually pass.
what:
- set strategy.fail-fast: false on the build matrix
why: command_prompt's -e flag is gated on tmux 3.7+ in the wrapper
(it's a master-only addition, upstream 1e5f93b7, not in any 3.6
release). The test parametrize had min_tmux_version="3.3", which
matched the -b flag the wrapper always emits, but the bspace_exit
success path needs the higher gate to actually exercise -e being
appended on a tmux that supports it.
what:
- bump min_tmux_version for the bspace_exit case from "3.3" to "3.7"
- update the comment: the 3.7 floor is the -e flag's wrapper gate,
not the -b minimum
why: test_allows_next_version asserts has_gt_version(TMUX_MAX_VERSION)
for a mocked "tmux next-3.7" parse. A prior commit bumped
TMUX_MAX_VERSION to "3.7", so parsed "3.7" is no longer strictly
greater than the max — assertion fails on every tmux job. Mirror
the same fixture bump already applied to tests/test_common.py.
what:
- TMUX_NEXT_VERSION "3.7" -> "3.8" so the parsed version stays one
minor ahead of TMUX_MAX_VERSION
…SION 3.7
why: Record 0.56.x user-facing additions from the parity branch.
what:
- What's new: interactive command wrappers, buffer I/O suite, key
bindings & shell execution, window/pane manipulation parity, filled-in
flag coverage on existing methods, control_mode pytest fixture
- Bug fixes: Window.move_window() refresh after move
- Development: TMUX_MAX_VERSION bumped to 3.7
docs(CHANGES,fixtures) cross-link 0.56.x entries to autodoc
why: the 0.56.x section listed dozens of new methods, classes, and
the control_mode fixture as plain backticked text. Converting to
MyST roles with the ~-prefix shortlink (matching the 0.55.0
precedent) turns the changelog into a navigable index — every
method name renders as a clickable link to its autodoc entry.
what:
- CHANGES: convert method/class/data references in the 0.56.x
block to {meth}/{class}/{data}/{mod}/{fixture} roles, using the
~libtmux.X.Y form so rendered text shows just the short name
(matching 0.55.0 style).
- CHANGES (correctness): Window.wait_for is actually
Server.wait_for (lives in src/libtmux/server.py:489);
Session.rotate doesn't exist (dropped); send_prefix is on Pane,
not Server.
- docs/api/testing/pytest-plugin/fixtures.md: add an explicit
autofixture entry for libtmux.pytest_plugin.control_mode in the
Factories section so {fixture}`control_mode` resolves to a
stable anchor.
- Verified: ruff/mypy/pytest clean; just build-docs succeeds with
no new warnings; rendered HTML confirms every role resolves to
a clickable internal link.
docs(CHANGES) drop unsupported values_only= from show_options entry
why: 0.56 added values_only= to the private _show_options_raw() but
it was never exposed on the public show_options() — and cannot
be without changing the dict return contract (values_only=True
emits bare values, not name/value pairs the parser expects).
The CHANGES claim was aspirational; correct it to match shipped
behavior. quiet= remains and is now actually wired through (see
preceding commit).
what:
- CHANGES (0.56.x): show_options entry now lists only `quiet=` as
a new kwarg.
why: 0.56 added quiet= and values_only= to the private
_show_options_raw() but never threaded them through the three
layers above (_show_options_dict, _show_options, show_options),
so the public method silently lacked the kwargs. Calling
Server().show_options(quiet=True) raised
TypeError: got an unexpected keyword argument 'quiet'.
what:
- _show_options_dict, _show_options, show_options: add
quiet: bool | None = None to each signature and forward it down
the call chain to _show_options_raw.
- show_options: document the new kwarg with versionadded:: 0.56.
- tests/test_options.py: add test_show_options_quiet_public to
exercise the public path and prevent this class of "private kwarg
not threaded through" regression.
- values_only= is intentionally NOT exposed on the public method:
it changes tmux output to bare values, which is incompatible with
show_options' dict return contract. Future work if needed.
why: pane.swap(move_up=True) raised TypeError because target was
positionally required, contradicting the docstring claim that
move_up/move_down "overrides target". The body also emitted
-s <target> unconditionally even with -U/-D.
what:
- Make target optional with default None
- Raise LibTmuxException for missing/conflicting flag combinations
(no flags, both move flags, target combined with a move flag)
- Skip -s <target> when target is None
- Update docstring: "Mutually exclusive with target" + versionchanged 0.56
- Add tests covering move_up/move_down happy path and three invalid
argument combinations
tony added 4 commits May 10, 2026 05:49
…per-client loop
why: tmux's `detach-client -s <session>` takes priority over -t and -a
inside cmd-detach-client.c — when -s is set tmux iterates every
client whose session matches and detaches them, ignoring the target
client and the -a flag. Behavior is identical at 3.2a through master
(verified: cmd-detach-client.c unchanged in `git log 3.6..master`).
The previous unconditional `tmux_args += ("-s", session_id)` made
`target_client=X` a no-op (everything in the session got detached)
and `all_clients=True, target_client=X` lose the target preservation.
test_detach_client_target_client and
test_detach_client_all_clients_session_scoped failed on every
released tmux for this reason.
what:
- when all_clients=True and target_client is set, enumerate
`list-clients -t <session>` and issue per-client `detach-client -t
<client>` for non-target clients (session-scoped, target preserved)
- when target_client is set without all_clients, pass -t alone — do
not append -s, which would override -t in tmux's parser
- only fall back to `-s <session>` when target_client is omitted (the
intentional "detach everything in this session" path)
- add Notes block documenting divergence from tmux's server-wide -a
(cites cmd-detach-client.c) and update target_client / all_clients
parameter docstrings
…roup APIs
why: the previous Session.detach_client(all_clients=True, target_client=X)
combination synthesized a "session-scoped except X" semantic that tmux
itself does not expose: cmd-detach-client.c (lines 77-90) early-returns
on -s, so -s shadows both -t and -a — making the original single-call
implementation silently broken and motivating the per-client list-clients
loop in bd20b7d. tmux's flag groups are deliberately non-composable
beyond -a -t (see tmux commit 3dc7b805 reordering the man page to put -s
first, "Prompted by jmc"), and tmux's only built-in keybinding is bare
`bind d { detach-client }`. Mirroring tmux's flag groups directly — one
method per group, one subprocess call per method — eliminates the loop,
the partial-failure semantics, and the race window between list-clients
and per-client detaches, while making the libtmux surface 1-to-1 with
tmux's. The method is unreleased, so the breaking parameter change is
contained.
what:
- Session.detach_client: drop all_clients parameter and the per-client
list-clients loop; map to `detach-client -s <session_id>` (no target)
or `detach-client -t <client>` (target). Single subprocess call.
Replace the divergence-explaining Notes block with a See Also pointing
at Server.detach_all_clients.
- Server.detach_all_clients: new method mapping to `detach-client -a
[-t keep_client]` for server-wide detach. Single subprocess call.
Documented `versionadded:: 0.56`. tmux's -a always preserves one
client; keep_client lets callers control which.
- tests/test_session.py: drop test_detach_client_all_clients_session_scoped
(the synthesized combination no longer exists); drop now-unused
functools and ControlMode imports.
- tests/test_server.py: add test_detach_all_clients_keep_client_spans_sessions
(verifies -a crosses session boundaries, keep_client survives) and
test_detach_all_clients_no_keep_preserves_one (codifies tmux's
most-recently-active fallback contract).
- CHANGES: extend the "Interactive tmux commands now scriptable" entry to
reference Server.detach_all_clients and document the flag-group split.
why: Earlier commit left Session.detach_client(target_client=X)
mapping to bare `tmux detach-client -t X`, but tmux resolves -t through
cmd_find_client (cmd-find.c:1289-1329) over the global &clients queue
with no session filter — so the call detaches client X regardless of
which session it is attached to. CMD_CLIENT_TFLAG (declared on line 42
of cmd-detach-client.c) routes -t to a server-wide client lookup, the
same pattern used by lock-client, refresh-client, and suspend-client.
Living on Session implied session-scoping that the underlying tmux call
never enforces; a caller doing s1.detach_client(target_client=<s2's
client>) would silently cross session boundaries. Moving the -t path
to Server.detach_client makes the receiver match the actual scope tmux
honors. Each detach_client wrapper now mirrors exactly one tmux flag
group, with one subprocess call.
what:
- Session.detach_client: drop target_client parameter; method now only
maps to `detach-client -s <session_id>` — the only flag group that is
genuinely session-scoped. Update See Also to reference the new
Server.detach_client and existing Server.detach_all_clients.
- Server.detach_client: new method mapping to `detach-client [-t
<target_client>]`, mirroring tmux's server-wide -t resolution.
Follows the keyword-only optional-target pattern used by sibling
client methods (suspend_client, lock_client, refresh_client). Without
target_client, tmux falls back to the most-recently-active client.
- Server.detach_all_clients: docstring See Also updated to reference the
new Server.detach_client (no behavior change).
- tests/test_session.py: drop test_detach_client_target_client (the
parameter no longer exists on Session). Keep
test_detach_client_no_target_detaches_all_session_clients.
- tests/test_server.py: add test_detach_client_target_client_spans_sessions
(concretely demonstrates server-wide resolution: detaches a client
attached to a different session) and test_detach_client_no_target_uses_active
(verifies the most-recently-active fallback).
- CHANGES: list all three detach-client wrappers explicitly with their
flag-group mapping; replace the round-1 paragraph that conflated
Session.detach_client's -s/-t with a precise three-method breakdown.
…se pre-release flag-coverage gaps
why: while preparing 0.56 for release, three flag-coverage gaps were
found by review against pinned tmux source trees under
~/study/c/tmux-<version>/. (1) Server.display_menu emitted -C/-b/-s/-S
unconditionally even though tmux's cmd-display-menu.c only added these
flags in 3.4 (.args = "c:t:OT:x:y:" on 3.2a–3.3a vs
"b:c:C:H:s:S:Ot:T:x:y:" on 3.4); calls on 3.3/3.3a would silently
construct invocations tmux's getopt rejects. (2) Server.confirm_before
has a 3.3+ entry guard for -b but emitted -c/-y unconditionally —
both added in tmux 3.4 (.args = "bp:t:" on 3.3 vs "bc:p:t:y" on 3.4),
so the same silent-bad-flag problem applied. (3) Pane.display_popup
lacked target_client= entirely, despite cmd-display-popup having
exposed -c target-client since tmux 3.2a (libtmux's TMUX_MIN_VERSION).
Server.display_menu already had target_client; the asymmetry was
incompletion, not design. None of these reach a released version,
so this is internal hygiene, not a bug fix CHANGES entry.
what:
- Server.display_menu: wrap -C/-b/-s/-S emissions in
has_gte_version("3.4") guards using the established
warnings.warn(..., stacklevel=2) pattern that already covers -H
and -M. Update parameter docstrings to note 3.4+ requirement.
- Server.confirm_before: wrap -c/-y emissions in
has_gte_version("3.4") guards with the same warnings.warn
fallback. Method-entry 3.3+ guard remains for -b. Update
parameter docstrings.
- Pane.display_popup: add target_client: str | None = None
keyword-only parameter; emit ("-c", target_client) when set.
No version guard (3.2a covers it). Document parameter and
cross-reference Server.display_menu's symmetric target_client.
- tests/test_server.py: rename four DISPLAY_MENU_CASES from
_v33 → _v34 (with_starting_choice, with_border_lines,
with_style, with_border_style) and update each min_tmux_version
from "3.3" / None to "3.4". Labels were misleading; the flags
these cases exercise only exist on 3.4+.
- tests/test_pane.py: add test_display_popup_target_client smoke
test exercising the new parameter via control_mode and a
tmp_path marker file. Mirrors the existing close_on_success
test pattern.
- CHANGES: extend the existing display_popup line in "Filled-in
flag coverage" to mention `target_client=` (-c). No "Bug fixes"
entry — the version-guard corrections are pre-release fixes to
unreleased 0.56 code.
@tony
tony merged commit 3631e76 into masterMay 10, 2026
13 checks passed
@tony
tony deleted the tmux-parity branch May 10, 2026 12:37
Sign up for freeto 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.

2 participants

@tony@github-advanced-security