Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3c7329f
Experimental(feat[chainable-commands]): add command-chain IR
tony Jun 14, 2026
6cd7b18
Experimental(feat[chainable-commands]): add deferred query plan and l…
tony Jun 14, 2026
6ea6981
Experimental(feat[chainable-commands]): add async facade and live asy…
tony Jun 14, 2026
9a0cfb5
Experimental(feat[chainable-commands]): add chainability contract
tony Jun 14, 2026
c17f473
Experimental(docs[chainable-commands]): polish landing copy and secti…
tony Jun 14, 2026
927adfd
Experimental(test[chainable-commands]): cover minimal-install import
tony Jun 14, 2026
6ed269f
Experimental(docs[chainable-commands]): Clarify sequence runner boundary
tony Jun 14, 2026
9be3a10
Pane,Session(fix[targets]): Scope window targets to owning session
tony Jun 16, 2026
e6bd61a
Experimental(refactor[chain]): drop vestigial CommandPlan ResultT gen…
tony Jun 20, 2026
05d3dff
Experimental(fix[chain]): fail closed on empty tmux targets
tony Jun 20, 2026
93d748f
Experimental(feat[chain]): enforce the chainability contract in to_chain
tony Jun 20, 2026
1b95a60
Experimental(test[chain]): use strict asyncio_mode with explicit markers
tony Jun 20, 2026
256af65
Experimental(feat[chain]): log the one-shot tmux dispatch
tony Jun 20, 2026
0326b51
Pane,Session(test[targets]): cover session-scoped window targeting
tony Jun 20, 2026
2c5ca46
Experimental(docs[chain]): add per-method doctests
tony Jun 20, 2026
77e7b8b
Experimental(feat[chain]): add a typed raw-command escape hatch
tony Jun 20, 2026
f5d40ac
Session(docs[kill_window]): document the colon-name targeting limit
tony Jun 20, 2026
07b633b
Experimental(feat[chain]): finish the deferred-result half of the con…
tony Jun 20, 2026
ea3ef65
test(retry): make timing tests deterministic under load
tony Jun 20, 2026
a5273a3
py(deps[dev]) Bump dev packages
tony Jun 20, 2026
68a94de
Experimental(docs[chain]): correct the "touches a live server" claim
tony Jun 20, 2026
2df0b35
Experimental(feat[chain]): add dual-purpose forward refs across pane/…
tony Jun 20, 2026
2521cc4
Experimental(feat[chain]): resolve independent forward handles over N…
tony Jun 20, 2026
41d455a
Experimental(feat[chain]): span pane, window, and session forward scopes
tony Jun 20, 2026
c9db0b1
Experimental(feat[chain]): fold a lone pane handle into one {marked} …
tony Jun 20, 2026
10e7e67
docs(CHANGES): experimental chainable tmux command system
tony Jun 20, 2026
ccbe4be
Experimental(fix[chain]): fail loudly when a forward creation dispatc…
tony Jun 20, 2026
ae89cdc
Experimental(feat[chain]): forward creates accept start_directory and…
tony Jun 20, 2026
94a7574
Experimental(feat[chain]): seed forward plans from existing sessions …
tony Jun 20, 2026
c1dd8e1
Experimental(feat[chain]): map resolved slots back to live libtmux ob…
tony Jun 20, 2026
08cce7d
Experimental(feat[chain]): add a server-level PlanRunner for create-f…
tony Jun 20, 2026
9bc35bb
Experimental(feat[chain]): typed verbs for options, rename, select, e…
tony Jun 20, 2026
892ec32
Experimental(feat[chain]): hand back a handle to a new session's defa…
tony Jun 20, 2026
8361914
Experimental(feat[chain]): preserve_mark opt-out for the single-dispa…
tony Jun 20, 2026
f4292cb
Experimental(docs[chain]): clarify the two forward systems and result…
tony Jun 20, 2026
f4405b4
Chain(fix[chain]): Fail closed unknown commands
tony Jun 20, 2026
c4cf13a
Chain(fix[scope]): Validate bound targets
tony Jun 20, 2026
93aa1b7
Chain(feat[control]): Add control runner
tony Jun 21, 2026
8c60a03
Chain(fix[control]): Keep percent output
tony Jun 21, 2026
86cc016
Chain(fix[plan]): Guard forward decorates
tony Jun 21, 2026
175d29c
Chain(fix[plan]): Keep seed order
tony Jun 21, 2026
95cf86b
Chain(fix[plan]): Clear mark on failure
tony Jun 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ $ uvx --from 'libtmux' --prerelease allow python
_Notes on the upcoming release will go here._
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### What's new

#### New: chainable tmux commands (`libtmux._experimental.chain`)

Build a sequence of tmux commands and run it in a single tmux call,
instead of one subprocess per command. References to panes, windows,
and sessions can be lazy — point at an object a command will create
and keep building against it — and they all resolve together when the
chain runs. Experimental and opt-in: import from
`libtmux._experimental.chain`, not the top-level `libtmux`.
When callers need per-command output, the experimental control-mode
runner batches command lines through one persistent `tmux -C` client
and returns one result per command. (#685)

## libtmux 0.58.1 (2026-06-16)

libtmux 0.58.1 restores compatibility with pytest 9.1. The bundled
Expand Down
13 changes: 13 additions & 0 deletions docs/experiment/api/libtmux._experimental.chain._async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Async - `libtmux._experimental.chain._async`

:::{warning}
Experimental. This API is **not** covered by version policies and can break or
be removed between minor versions.
:::

```{eval-rst}
.. automodule:: libtmux._experimental.chain._async
:members:
:undoc-members:
:show-inheritance:
```
13 changes: 13 additions & 0 deletions docs/experiment/api/libtmux._experimental.chain._connection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Connecting to live tmux sessions - `libtmux._experimental.chain._connection`

:::{warning}
Experimental. This API is **not** covered by version policies and can break or
be removed between minor versions.
:::

```{eval-rst}
.. automodule:: libtmux._experimental.chain._connection
:members:
:undoc-members:
:show-inheritance:
```
13 changes: 13 additions & 0 deletions docs/experiment/api/libtmux._experimental.chain.chain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Chain - `libtmux._experimental.chain.chain`

:::{warning}
Experimental. This API is **not** covered by version policies and can break or
be removed between minor versions.
:::

```{eval-rst}
.. automodule:: libtmux._experimental.chain.chain
:members:
:undoc-members:
:show-inheritance:
```
13 changes: 13 additions & 0 deletions docs/experiment/api/libtmux._experimental.chain.control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Control mode - `libtmux._experimental.chain.control`

:::{warning}
Experimental. This API is **not** covered by version policies and can break or
be removed between minor versions.
:::

```{eval-rst}
.. automodule:: libtmux._experimental.chain.control
:members:
:undoc-members:
:show-inheritance:
```
13 changes: 13 additions & 0 deletions docs/experiment/api/libtmux._experimental.chain.ir.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Intermediate representation - `libtmux._experimental.chain.ir`

:::{warning}
Experimental. This API is **not** covered by version policies and can break or
be removed between minor versions.
:::

```{eval-rst}
.. automodule:: libtmux._experimental.chain.ir
:members:
:undoc-members:
:show-inheritance:
```
13 changes: 13 additions & 0 deletions docs/experiment/api/libtmux._experimental.chain.plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Expressions - `libtmux._experimental.chain.plan`

:::{warning}
Experimental. This API is **not** covered by version policies and can break or
be removed between minor versions.
:::

```{eval-rst}
.. automodule:: libtmux._experimental.chain.plan
:members:
:undoc-members:
:show-inheritance:
```
195 changes: 195 additions & 0 deletions docs/experiment/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
(experimental)=

# Experimental

:::{danger}
**No stability guarantee.** Everything under `libtmux._experimental` is **not**
covered by the project's versioning policy. It can change or be removed between
any releases without notice.

These APIs are published so the design can be exercised and reviewed before any
stability commitment. If you depend on something here and want it stabilized,
please [file an issue](https://github.com/tmux-python/libtmux/issues).
:::

## Chainable commands

`libtmux._experimental.chain` lets you build an ordered sequence of
typed tmux commands that runs as **one** native `tmux ... \; ...` invocation,
instead of one subprocess per command. The pieces layer up, so you can reach for
as much or as little as you need:

- **Intermediate representation** -- the typed argv layer beneath everything: a
{class}`~libtmux._experimental.chain.ir.CommandCall` is a single
command, and a
{class}`~libtmux._experimental.chain.ir.CommandChain` is an
ordered group that renders to one argv (with standalone `;` separators) and
dispatches once.
- **Expressions** -- compose commands from a lazy, target-safe pane query. A
{class}`~libtmux._experimental.chain.plan.PaneQuery` resolves
against a pure
{class}`~libtmux._experimental.chain.plan.TmuxSnapshot`, maps each
typed row to commands, and compiles to one sequence -- so you can build and
assert the result without touching tmux.
- **Async** -- {mod}`~libtmux._experimental.chain._async` mirrors the
same query and dispatch API with `await`, while command construction stays
synchronous and one expression still compiles to one invocation.
- **Connecting to live tmux sessions** -- the bridge to a real server:
{func}`~libtmux._experimental.chain._connection.snapshot_from_session`
reads live panes, and
{class}`~libtmux._experimental.chain._connection.SessionPlanExecutor`
(with its async counterpart
{class}`~libtmux._experimental.chain._connection.AsyncSessionPlanExecutor`)
resolves and runs an expression against a live {class}`~libtmux.Session` in one
invocation.
- **Control mode** --
{class}`~libtmux._experimental.chain.control.ControlModeRunner`
batches command lines through one persistent `tmux -C` client and returns one
result per command when callers need per-command output.
- **Chainability** --
{mod}`~libtmux._experimental.chain.chain` decides which commands
may share one invocation: the static
{attr}`~libtmux._experimental.chain.ir.CommandSpec.chainable`
flag, plus a deferred result that won't hand back output until the chain has
run.

::::{grid} 1 2 2 2
:gutter: 2 2 3 3

:::{grid-item-card} Intermediate representation
:link: api/libtmux._experimental.chain.ir
:link-type: doc
The typed argv layer: `CommandCall`, `CommandChain`, `CommandSpec`.
:::

:::{grid-item-card} Expressions
:link: api/libtmux._experimental.chain.plan
:link-type: doc
Build commands from a lazy, target-safe pane query.
:::

:::{grid-item-card} Async
:link: api/libtmux._experimental.chain._async
:link-type: doc
The same query and dispatch API, with `await`.
:::

:::{grid-item-card} Connecting to live tmux sessions
:link: api/libtmux._experimental.chain._connection
:link-type: doc
Read live panes and run an expression against a real session.
:::

:::{grid-item-card} Chainability
:link: api/libtmux._experimental.chain.chain
:link-type: doc
Which commands may share one invocation.
:::

:::{grid-item-card} Control mode
:link: api/libtmux._experimental.chain.control
:link-type: doc
Batch command lines with per-command results.
:::

::::

## At a glance

Compose typed calls and dispatch them as one tmux invocation:

```python
>>> from libtmux._experimental.chain.ir import CommandCall
>>> sequence = (
... CommandCall("set-option", ("-g", "@cc_docs_a", "1"))
... >> CommandCall("set-option", ("-g", "@cc_docs_b", "2"))
... )
>>> sequence.argv()
('set-option', '-g', '@cc_docs_a', '1', ';', 'set-option', '-g', '@cc_docs_b', '2')
>>> sequence.run(session.server).returncode
0
>>> session.server.cmd("show-option", "-gv", "@cc_docs_b").stdout
['2']
```

Build an expression from a query and compile it to one sequence -- pure, no tmux
required:

```python
>>> from libtmux._experimental.chain.plan import (
... PaneRef,
... PaneTarget,
... SessionTarget,
... TmuxSnapshot,
... WindowTarget,
... panes,
... )
>>> snapshot = TmuxSnapshot(
... panes=(
... PaneRef.concrete(pane_id="%1", window_id="@1", session_id="$0",
... pane_index=0, active=True, title="editor"),
... PaneRef.concrete(pane_id="%2", window_id="@1", session_id="$0",
... pane_index=1, active=True, title="logs"),
... ),
... )
>>> plan = (
... panes()
... .filter(active=True)
... .order_by("pane_index")
... .commands(lambda pane: pane.cmd.resize_pane(height=20))
... )
>>> plan.to_chain(snapshot).argvs()
(('resize-pane', '-t', '%1', '-y', '20'), ('resize-pane', '-t', '%2', '-y', '20'))
```

Against a live server, run the same expression in one invocation with
{class}`~libtmux._experimental.chain._connection.SessionPlanExecutor`:

```python
>>> from libtmux._experimental.chain import SessionPlanExecutor, panes
>>> runner = SessionPlanExecutor(session)
>>> live_plan = panes().filter(active=True).commands(
... lambda pane: pane.cmd.send_keys("echo libtmux", enter=True),
... )
>>> live_plan.run(runner)
```

The same expression can be built and compiled asynchronously -- construction
stays synchronous; only resolution and dispatch await:

```python
>>> import asyncio
>>> from libtmux._experimental.chain import aio
>>> from libtmux._experimental.chain.plan import (
... PaneRef,
... PaneTarget,
... SessionTarget,
... TmuxSnapshot,
... WindowTarget,
... )
>>> snapshot = TmuxSnapshot(
... panes=(
... PaneRef.concrete(pane_id="%1", window_id="@1", session_id="$0",
... pane_index=0, active=True, title="editor"),
... ),
... )
>>> async def _resize() -> tuple[tuple[str, ...], ...]:
... plan = aio.panes().filter(active=True).commands(
... lambda pane: pane.cmd.resize_pane(height=20),
... )
... return (await plan.to_chain(snapshot)).argvs()
>>> asyncio.run(_resize())
(('resize-pane', '-t', '%1', '-y', '20'),)
```

```{toctree}
:hidden:
:maxdepth: 1

api/libtmux._experimental.chain.ir
api/libtmux._experimental.chain.plan
api/libtmux._experimental.chain._async
api/libtmux._experimental.chain._connection
api/libtmux._experimental.chain.chain
api/libtmux._experimental.chain.control
```
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ topics/index
api/index
api/testing/index
internals/index
experiment/index
project/index
history
migration
Expand Down
8 changes: 6 additions & 2 deletions docs/project/public-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@ This includes:

## What Is Internal

Modules under `libtmux._internal` and `libtmux._vendor` are **not public**.
They may change or be removed without notice between any release.
Modules under `libtmux._internal`, `libtmux._vendor`, and
`libtmux._experimental` are **not public**. They may change or be removed
without notice between any release. `libtmux._experimental` additionally hosts
in-progress designs that are published for feedback before any stability
commitment (see {ref}`the experimental docs <experimental>`).

Do not import from:
- `libtmux._internal.*`
- `libtmux._vendor.*`
- `libtmux._experimental.*`

## Pre-1.0 Stability Policy

Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ dev = [
"pytest-mock",
"pytest-watcher",
"pytest-xdist",
"pytest-asyncio",
# Coverage
"codecov",
"coverage",
Expand All @@ -87,6 +88,7 @@ testing = [
"pytest-rerunfailures",
"pytest-mock",
"pytest-watcher",
"pytest-asyncio",
]
coverage =[
"codecov",
Expand Down Expand Up @@ -242,6 +244,8 @@ doctest_optionflags = [
"ELLIPSIS",
"NORMALIZE_WHITESPACE"
]
asyncio_mode = "strict"
asyncio_default_fixture_loop_scope = "function"
testpaths = [
"src/libtmux",
"tests",
Expand Down
13 changes: 13 additions & 0 deletions src/libtmux/_experimental/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Experimental libtmux APIs.

Note
----
This is an **experimental** namespace. Everything under
:mod:`libtmux._experimental` is **not** covered by the project's versioning
policy and may change or be removed between any releases without notice.

If you depend on something here and want it stabilized, please
`file an issue <https://github.com/tmux-python/libtmux/issues>`_.
"""

from __future__ import annotations
Loading
Loading