Skip to content

Typed command descriptors for native tmux command chains #683

@tony

Description

@tony

Summary

libtmux has prior tracking for this idea in #80, but that issue was closed as stale in 2018. The need is still current and sharper now: libtmux should have a typed way to describe the tmux command behind each wrapper method so callers can compose native tmux command sequences without dropping to raw cmd(..., ";", ...) calls.

This is also a downstream need for tmux-python/libtmux-mcp. The MCP currently has tool-level batching for ordered operations, but that batching still sits above libtmux/tmux. A libtmux-native command descriptor and chain layer would let downstream tools opt into tmux's command queue semantics through typed Python APIs instead of hand-building argv or running several subprocesses.

Related context:

tmux behavior to model

tmux already has the native behavior this should expose:

  • The manual defines semicolon-separated commands as a command sequence and says later commands do not run after a command in the sequence errors: tmux.1 at 3.6a.
  • tmux's argv parser receives already-tokenized arguments and splits commands on unescaped trailing semicolons: cmd-parse.y at 3.6a.
  • tmux turns the parsed command list into ordered command-queue items: cmd-queue.c at 3.6a.

libtmux already has a central execution path through Server.cmd() and tmux_cmd. Object-scoped methods such as Pane.cmd(), Window.cmd(), and Session.cmd() already add target context. That makes the command boundary the right place to add chaining rather than teaching every caller to concatenate strings.

Design inspiration: Django callable metadata

Django's admin has a useful precedent: display callables keep their normal callable behavior, while decorators attach metadata that later admin code consumes. The modern @admin.display decorator is explicitly equivalent to setting attributes such as boolean, admin_order_field, short_description, and empty_value_display on the function; the versioned docs call this the display decorator for custom display functions in list_display and readonly_fields: https://docs.djangoproject.com/en/6.0/ref/contrib/admin/#django.contrib.admin.display

For libtmux, the analogous contract should be stricter and typed. Instead of only setting ad hoc attributes on methods, a decorator or descriptor could attach a typed CommandSpec to each wrapper method while preserving the existing method API.

Django's Q objects are also relevant for a future query-builder shape: they are composable expression objects with boolean operators and deconstruction support, as shown in django.db.models.Q. libtmux could use a similar pattern for optional query/filter expressions or command-chain predicates without changing ordinary method calls.

Proposed libtmux contract

Add a typed command-descriptor layer around wrapper methods:

@command(
    name="rename-window",
    scope="window",
    chainable=True,
    target="window_id",
    returns="self",
    refresh="after_success",
)
def rename_window(self, new_name: str) -> Window: ...

The descriptor should define, at minimum:

  • tmux command name and argv builder
  • scope and target-injection policy
  • command safety tier and whether it is chainable
  • return policy (self, new object, raw stdout, None, etc.)
  • refresh policy after execution
  • stderr/error handling policy
  • optional tmux version constraints

Normal method calls should keep today’s public behavior. Internally, the same metadata should allow a caller to build a CommandCall instead of executing immediately. A CommandChain can then flatten those calls to one tmux argv using standalone ; separators and run one tmux subprocess.

This should remain argv-based, not shell-string-based. Literal user arguments that end in semicolons need escaping so they are not accidentally treated as command separators.

API avenues to evaluate

  • Explicit builder: server.chain().add(window.rename_window.as_command("logs")).add(pane.clear_history.as_command()).run()
  • Context capture: with server.command_chain() as chain: window.rename_window("logs"); pane.clear_history(); result = chain.run()
  • Q-like expression objects for query/filter composition where the chain needs conditional or selection logic.

The context-manager path is attractive ergonomically, but it should be a recording mode built on descriptors, not AST rewriting in the first implementation. AST translation is powerful but harder to type, test, and explain.

return self should not be the implicit signal for chainability. Many libtmux methods return self for fluent convenience, but a chainable command needs explicit metadata for argv construction, target policy, and refresh behavior. Self-returning methods can be chainable when their descriptor declares how to refresh or rehydrate after execution.

Non-goals

  • Do not require existing users to call .execute() for normal method usage.
  • Do not promise transaction semantics. tmux command sequences stop later commands after an error, but earlier successful commands have already run.
  • Do not concatenate shell strings to simulate tmux command syntax.
  • Do not require every wrapper to be converted in one PR; this can be incremental.

Acceptance criteria

  • A public typed CommandSpec or equivalent metadata contract exists for wrapper methods.
  • Decorated methods preserve normal runtime behavior and type signatures.
  • Callers can build and execute at least one native tmux command chain through libtmux without raw cmd(..., ";", ...) composition.
  • Chain flattening uses argv with explicit separators and safely handles literal semicolon arguments.
  • Tests cover target injection, ordered execution, stop-on-error behavior, stdout/stderr result handling, and refresh/return policies for self-returning methods.
  • Documentation explains when to use normal methods, raw .cmd(), and chainable command descriptors.
  • libtmux-mcp can consume the resulting API for ordered chainable commands instead of duplicating tmux argv-building logic downstream.

Metadata

Metadata

Assignees

No one assigned

    Labels

    MCPMCP server (src/libtmux/mcp/)enhancement

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions