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
79bc31c
Experimental(feat[chainable-commands]): add command-chain IR
tony Jun 14, 2026
2c1ec62
Experimental(feat[chainable-commands]): add deferred query plan and l…
tony Jun 14, 2026
03be9fc
Experimental(feat[chainable-commands]): add async facade and live asy…
tony Jun 14, 2026
f1c73f0
Experimental(feat[chainable-commands]): add chainability contract
tony Jun 14, 2026
2edf540
Experimental(docs[chainable-commands]): polish landing copy and secti…
tony Jun 14, 2026
3ec018f
Experimental(test[chainable-commands]): cover minimal-install import
tony Jun 14, 2026
70b8a30
Experimental(docs[chainable-commands]): Clarify sequence runner boundary
tony Jun 14, 2026
6ef2615
Pane,Session(fix[targets]): Scope window targets to owning session
tony Jun 16, 2026
d5d571b
Experimental(refactor[chain]): drop vestigial CommandPlan ResultT gen…
tony Jun 20, 2026
47e754f
Experimental(fix[chain]): fail closed on empty tmux targets
tony Jun 20, 2026
41959e5
Experimental(feat[chain]): enforce the chainability contract in to_chain
tony Jun 20, 2026
6c69260
Experimental(test[chain]): use strict asyncio_mode with explicit markers
tony Jun 20, 2026
3178c24
Experimental(feat[chain]): log the one-shot tmux dispatch
tony Jun 20, 2026
a1f037b
Pane,Session(test[targets]): cover session-scoped window targeting
tony Jun 20, 2026
0812f6a
Experimental(docs[chain]): add per-method doctests
tony Jun 20, 2026
1772c07
Experimental(feat[chain]): add a typed raw-command escape hatch
tony Jun 20, 2026
a546c80
Session(docs[kill_window]): document the colon-name targeting limit
tony Jun 20, 2026
b582e3d
Experimental(feat[chain]): finish the deferred-result half of the con…
tony Jun 20, 2026
ac093c7
test(retry): make timing tests deterministic under load
tony Jun 20, 2026
c045af9
py(deps[dev]) Bump dev packages
tony Jun 20, 2026
e8f8ef4
Experimental(docs[chain]): correct the "touches a live server" claim
tony Jun 20, 2026
58b9189
Experimental(feat[chain]): add dual-purpose forward refs across pane/…
tony Jun 20, 2026
cc033fa
Experimental(feat[chain]): resolve independent forward handles over N…
tony Jun 20, 2026
6aa7c05
Experimental(feat[chain]): span pane, window, and session forward scopes
tony Jun 20, 2026
b3a2eeb
Experimental(feat[chain]): fold a lone pane handle into one {marked} …
tony Jun 20, 2026
b21935c
docs(CHANGES): experimental chainable tmux command system
tony Jun 20, 2026
45eef7a
Experimental(fix[chain]): fail loudly when a forward creation dispatc…
tony Jun 20, 2026
c7a593f
Experimental(feat[chain]): forward creates accept start_directory and…
tony Jun 20, 2026
b2e2cd1
Experimental(feat[chain]): seed forward plans from existing sessions …
tony Jun 20, 2026
3e4a335
Experimental(feat[chain]): map resolved slots back to live libtmux ob…
tony Jun 20, 2026
15aa343
Experimental(feat[chain]): add a server-level PlanRunner for create-f…
tony Jun 20, 2026
0f9a328
Experimental(feat[chain]): typed verbs for options, rename, select, e…
tony Jun 20, 2026
a4319f6
Experimental(feat[chain]): hand back a handle to a new session's defa…
tony Jun 20, 2026
5404859
Experimental(feat[chain]): preserve_mark opt-out for the single-dispa…
tony Jun 20, 2026
b8ecfed
Experimental(docs[chain]): clarify the two forward systems and result…
tony Jun 20, 2026
c07f728
Chain(fix[chain]): Fail closed unknown commands
tony Jun 20, 2026
c375cd2
Chain(fix[scope]): Validate bound targets
tony Jun 20, 2026
e1c883f
Chain(feat[control]): Add control runner
tony Jun 21, 2026
302e346
Chain(fix[control]): Keep percent output
tony Jun 21, 2026
7275ce0
Chain(fix[plan]): Guard forward decorates
tony Jun 21, 2026
2b77da0
Chain(fix[plan]): Keep seed order
tony Jun 21, 2026
b34c006
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