Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
82 changes: 82 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,90 @@ $ uvx --from 'libtmux' --prerelease allow python
_Notes on the upcoming release will go here._
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### Breaking changes

#### `raise_if_dead()` no longer echoes tmux's error (#739)

{meth}`Server.raise_if_dead() <libtmux.Server.raise_if_dead>` previously let
tmux write its message straight to the terminal. It now captures that text onto
the raised {exc}`subprocess.CalledProcessError`. The exception type is
unchanged.

#### `tmux_cmd.process` is a property (#739)

{attr}`~libtmux.common.tmux_cmd.process` was a plain attribute holding the
{class}`subprocess.Popen` that ran the command; it is now a read-only property.
Reading it is unchanged under the default engine. Assigning to it no longer
works, and reading it after a command ran through an engine that forks no
process raises {exc}`~libtmux.exc.LibTmuxException` rather than returning
`None`.

### What's new

#### Pluggable command engines (#739)

Every tmux command libtmux runs now goes through an *engine* — an object that
takes a rendered argv and returns a structured result. The default,
{class}`~libtmux.engines.subprocess.SubprocessEngine`, forks the tmux binary
exactly as before, so existing code is unaffected.

Pass `engine=` to {class}`~libtmux.Server` and every command on that server runs
through your object instead. {class}`~libtmux.engines.base.TmuxEngine` is a
{class}`typing.Protocol`, so any object with `run()` and `run_batch()` qualifies
— there is no base class to inherit. That makes it possible to drive libtmux
against a recorded or in-memory tmux with no server running, and it is the seam
the control-mode, asyncio, and native-protocol engines plug into.

This ships the seam only. {meth}`Server.cmd() <libtmux.Server.cmd>` still
returns a {class}`~libtmux.common.tmux_cmd`, arguments still reach tmux
unchanged, and nothing about the default path is new — an engine is the one
thing you can now replace.

An engine that names no tmux server of its own adopts the server's connection,
so injecting one into a socket-scoped {class}`~libtmux.Server` cannot silently
dispatch to the ambient tmux server. Engines that name a server keep it. A
custom `tmux_bin` selects a program rather than a server, so an engine carrying
only one adopts the server's flags and keeps its own binary.

{class}`~libtmux.engines.connection.ServerConnection` is now the single place
the tmux binary and the `-L`/`-S`/`-f`/`-2`/`-8` flags are computed; three
separate copies previously disagreed about which flags to emit. It is derived
from the server's public attributes on each use, so reassigning `socket_name`
takes effect on the next command, and it memoizes its {func}`shutil.which`
lookup instead of re-walking `$PATH` for every command. It can also report the
tmux version it targets via
{meth}`~libtmux.engines.connection.ServerConnection.tmux_version`, memoizing one
`tmux -V` probe; an engine that forwards it satisfies the optional
{class}`~libtmux.engines.base.SupportsTmuxVersion` capability, which callers
rendering version-gated argv read to decide whether a flag is safe to send.

An engine that folds several commands into one dispatch needs to know which `;`
in an argv is a boundary and which is data.
{class}`~libtmux.engines.base.CommandSeparator` marks the boundary and
{func}`~libtmux.engines.base.is_command_separator` finds it, so a `;` a caller
passes as an ordinary argument can never become one by accident.

See {ref}`engines` for the guide and {ref}`engines-api` for the reference.

### Fixes

#### Listing queries honor `config_file` and `colors` (#739)

{meth}`Server.raise_if_dead() <libtmux.Server.raise_if_dead>` and the listing
queries behind {attr}`~libtmux.Server.sessions` built their own connection flags
and emitted only `-L`/`-S`, so a server constructed with `config_file=` or
`colors=` passed those flags on some commands and not others. All paths now
share one connection. A `colors=` value other than `256` or `88` raises
{exc}`~libtmux.exc.UnknownColorOption` on those paths as well.

### Documentation

#### Engines guide and API reference (#739)

{ref}`engines` covers what an engine is, writing one, the optional capability
protocols, and explicit command separators. {ref}`engines-api` documents the
module.

#### Cleaner `from_env` examples (#719)

The rendered examples for {meth}`Pane.from_env() <libtmux.Pane.from_env>` and
Expand Down
7 changes: 7 additions & 0 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ Base classes and command execution.
Dataclass-based query interface.
:::

:::{grid-item-card} Engine
:link: libtmux.engines
:link-type: doc
How tmux commands are executed, and how to swap that out.
:::

:::{grid-item-card} Options
:link: libtmux.options
:link-type: doc
Expand Down Expand Up @@ -173,6 +179,7 @@ Window <libtmux.window>
Pane <libtmux.pane>
Client <libtmux.client>
Common <libtmux.common>
Engine <libtmux.engines>
Neo <libtmux.neo>
Options <libtmux.options>
Hooks <libtmux.hooks>
Expand Down
57 changes: 57 additions & 0 deletions docs/api/libtmux.engines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
(engines-api)=

# Engines

An *engine* is the object that actually runs a tmux command. Every dispatch in
libtmux — {meth}`Server.cmd() <libtmux.Server.cmd>`, the listing queries behind
{attr}`~libtmux.Server.sessions`, and {meth}`Server.raise_if_dead()
<libtmux.Server.raise_if_dead>` — goes through one, and by default that is
{class}`~libtmux.engines.subprocess.SubprocessEngine`, which forks the tmux
binary exactly as libtmux always has.

The engine is swappable. Pass `engine=` to {class}`~libtmux.Server` and every
command on that server runs through your object instead, which is how you drive
libtmux against a recorded or in-memory tmux without a running server.

See {ref}`engines` for the guide, with worked examples.

Every symbol below is re-exported from `libtmux.engines`, so
`from libtmux.engines import SubprocessEngine` works regardless of which
submodule defines it.

## Requests and results

A {class}`~libtmux.engines.base.CommandRequest` is a rendered tmux argv; a
{class}`~libtmux.engines.base.CommandResult` is the structured outcome. A
tmux-side failure is *data* here — it sets `returncode` and `stderr` rather than
raising. Only an engine-broken condition (missing binary, lost connection)
raises.

{class}`~libtmux.engines.base.TmuxEngine` is a {class}`typing.Protocol`, so any
object with `run()` and `run_batch()` is an engine; there is no base class to
inherit. The `Supports*` protocols are optional capabilities an engine may
also implement.

```{eval-rst}
.. automodule:: libtmux.engines.base
:members:
```

## Connections

A {class}`~libtmux.engines.connection.ServerConnection` is the pair every engine
needs before it can dispatch anything: which tmux *binary* to run, and the
connection flags (`-L`/`-S`/`-f`/`-2`/`-8`) naming one tmux server. It is the
single place either is computed.

```{eval-rst}
.. automodule:: libtmux.engines.connection
:members:
```

## The default engine

```{eval-rst}
.. automodule:: libtmux.engines.subprocess
:members:
```
Loading
Loading