Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
ed320ce
Experimental(feat[chainable-commands]): add command-chain IR
tony Jun 14, 2026
0ae1ecd
Experimental(feat[chainable-commands]): add deferred query plan and l…
tony Jun 14, 2026
efd456a
Experimental(feat[chainable-commands]): add async facade and live asy…
tony Jun 14, 2026
2fe351e
Experimental(feat[chainable-commands]): add chainability contract
tony Jun 14, 2026
bae22f2
Experimental(docs[chainable-commands]): polish landing copy and secti…
tony Jun 14, 2026
0e13624
Experimental(test[chainable-commands]): cover minimal-install import
tony Jun 14, 2026
ccf45cb
Experimental(docs[chainable-commands]): Clarify sequence runner boundary
tony Jun 14, 2026
e6f36b9
Pane,Session(fix[targets]): Scope window targets to owning session
tony Jun 16, 2026
0c44390
Experimental(refactor[chain]): drop vestigial CommandPlan ResultT gen…
tony Jun 20, 2026
ac3ca9d
Experimental(fix[chain]): fail closed on empty tmux targets
tony Jun 20, 2026
82567a5
Experimental(feat[chain]): enforce the chainability contract in to_chain
tony Jun 20, 2026
51a5aed
Experimental(test[chain]): use strict asyncio_mode with explicit markers
tony Jun 20, 2026
6eebc0e
Experimental(feat[chain]): log the one-shot tmux dispatch
tony Jun 20, 2026
d0d4a67
Pane,Session(test[targets]): cover session-scoped window targeting
tony Jun 20, 2026
542ecbd
Experimental(docs[chain]): add per-method doctests
tony Jun 20, 2026
8545204
Experimental(feat[chain]): add a typed raw-command escape hatch
tony Jun 20, 2026
587a9af
Session(docs[kill_window]): document the colon-name targeting limit
tony Jun 20, 2026
e9b4941
Experimental(feat[chain]): finish the deferred-result half of the con…
tony Jun 20, 2026
215fafd
test(retry): make timing tests deterministic under load
tony Jun 20, 2026
7d4dbd4
Experimental(docs[chain]): correct the "touches a live server" claim
tony Jun 20, 2026
cea5a91
Experimental(feat[chain]): add dual-purpose forward refs across pane/…
tony Jun 20, 2026
51732fc
Experimental(feat[chain]): resolve independent forward handles over N…
tony Jun 20, 2026
170ab49
Experimental(feat[chain]): span pane, window, and session forward scopes
tony Jun 20, 2026
d6b9e44
Experimental(feat[chain]): fold a lone pane handle into one {marked} …
tony Jun 20, 2026
8a387f3
docs(CHANGES): experimental chainable tmux command system
tony Jun 20, 2026
60a567d
Experimental(fix[chain]): fail loudly when a forward creation dispatc…
tony Jun 20, 2026
e7626b9
Experimental(feat[chain]): forward creates accept start_directory and…
tony Jun 20, 2026
0af614c
Experimental(feat[chain]): seed forward plans from existing sessions …
tony Jun 20, 2026
990d2de
Experimental(feat[chain]): map resolved slots back to live libtmux ob…
tony Jun 20, 2026
c2a8a99
Experimental(feat[chain]): add a server-level PlanRunner for create-f…
tony Jun 20, 2026
f678e97
Experimental(feat[chain]): typed verbs for options, rename, select, e…
tony Jun 20, 2026
7fdcfe7
Experimental(feat[chain]): hand back a handle to a new session's defa…
tony Jun 20, 2026
bc7adc3
Experimental(feat[chain]): preserve_mark opt-out for the single-dispa…
tony Jun 20, 2026
fa95c03
Experimental(docs[chain]): clarify the two forward systems and result…
tony Jun 20, 2026
da68e3a
Chain(fix[chain]): Fail closed unknown commands
tony Jun 20, 2026
67ca67f
Chain(fix[scope]): Validate bound targets
tony Jun 20, 2026
4818945
Chain(feat[control]): Add control runner
tony Jun 21, 2026
0700f83
Chain(fix[control]): Keep percent output
tony Jun 21, 2026
3bfced4
Chain(fix[plan]): Guard forward decorates
tony Jun 21, 2026
726a9a2
Chain(fix[plan]): Keep seed order
tony Jun 21, 2026
96b71a3
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._experimental.chain`, not the top-level `libtmux`. (#685)
### Documentation

#### Cleaner `from_env` examples (#719)
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 @@ -124,6 +124,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