tmux parity: add interactive command coverage and align flag semantics with tmux#653
Draft
tmux parity: add interactive command coverage and align flag semantics with tmux#653
Conversation
why: libtmux wraps ~28 of tmux's ~88 commands (~32% coverage). Need tooling to systematically audit gaps, compare across tmux versions, and guide implementation of new command wrappers. what: - Add .claude-plugin/ with manifest, commands, agent, skill, and scripts - /parity-audit: generate full feature parity report (commands, flags, format variables, options) - /version-diff: compare tmux features across 41 version worktrees - /implement-command: guided workflow for wrapping new tmux commands - parity-analyzer agent: auto-triggers on natural language parity queries - tmux-parity skill: shared domain knowledge with reference files (command mapping, implementation patterns, C source navigation) - Extraction scripts: parse tmux cmd-*.c and libtmux .cmd() invocations
…uto-discovery why: Claude Code auto-discovers plugin components at the project root, not inside .claude-plugin/. Agent wasn't showing up because it was nested under .claude-plugin/agents/. what: - Move agents/, commands/, skills/ to project root - Keep scripts/ in .claude-plugin/ (not auto-discovered) - Remove custom path overrides from plugin.json - Update cross-references between components
…d discovery why: Claude Code discovers project commands from .claude/commands/ and agents from .claude/agents/, not top-level directories. what: - Move 3 commands to .claude/commands/ - Move parity-analyzer agent to .claude/agents/ - Remove now-empty top-level commands/ and agents/ dirs
…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
… flags why: select-pane has rich flag support for directional navigation, pane marking, and input control that was not exposed in the Python API. what: - Add direction (-D/-U/-L/-R), last (-l), keep_zoom (-Z), mark (-m), clear_mark (-M), disable_input (-d), enable_input (-e) parameters - Reuse existing ResizeAdjustmentDirection enum for direction flags - Skip deprecated -P (style) and -g (show style) flags - Add tests for direction, last pane, mark/clear, and input toggle
…, and style flags why: display-message supports many useful flags for format queries and output control that were not exposed in the Python API. what: - Add format_string (-F), all_formats (-a), verbose (-v), no_expand (-I), target_client (-c), delay (-d), notify (-N), list_formats (-l, 3.4+), no_style (-C, 3.6+) parameters - Version-gate list_formats and no_style with has_gte_version - Fix cmd argument handling: only pass when non-empty - Add DisplayMessageCase NamedTuple parametrized tests
why: select-layout supports flags for spreading panes evenly and cycling through layouts that were not exposed in the Python API. what: - Add spread (-E), next_layout (-n), previous_layout (-o) parameters - Validate mutual exclusion between layout string and flag parameters - Add tests for spread, next/previous cycling, and mutual exclusion
…number flags why: move-window supports flags for positioning, conflict resolution, and renumbering that were not exposed in the Python API. what: - Add after (-a), before (-b), no_select (-d), kill_target (-k), renumber (-r) parameters to move_window() - Add tests for kill_target, renumber, and no_select behaviors
why: new-session supports flags for detaching other clients, suppressing initial sizing, and specifying config files that were not exposed. what: - Add detach_others (-D), no_size (-X), config_file (-f) parameters - Skip -A (attach-or-create) as it requires a terminal and does not work in libtmux's programmatic non-terminal context - Add test for config_file parameter
why: new-window supports flags for replacing existing windows at a target index and selecting existing windows by name that were not exposed. what: - Add kill_existing (-k) and select_existing (-S) parameters to new_window() - -k destroys existing window at target index before creating new one - -S selects existing window with matching name instead of creating new - Add tests for both flags
why: split-window supports -p for percentage-based sizing which is more intuitive than the absolute cell count provided by the existing size parameter. what: - Add percentage (-p) parameter to Pane.split(), mutually exclusive with size - Validate that size and percentage are not both specified - Add tests for percentage split and mutual exclusion
…kup flags
why: capture-pane supports flags for alternate screen capture, silent error
handling, and markup escaping that were not exposed.
what:
- Add alternate_screen (-a), quiet (-q), escape_markup (-M, 3.6+) parameters
- Version-gate escape_markup with has_gte_version("3.6")
- Add tests for quiet, alternate_screen, and escape_markup flags
…en to set_environment why: show-options supports -q (quiet) and -v (values only) flags, and set-environment supports -F (format expansion) and -h (hidden) flags that were not exposed in the Python API. what: - Add quiet (-q) and values_only (-v) to _show_options_raw() - Add expand_format (-F) and hidden (-h) to set_environment() - Add tests for quiet show_options, hidden env vars, and format expansion
…story
why: clear-history is useful for clearing pane scrollback buffers,
especially important for test isolation and monitoring workflows.
what:
- Add clear_history() method with clear_pane (-H, 3.4+) parameter
- Version-gate -H flag with has_gte_version("3.4")
- Add test verifying history is cleared after sending commands
…window why: swap-pane and swap-window are core layout manipulation commands needed for programmatic pane and window reordering. what: - Add Pane.swap() with target, detach (-d), move_up (-U), move_down (-D), keep_zoom (-Z) parameters wrapping swap-pane - Add Window.swap() with target, detach (-d) parameters wrapping swap-window - Add tests verifying pane indices and window indices swap correctly
why: break-pane is essential for layout management, allowing a pane to be
moved into its own window programmatically.
what:
- Add break_pane() method with detach (-d) and window_name (-n) parameters
- Use -P -F#{window_id} to capture new window ID from output
- Return Window object via Window.from_window_id()
- Use server.cmd with explicit -s flag to avoid auto-target conflicts
- Add tests for basic break and named window
why: join-pane is the inverse of break-pane, needed for programmatically merging panes between windows. what: - Add join() method with vertical (-v/-h), detach (-d), full_window (-f), size (-l), before (-b) parameters - Accept Pane, Window, or string target ID - Use server.cmd with explicit -s/-t to avoid auto-target conflicts - Add roundtrip test (break + join) and horizontal join test
…respawn-window why: respawn-pane and respawn-window are needed for restarting processes in panes/windows without destroying and recreating them. what: - Add Pane.respawn() with shell, start_directory (-c), environment (-e), kill (-k) parameters wrapping respawn-pane - Add Window.respawn() with same parameters wrapping respawn-window - Add tests verifying respawn with kill on active panes and windows
why: pipe-pane is needed for monitoring, logging, and capturing pane output to external commands or files programmatically. what: - Add pipe() method with command, output_only (-O), input_only (-I), toggle (-o) parameters wrapping pipe-pane - Calling with no command stops piping - Add test piping to file via cat, verifying output captured
why: run-shell executes shell commands in the tmux server context, useful for background tasks and capturing command output programmatically. what: - Add Server.run_shell() with background (-b), delay (-d), capture (-C), target_pane (-t) parameters - Returns stdout when not in background mode, None otherwise - Add tests for basic command execution and background mode
…rotate why: Window navigation commands (last/next/previous) and pane rotation are commonly needed for programmatic window management workflows. what: - Add Session.last_window() wrapping last-window - Add Session.next_window() wrapping next-window - Add Session.previous_window() wrapping previous-window - Add Window.rotate() wrapping rotate-window with direction_up (-U), keep_zoom (-Z) parameters - Add tests for all navigation commands and rotation
why: link-window, unlink-window, and wait-for are needed for sharing windows across sessions and for synchronization between tmux commands. what: - Add Window.link() wrapping link-window with target_session, target_index, kill_existing (-k), after (-a), before (-b), detach (-d) parameters - Add Window.unlink() wrapping unlink-window with kill_if_last (-k) parameter - Add Server.wait_for() wrapping wait-for with lock (-L), unlock (-U), set_flag (-S) parameters - Add tests for link/unlink roundtrip and wait-for set_flag
…pping tmux buffer commands why: Paste buffer management is needed for programmatic clipboard-like operations between panes and for exporting/importing text data. what: - Add Server.set_buffer() wrapping set-buffer with append (-a) and buffer_name (-b) parameters - Add Server.show_buffer() wrapping show-buffer with buffer_name (-b) - Add Server.delete_buffer() wrapping delete-buffer with buffer_name (-b) - Add BufferCase NamedTuple parametrized tests for set/show cycle, named buffers, append, and delete
… buffer I/O why: Buffer file I/O and listing are needed for exporting pane content, importing data, and querying available buffers programmatically. what: - Add Server.save_buffer() wrapping save-buffer with append (-a), buffer_name (-b), and file path - Add Server.load_buffer() wrapping load-buffer with buffer_name (-b) and file path - Add Server.list_buffers() wrapping list-buffers returning raw output - Add tests for save/load cycle, append mode, and buffer listing
why: paste-buffer is needed for programmatically inserting buffer content into panes, completing the buffer management API. what: - Add Pane.paste_buffer() with buffer_name (-b), delete_after (-d), no_trailing_newline (-r), bracket (-p), separator (-s) parameters - Add test verifying buffer content appears in pane after paste
…popup why: display-popup (3.2+) creates overlay popups for running commands, useful in interactive tmux sessions. what: - Add display_popup() with command, close_on_exit (-E), close_on_success (-C), width (-w), height (-h), start_directory (-d) core parameters - Version-gate 3.3+ flags: title (-T), border_lines (-b), border_style (-s), environment (-e) - Note: requires attached client, cannot be tested in headless context
…display-popup" This reverts commit 4944756.
…ents why: list-clients is needed for monitoring connected clients and multi-client session management. what: - Add Server.list_clients() returning raw stdout lines - Returns empty list when no clients are attached - Add test verifying return type
why: source-file is needed for loading configuration files programmatically, useful for applying settings or initializing environments. what: - Add Server.source_file() with path and quiet (-q) parameters - Quiet mode suppresses errors for missing files - Add tests for sourcing a config and verifying option applied, and quiet mode
why: if-shell enables conditional tmux command execution based on shell command exit status, useful for scripted environment setup. what: - Add Server.if_shell() with shell_command, tmux_command, else_command, background (-b), target_pane (-t) parameters - Add tests for true branch and false-with-else branch
why: The pane/window split() and new_window() methods use the fused
form (f"-c{path}",) for the start-directory flag. The respawn methods
used the separated form ("-c", str(path)), diverging from the dominant
pattern in pane/window code.
what:
- Change Pane.respawn -c from ("-c", str(start_path)) to (f"-c{start_path}",)
- Change Window.respawn -c from ("-c", str(start_path)) to (f"-c{start_path}",)
why: last-pane -d disables input and -e enables input per tmux source (cmd-select-pane.c). The parameters were misnamed: detach mapped to -d (actually disables input, not detach) and disable_input mapped to -e (actually enables input). There is no "detach" flag for last-pane. what: - Replace detach/disable_input params with disable_input(-d)/enable_input(-e) - Match the pattern used by Pane.select() which already maps these correctly - Update docstrings and versionadded annotations
why: Pane.split() gained a percentage parameter for the -p flag but Window.split() which delegates to it neither accepts nor forwards it. what: - Add percentage parameter to Window.split() signature - Forward percentage to active_pane.split() - Fix size docstring to say "Cell/row count" (not "or percentage")
why: select-layout -o restores the old/saved layout (undo), while -p cycles to the previous layout preset. The previous_layout parameter was sending -o instead of -p. Verified in cmd-select-layout.c:89. what: - Change -o to -p for previous_layout flag - Update docstring to reference correct flag
why: new-session -f calls server_client_set_flags(), setting client flags like no-output and read-only. It does not load a config file (that is the top-level tmux -f flag). Verified in cmd-new-session.c:326. what: - Rename config_file param to client_flags - Change type from StrPath to str (flags are strings, not paths) - Remove pathlib.Path wrapping in implementation - Update test to use actual client flag value
why: run-shell -C parses the argument as a tmux command instead of a shell command (ARGS_PARSE_COMMANDS_OR_STRING). It does not capture output. Verified in cmd-run-shell.c:50. what: - Rename capture param to as_tmux_command - Update docstring to reflect actual semantics
why: command-prompt -N sets PROMPT_NUMERIC (accept only numeric input). It does not suppress command execution. Verified in cmd-command-prompt.c:161. what: - Rename no_execute param to numeric - Update docstring to reflect actual semantics
why: capture-pane -M captures from the mode screen (e.g. copy mode), not escape markup. Verified in cmd-capture-pane.c:132 and tmux CHANGES: "Add -M flag to capture-pane to use the copy mode screen." what: - Rename escape_markup param to mode_screen - Update docstring and warning message - Update test name and parameter usage
why: clear-history -H calls screen_reset_hyperlinks(), removing OSC 8 hyperlinks. It does not clear visible pane content. Verified in cmd-capture-pane.c:226. what: - Rename clear_pane param to reset_hyperlinks - Update docstring and warning message
why: copy-mode -q cancels all modes (window_pane_reset_mode_all), not "quiet". And -e exits copy mode when scrolling reaches the bottom of history, not "exit on copy". Verified in cmd-copy-mode.c and tmux manpage. Internal tmux name for -e is scroll_exit. what: - Rename quiet param to cancel - Rename exit_on_copy param to exit_on_bottom - Update docstrings to reflect actual semantics
…ling why: paste-buffer -r changes the inter-line separator from carriage return to newline. It does not suppress a trailing newline. Verified in cmd-paste-buffer.c: default sepstr is "\r", -r changes to "\n". what: - Rename no_trailing_newline param to linefeed_separator - Update docstring to reflect actual semantics
why: choose-tree -s starts with sessions collapsed and -w with windows collapsed. They do not filter to show only sessions/windows. Verified in tmux manpage and cmd-choose.c source. what: - Rename sessions_only to sessions_collapsed - Rename windows_only to windows_collapsed - Update docstrings to reflect actual semantics
why: rotate-window with no flags defaults to upward rotation in tmux (cmd-rotate-window.c:82). The code always injected -D in the else branch, making rotate() behave as downward instead of tmux's default. what: - Replace direction_up with separate upward/downward params - Only send -U/-D when explicitly requested - No flags sent by default (tmux default = upward)
…t 3.4+ why: The -b flag for confirm-before and command-prompt was added in tmux 3.3, not 3.4. Verified across version worktrees: tmux-3.2a has args "p:t:" while tmux-3.3 has "bp:t:". what: - Change "Requires tmux 3.4+" to "Requires tmux 3.3+" in both docstrings
why: The -C flag in tmux display-popup means "close any existing popup on the client" (server_client_clear_overlay), not "close on success exit code." The close-on-success behavior is achieved by passing -E twice (-EE), which sets POPUP_CLOSEEXITZERO in tmux's popup.c. what: - Fix close_on_success to emit -E -E instead of -C - Add close_existing parameter for the actual -C flag behavior - Update docstrings to document correct flag semantics
…er-move why: In tmux's cmd-move-window.c, the -r flag triggers session_renumber_windows() and returns CMD_RETURN_NORMAL immediately — the move logic on subsequent lines is never reached. The docstring incorrectly said "Renumber all windows after moving" implying both a renumber and a move occur. what: - Fix docstring to document -r as a standalone operation - Note that other parameters are ignored when renumber is used
why: tmux detach-client treats -s before -t, so always forcing a session target detached every client on the session instead of the requested client. what: - remove the unconditional session target from Session.detach_client - clarify all_clients plus target_client behavior in the docstring - add regressions for default and explicit target detach behavior
why: move-window can land on an index different from the requested target and can also change the owning session, leaving the returned Window stale. what: - refresh Window state after every successful move-window command - add regressions for relative move freshness - add a cross-session move regression with an explicit destination
why: recording the first client returned by list-clients can capture an existing attached client instead of the control-mode client spawned for the test. what: - wait for the spawned client pid to appear in list-clients - record client_name from the matching pid row - add a nested control-mode regression that verifies pid-to-name binding
…nup, socket_path
why: tempfile.mktemp() has a TOCTOU race; failure-path left open fds and live
subprocess; socket_path was silently ignored.
what:
- Replace tempfile.mktemp() + os.mkfifo() FIFO with os.pipe() pair (no
filesystem, no race condition)
- Add failure-path cleanup in __enter__: if retry_until fails, close _write_fd
and terminate subprocess before re-raising
- Handle socket_path: build socket_args checking socket_name first (-L),
then socket_path (-S), then empty
- Remove unused tempfile and pathlib imports
…_on_success combined
why: tmux counts -E flags via args_count(); 3x -E evaluates as != 2, falls
through to the args_has() branch and silently behaves like 1x -E
(close-on-any-exit), discarding the close_on_success intent entirely.
what:
- Add mutual-exclusion guard: raise ValueError when both close_on_exit and
close_on_success are True
- Assign message to variable first to satisfy EM101/TRY003 linting rules
…rsion("3.3")
why: Both methods unconditionally emit -b despite their docstrings noting it
requires tmux 3.3+. On older tmux this causes a hard command error instead
of the project-convention warn-and-skip behaviour.
what:
- confirm_before: add has_gte_version("3.3") guard; warn and skip -b on older tmux
- command_prompt: same
- Add lazy imports for warnings and has_gte_version inside each method,
matching the pattern already used in pane.py
… -b; scope detach-client to session
why: Warn-and-skip for -b would silently change semantics to blocking, hanging
the caller indefinitely. Session.detach_client() without -s was not actually
session-scoped, making it wrong on multi-session servers.
what:
- confirm_before / command_prompt: replace warnings.warn with LibTmuxException
when tmux < 3.3; -b is not optional, dropping it hangs the command queue
- Session.detach_client(): restructure arg building so no-target uses
-s self.session_id, target_client+all_clients uses -a -t, target_client
alone uses -t
- Update docstring: no-target now says "detaches all clients in this session"
- Update tests: test_detach_client and renamed test now assert 0 clients
remain (all session clients detached) rather than before-1
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #653 +/- ##
==========================================
- Coverage 51.19% 48.77% -2.42%
==========================================
Files 25 26 +1
Lines 2590 3401 +811
Branches 402 669 +267
==========================================
+ Hits 1326 1659 +333
- Misses 1094 1389 +295
- Partials 170 353 +183 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Member
Author
Code reviewFound 4 issues:
Lines 700 to 714 in 857384a
Lines 822 to 829 in 857384a
Lines 1119 to 1158 in 857384a
Lines 1202 to 1206 in 857384a 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
why: libtmux.test.control_mode does not exist; Sphinx cross-reference
would produce a broken link. Correct path is _internal.control_mode.
what:
- Replace ~libtmux.test.control_mode.ControlMode with
~libtmux._internal.control_mode.ControlMode in display_popup docstring
why: Both commands were added in tmux 3.3 but libtmux supports 3.2a.
Without a guard, callers on 3.2a get a raw "unknown command" tmux
error instead of a clean LibTmuxException with version context.
what:
- Add has_gte_version("3.3") guard to show_prompt_history
- Add has_gte_version("3.3") guard to clear_prompt_history
- Pattern matches confirm_before / command_prompt guards in same file
why: swap-window swaps window object pointers at tmux indices, changing
window_index on both objects. Not refreshing leaves stale state,
identical to the issue fixed in move_window by commit 3654a36.
what:
- Add self.refresh() after successful swap-window cmd
- Refresh target Window if target is a Window instance
- Remove manual refresh() calls from docstring examples (now automatic)
…_formats
why: -I opens pane stdin input mode (window_pane_start_input), not suppress
expansion. -l (tmux 3.4, commit 3be36952) is the correct literal flag.
list_formats mapped to -l with doc "List format variables" — but -l
suppresses expansion, not lists variables; -a (all_formats) already
covers listing, making list_formats a wrong duplicate.
what:
- Fix no_expand to emit -l with has_gte_version("3.4") guard + warn on older
- Remove list_formats param from both overloads, implementation, and docstring
- Update no_expand docstring: "-l flag. Requires tmux 3.4+"
- Remove list_formats test case; add no_expand test verifying literal output
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
This PR expands libtmux's tmux command parity across
Server,Session,Window, andPane, 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.ControlModenow bindsclient_nameto the spawned client by matchingclient_pid, which fixes multi-client races.Main Changes
New or expanded tmux command coverage
Serversupport for commands such asbind_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, anddisplay_menu.Session.detach_client()and window navigation helpers.Windowsupport formove_windowflags,select_layoutflags,last_pane,next_layout,previous_layout,rotate,swap,respawn,link, andunlink.Panecoverage fordisplay_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
move-window -ras standalone renumbering.Test and tooling support
ControlModeand pytest fixture support for commands that require a real attached client..claude/andskills/tmux-parity/to help maintain command coverage against tmux.Testing
Ran:
uv run ruff check . --fix --show-fixesuv run ruff format .uv run mypyuv run py.test --reruns 0 -vvvLatest full run:
1067 passed, 1 skippedNotes
origin/masteris broad because this branch includes both the parity feature work and the review-driven correctness fixes on top.