tmux-python / tmux-python/libtmux

Wrapper gaps surfaced by libtmux-mcp adoption of 0.56.0 (14 sites)

Open
#670 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement MCP
Dominant language
Python
Stars
1.2k
Forks
127
Avg merge
2h 13m
Merged PRs (30d)
1

Description

Summary

Adopting libtmux 0.56.0's new wrappers across tmux-python/libtmux-mcp (15 raw cmd() call sites → typed wrappers) surfaced 9 spots where the wrapper either doesn't exist or doesn't quite fit a tmux invocation that the underlying command supports. A follow-up sweep against tmux 3.6a's full cmd_entry flag schemas and format_table[] registry surfaced 4 more gap categories (gaps #10#13) plus an 8-token forward-looking set from tmux master post-3.6a (gap #14). tmux source citations below reference local copies at ~/study/c/tmux-3.6a/ (matching the upstream tmux/tmux@3.6a tag — the latest tmux release as of 2026-05-12) and link to the same files on GitHub at tmux/tmux/tree/3.6a.

Each gap is independently fixable; they cluster into five shapes (see end of issue).


1. Server.display_message doesn't exist

Site: Only Pane.display_message is wrapped (pane.py:625-774). Server has show_messages (message log) but no format-string evaluator.

Why tmux supports it: cmd-display-message.c:38-50 (local: ~/study/c/tmux-3.6a/cmd-display-message.c:38-50) — the entry's target is CMD_FIND_PANE with CMD_FIND_CANFAIL, so omitting -t falls back to the current/default pane and the format engine still resolves server-scoped variables like #{version} and #{socket_path}.

Where it bites callers: libtmux-mcp's get_server_info and _effective_socket_path need #{version} / #{socket_path} reads with no specific pane handle; they have to drop down to server.cmd("display-message", "-p", "#{…}"). Three sites in the MCP carry this workaround.

Proposed: add Server.display_message(format_string, *, get_text=True, target_client=…) returning list[str]. Same shape as Pane.display_message but no -t injection.


2. Window.display_message doesn't exist

Site: No wrapper at the Window level either.

Why tmux supports it: Same CMD_FIND_PANE | CMD_FIND_CANFAIL target as gap #1 (cmd-display-message.c:46); callers commonly pass -t @<window> for window-scoped reads like #{window_zoomed_flag}.

Where it bites callers: libtmux-mcp's resize_pane(zoom=…) needs the zoom state before deciding to toggle; with no Window.display_message, it must call window.cmd("display-message", "-p", "#{window_zoomed_flag}") directly.

Proposed: add Window.display_message (same signature as Pane.display_message), or generalize the Pane method to accept arbitrary targets.


3. Window.window_zoomed_flag is not a typed attribute on the dataclass

Site: window.py:53 — the Window dataclass declares many window_* fields but not window_zoomed_flag. mypy rejects window.window_zoomed_flag even after window.refresh().

Why tmux supports it: the variable is a first-class format token with a dedicated callback at format.c:2854-2864 (local: ~/study/c/tmux-3.6a/format.c:2854), formally registered in the format table at format.c:3557.

Where it bites callers: the umbrella issue libtmux-mcp#46 suggested swapping a display-message call for the ORM read — turned out it doesn't type-check, so the MCP reverted to cmd() with a comment.

Proposed: add window_zoomed_flag: str | None (and other common window_* format vars) as declared fields populated via refresh(). See gap #10 for the full undeclared token set across all object scopes.


4. Pane.reset() is still broken in 0.56.0 (open as #650)

Site: pane.py:2150-2153:

def reset(self) -> Pane:
    self.cmd("send-keys", r"-R \; clear-history")
    return self

The \; is tmux's interactive command separator. subprocess.run argv doesn't expand it — tmux sees a single argv "-R \; clear-history" and interprets the trailing \; clear-history as literal keys to send, never running clear-history. Pre-existing tracking issue: #650.

Why tmux supports both individually: send-keys -R is handled at cmd-send-keys.c:217-220 (local: ~/study/c/tmux-3.6a/cmd-send-keys.c:217); clear-history is its own entry in cmd-capture-pane.c.

Where it bites callers: libtmux-mcp's clear_pane (see tools/pane_tools/io.py) keeps a two-call workaround. Comment in the MCP cites this issue.

Proposed: split into two cmd() calls inside Pane.reset (the same workaround the MCP carries), or use tmux's \; separator via shell=True.


5. Pane.send_keys has no flag-only invocation

Site: pane.py:499-622. The wrapper always appends prefix + cmd (or copy_mode_cmd) as a trailing argv:

else:
    self.cmd("send-keys", *tmux_args, prefix + cmd)

There's no path that emits just tmux send-keys -R (reset flag, no key argument).

Why tmux supports it: cmd-send-keys.c:223-227 (local: ~/study/c/tmux-3.6a/cmd-send-keys.c:223) — when count == 0 and either -N or -R is set, the command returns CMD_RETURN_NORMAL without sending any keys. tmux's authors explicitly handle the flag-only case.

Where it bites callers: libtmux-mcp's clear_pane needs tmux send-keys -R (no keys). pane.send_keys("", reset=True, enter=False) produces tmux send-keys -R "" — likely a no-op in practice, but the trailing empty arg isn't what the underlying tmux command expects for a flag-only invocation. The MCP keeps pane.cmd("send-keys", "-R") for that reason.

Proposed: make the cmd parameter Optional[str] with default None; when None and a flag like reset or repeat is set, emit tmux send-keys <flags> with no trailing arg. Backward-compatible because cmd is currently positional-required.


6. Server.list_buffers() returns default-formatted lines, not raw names

Site: server.py:1643-1663 — calls tmux list-buffers with no -F, so the output is tmux's default template (buf_name: N bytes: …). No format= kwarg on the wrapper.

Why tmux supports it: cmd-list-buffers.c:35-44 (local: ~/study/c/tmux-3.6a/cmd-list-buffers.c:35) — entry usage is [-F format] [-f filter]; the -F template is read at line 56 with a builtin fallback, the -f filter at line 58.

Where it bites callers: libtmux-mcp's buffer GC needs just the names matching libtmux_mcp_* prefix. The wrapper returns lines like libtmux_mcp_abc: 5 bytes: "…"; parsing requires regex. The MCP keeps server.cmd("list-buffers", "-F", "#{buffer_name}") instead.

Proposed: add format: str | None = None and filter: str | None = None kwargs to Server.list_buffers. When format is set, pass -F <fmt>; otherwise keep the default tmux output for backward compat.


7. server.panes / QueryList doesn't expose tmux's -f C-filter

Site: [Server.panes at server.py:2060-2076](https://github.com/tmux-python/libtmux/blob/v0.56.0/src/libtmux/server.py#L2060) returns a QueryList[Pane] that filters in Python. Callers who want the **C-level filter** with format-expression predicates have no wrapper path; [fetch_objsin neo.py](https://github.com/tmux-python/libtmux/blob/v0.56.0/src/libtmux/neo.py#L248) always emits libtmux's own-F{format_string}` template.

Why tmux supports it: cmd-list-panes.c:37-42 (local: ~/study/c/tmux-3.6a/cmd-list-panes.c:37) — entry usage is [-as] [-F format] [-f filter]; the filter is read at line 125 and evaluated by format_true(expanded) at line 134, gating which panes appear in the output.

Where it bites callers: libtmux-mcp's search_panes fast path uses tmux list-panes -a -f '#{C/i:pattern}' -F '#{pane_id}' — pushes a regex match into tmux's C code, returning only matching pane IDs without capturing every pane in Python. With QueryList, the only option is "capture all panes in Python and filter post-hoc" — orders of magnitude slower for the common case. The MCP keeps server.cmd("list-panes", ...) for this site.

Proposed: add a filter: str | None = None kwarg to Server.panes (and the analogous Session.panes, Window.panes) that round-trips a tmux format expression through -f. Alternatively, a Server.list_panes(filter=…, format=…) low-level method.


8. LibTmuxException translation loses subcommand context

Site: wrappers like Session.last_window raise exc.LibTmuxException(proc.stderr) (session.py:298-318). The exception (exc.py:18-19 — a bare Exception subclass with no payload fields) carries the raw stderr list but not the subcommand name that produced it.

Why this matters: downstream wrappers (libtmux-mcp's handle_tool_errors translates LibTmuxException → ToolError(f"tmux error: {e}")) and any agent-facing error string lose the "which tmux command failed" context. Pre-0.56.0, the MCP built f"tmux {subcommand} failed: {stderr}" manually; the wrapper version surfaces only "tmux error: ['no last window']". One MCP test had to be updated to match the new shape.

Proposed: extend LibTmuxException (and friends) with an optional subcommand: str | None attribute. Format __str__ as f"{subcommand}: {stderr_text}" when present. Wrappers populate it implicitly (e.g., a _run_cmd("last-window", *args) helper that catches and re-raises with the subcommand).


9. Pane.cmd always injects -t <pane_id>, no warning on duplicate

Site: Pane.cmd at pane.py:178-212](https://github.com/tmux-python/libtmux/blob/v0.56.0/src/libtmux/pane.py#L178) always sets target = self.pane_idbefore delegating toServer.cmd. [Server.cmd at server.py:326 then emits -t <target> at the front of cmd_args regardless of what the caller passes in *args. If the caller also adds -t %X, tmux sees tmux <subcmd> -t %1 -t %1 and silently accepts the duplicate.

Why tmux tolerates it: tmux's args.c args_get() returns the last value of a repeated flag — so -t %1 -t %1 is functionally -t %1. Safe but masks caller bugs.

Where it bites callers: libtmux-mcp had two sites doing this (buffer_tools.py:275 and copy_mode.py:60-68) before the 0.56.0 wrappers were adopted; both were quietly accepted by tmux despite being incorrect Python. Discovered only because the new wrappers (which inject -t correctly) made the duplicate impossible to keep.

Proposed: when Pane.cmd (and Session.cmd, Window.cmd, Server.cmd) detect a -t already in *args, either (a) raise a clear TypeError, or (b) emit warnings.warn("Pane.cmd auto-injects -t; redundant -t in args") once per call site. Option (a) is safer; option (b) is gentler if the existing wild ecosystem relies on the no-op.


10. ~37 first-class format tokens aren't typed on Pane/Window/Session dataclasses

Site: neo.py:139-159 declares a hand-curated allowlist of format tokens as str | None = None fields on Obj. Sweeping tmux's format_table[] array at format.c:3010-3563 against neo.py's declared fields shows 37 scope-relevant tokens that ship in tmux 3.6a but are not declared. Gap #3 is one instance; the full set:

Pane (13) — registered in format.c between line 3254 and 3341:

Token format.c line
pane_dead 3254
pane_format 3269
pane_in_mode 3278
pane_input_off 3284
pane_key_mode 3287
pane_last 3290
pane_marked 3296
pane_marked_set 3299
pane_mode 3302
pane_path 3305
pane_pipe 3311
pane_synchronized 3326
pane_unseen_changes 3341

Window (13) — registered in format.c between line 3464 and 3557:

Token format.c line
window_active_clients_list 3464
window_active_sessions_list 3470
window_activity_flag 3476
window_bell_flag 3479
window_bigger 3482
window_end_flag 3491
window_flags 3494
window_format 3497
window_last_flag 3509
window_silence_flag 3542
window_start_flag 3548
window_visible_layout 3551
window_zoomed_flag 3557 — already in gap #3

Session (11) — registered in format.c between line 3359 and 3428:

Token format.c line
session_active 3359
session_activity_flag 3365
session_alert 3368
session_bell_flag 3380
session_format 3386
session_group_attached_list 3395
session_group_many_attached 3401
session_grouped 3407
session_many_attached 3416
session_marked 3419
session_silence_flag 3428

Why tmux supports them: every entry above appears verbatim in the format_table[] array — they're registered first-class format tokens with FORMAT_TABLE_STRING callbacks. tmux's format_create_defaults path populates them whenever the relevant scope is bound (pane→format_defaults_pane, window→format_defaults_winlink, session→format_defaults_session).

Where it bites callers: every undeclared token forces typed-Python callers back to pane.cmd("display-message", "-p", "#{…}"). libtmux-mcp's resize_pane(zoom=…) (window_zoomed_flag), copy-mode detection (pane_in_mode, pane_mode), and pane-death checks (pane_dead) all carry these workarounds.

Proposed: declare the full scope-relevant set on Obj (or split into per-scope mixins). Better: generate the field declarations from format.c parsing at build time so future tmux releases don't drift the typed surface — that closes gap #14 (HEAD tokens) for free.


11. No Client class — client_* format tokens have nowhere to land

Site: Server.list_clients at server.py:1392 returns raw output lines. No Client dataclass exists in src/libtmux/. Compare with Session/Window/Pane, which each have an Obj-backed dataclass and a QueryList-returning collection accessor.

Why tmux supports it: twelve client_* format tokens are registered in format.c:

Token format.c line
client_activity 3041
client_control_mode 3050
client_created 3053
client_last_session 3068
client_mode_format 3071
client_prefix 3080
client_readonly 3083
client_session 3086
client_termfeatures 3089
client_termtype 3095
client_theme 3098
client_utf8 3110

The matching tmux subcommand is cmd-list-clients.c, which accepts [-F format] [-t target-session] for arbitrary projection.

Where it bites callers: anything inspecting attached clients (multi-client coordination, "is this client read-only?", per-client status drawing) must call server.cmd("list-clients", "-F", "#{…}") and hand-parse output. The typed-ORM ergonomics that exist for sessions/windows/panes simply aren't available for clients.

Proposed: add src/libtmux/client.py with a Client(Obj) dataclass declaring all twelve client_* fields, plus Server.clients -> QueryList[Client] mirroring Server.sessions. Reuse fetch_objs with list_cmd="list-clients".


12. Server.run_shell drops -c (cwd) and -E (show_stderr) flags

Site: server.py:427-486 exposes background (-b), delay (-d), as_tmux_command (-C), and target_pane (-t). The full flag schema in tmux is "bd:Ct:Es:c:".

Why tmux supports them: cmd-run-shell.c:47 declares the args; cmd-run-shell.c:156-159 reads -c to override the working directory (cdata->cwd = xstrdup(args_get(args, 'c'))), and cmd-run-shell.c:161-162 reads -E to enable JOB_SHOWSTDERR — the latter is the difference between "stdout only" and "stdout + stderr merged into the captured output."

Where it bites callers: any caller that needs the shell command to run in a specific directory (independent of any target pane's cwd) or wants to surface stderr in the result must drop to server.cmd("run-shell", …). libtmux-mcp's diagnostic shell-outs (e.g., probing helper binaries) fall into this bucket.

Proposed: add cwd: StrPath | None = None and show_stderr: bool | None = None kwargs, emitting -c <cwd> and -E respectively.


13. Pane.capture_pane drops -P (pending input) flag

Site: pane.py:354-498 exposes 12 of the 13 flags from tmux's "ab:CeE:JMNpPqS:Tt:" schema. -P is the missing one.

Why tmux supports it: cmd-capture-pane.c:42 declares the args; cmd-capture-pane.c:231-232 branches: when -P is set, cmd_capture_pane_pending(args, wp, &len) returns bytes tmux has buffered as input for the pane but the program hasn't consumed yet — distinct from -p (lowercase, "print to stdout") and from the default history capture.

Where it bites callers: diagnostic tooling that needs to inspect "what input is queued but unprocessed?" (debugging hung programs, copy-mode race conditions, paste-buffer drains) has no typed access; callers must do pane.cmd("capture-pane", "-P", "-p").

Proposed: add pending: bool = False kwarg. When set, emit -P (and keep -p for stdout return). Document that this returns pending input bytes, not history.


14. Forward-looking: 8 new format tokens in tmux master post-3.6a

Site: None of these are declared on libtmux dataclasses (same shape as gap #10), and they don't yet exist in tmux 3.6a — they were added between the 3.6a tag and tmux master. Tracking them now means libtmux is ready when the next tmux tag lands.

Why tmux is adding them: present in tmux master (tmux/tmux/blob/master/format.c), absent from the 3.6a tag. The full diff:

Token Scope Purpose
pane_zoomed_flag pane Pane-level zoom indicator (companion to window_zoomed_flag)
pane_floating_flag pane Pane is a floating/popup pane
pane_flags pane Combined pane flag string (analogous to window_flags)
pane_pb_state pane Paste-buffer transfer state
pane_pb_progress pane Paste-buffer transfer progress
pane_pipe_pid pane PID of the pipe-pane child process
synchronized_output_flag terminal DEC mode 2026 synchronized-output state
bracket_paste_flag terminal Bracketed-paste mode active

Where it bites callers: none yet — these only become reachable when callers upgrade to the next tmux release. But the Obj allowlist will need to be re-walked when that happens. Treating gap #10's fix as "auto-generated from tmux's format.c" makes gap #14 free.

Proposed: roll into the same generator described under gap #10 — when tmux ships these in a tagged release, regenerate; no manual list to maintain.


Suggested groupings for fix PRs

These cluster into five shapes — each cluster is a coherent PR:

Cluster A — display_message parity (gaps #1, #2)
Add Server.display_message and Window.display_message with the same signature as Pane.display_message. Single-commit PR; mirrors the symmetry the rest of the 0.56.0 release set up.

Cluster B — typed format-token coverage (gaps #3, #10, #14)
Expand the declared field set on Pane/Window/Session to cover all scope-relevant tokens from format.c's format_table[]. Prefer a build-time generator that reads format.c so the typed surface tracks tmux automatically; that closes gap #14 (HEAD tokens) for free when the next tmux release tag lands.

Cluster C — flag-only / format-aware wrappers (gaps #5, #6, #7, #12, #13)
Make cmd optional on Pane.send_keys to enable flag-only invocations; add format= / filter= kwargs to Server.list_buffers and Server.panes (et al.) to expose tmux's C-level filtering; add cwd= / show_stderr= to Server.run_shell; add pending= to Pane.capture_pane. These all expose existing tmux capability that the wrapper currently hides.

Cluster D — new Client object hierarchy (gap #11)
Add Client(Obj) dataclass with the twelve client_* fields and Server.clients -> QueryList[Client]. Mirrors the Session/Window/Pane shape and brings list-clients into the typed API.

Cluster E — diagnostics and safety (gaps #4, #8, #9)
Fix Pane.reset per #650; thread subcommand context through LibTmuxException; add duplicate--t detection on the cmd methods. Quality-of-life cluster; each item is small but compounds across the wider ecosystem.


Evidence / Motivation

The MCP-side adoption PR is at tmux-python/libtmux-mcp#48. Gaps #1#9 are documented inline in commit messages or code comments there — pointers in the PR description and the umbrella issue libtmux-mcp#46. Gaps #10#14 surfaced from a follow-up sweep of tmux/tmux@3.6a's cmd_entry flag strings and format_table[] cross-referenced against src/libtmux/neo.py field declarations and src/libtmux/*.py wrapper signatures.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the gap sections alongside the referenced entry points in pane.py, server.py, window.py, session.py, exc.py, and neo.py. Compare each proposed wrapper with the cited tmux command usage, then inspect existing tests before selecting one independently fixable gap. Done means the selected invocation is exposed without breaking existing behavior and its workaround can be removed or avoided.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, cli, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.