Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
1c13972
ControlMode(core): Add engine stack and protocol bridge
tony Nov 24, 2025
fc19942
Server(refactor[sessions]): Use engine internal filters
tony Nov 24, 2025
244abe8
ControlMode(test): Add control-mode regression coverage and helpers
tony Nov 24, 2025
c4ea5fb
docs(control-mode): Document engine usage, errors, env reqs, sandbox
tony Nov 24, 2025
2b0dc21
docs(control-mode): Note env requirements and capture normalization i…
tony Nov 24, 2025
2781d57
docs(control-mode): Restructure release notes with examples (#605)
tony Nov 24, 2025
9259c24
ControlMode(test): Cover protocol errors, attach_to, sandbox isolation
tony Nov 24, 2025
94786eb
test(control-mode): Adjust attach_to expectation to ignore control-on…
tony Nov 24, 2025
1885fa3
ControlMode(test): Add restart, overflow, attach failure, capture ran…
tony Nov 24, 2025
1118585
ControlMode(core,test): Preflight attach_to and harden restart/captur…
tony Nov 24, 2025
bd271c8
ControlMode(test): Add attach failure, flags, and retry placeholder
tony Nov 24, 2025
c6aa003
test(control-mode): Add attach notifications and capture flag coverage
tony Nov 24, 2025
763cb85
test(control-mode): Expand attach notifications, capture flags, retry…
tony Nov 24, 2025
cb41706
ControlMode(core,test): Preflight attach_to and stabilize control fla…
tony Nov 24, 2025
41d9786
ControlMode(core,test): Attach preflight and stabilize control flag/c…
tony Nov 24, 2025
7af0ca0
test(control-mode): Xfail capture range/joined races
tony Nov 24, 2025
f7e04dc
test(control-mode): Xfail restart retry and capture races
tony Nov 24, 2025
8c0013c
test(control-mode): Xfail notification, collision, switch-client gaps
tony Nov 24, 2025
4cfe182
test(control-mode): Add xfail coverage for backlog, notifications, co…
tony Nov 24, 2025
5e6cfc9
ControlMode(test[xfail]): Add scrollback/timeout placeholders
tony Nov 24, 2025
0a6b2e1
ControlModeEngine(feat[testability]): Allow injectable process factory
tony Nov 24, 2025
9e869d5
ControlModeEngine(types): Introduce process protocol for test hooks
tony Nov 24, 2025
b85e61d
ControlModeEngine(types): Extend process protocol (pid/poll)
tony Nov 24, 2025
c1be188
ControlModeEngine(types): Cast fakes to protocol in tests
tony Nov 24, 2025
8b479e8
ControlModeEngine(test): Make fakes satisfy _ControlProcess without c…
tony Nov 24, 2025
3878198
ControlModeEngine(feat[testability]): Configurable retries and thread…
tony Nov 24, 2025
8d868e0
ControlModeEngine(test): Tighten scripted process typing for mypy
tony Nov 24, 2025
2e7a371
ControlModeEngine(test): Backpressure integration via scripted process
tony Nov 24, 2025
c1dc063
ControlModeEngine(core): UUID session names, control-mode flag, drain…
tony Nov 25, 2025
7b628fe
ControlModeEngine(threading): Non-daemon threads with explicit join
tony Nov 25, 2025
f79085d
ControlModeEngine(abstraction): Engine hooks for probe/switch
tony Nov 25, 2025
e13205c
neo: Route fetch_objs through engine for unified command execution
tony Nov 25, 2025
d152f50
ControlModeEngine(test): ScriptedStdout/Stdin with queue-based I/O
tony Nov 25, 2025
23f40bd
pane(fix): Unify capture_pane trimming with engine behavior
tony Nov 25, 2025
26a9b95
conftest(fix): Skip doctests for control mode engine
tony Nov 25, 2025
59125dd
conftest(fix): Filter doctests at collection for control engine
tony Nov 25, 2025
e9661cc
test(cleanup): Remove obsolete timeout restart xfail
tony Nov 25, 2025
a5deb9e
engine(refactor): Add ServerContext and Engine.bind()
tony Nov 25, 2025
ac53305
engine(api): Standardize hook signatures using stored context
tony Nov 25, 2025
3368745
ControlModeEngine(api): Rename attach_to to control_session
tony Nov 25, 2025
ead055d
ControlModeEngine(feat): Add set_client_flags() for runtime flag control
tony Nov 25, 2025
de85da9
ControlProtocol(fix): Add SKIPPING state for unexpected %begin blocks
tony Dec 1, 2025
a96248c
test(hooks): Mark raw_cmd and dataclass tests as subprocess-only
tony Dec 1, 2025
56da2b8
test(options): Avoid % format expansion in control mode test
tony Dec 1, 2025
df800ea
test(env): Use wait_for_line() for environment propagation tests
tony Dec 1, 2025
5d438b8
ControlProtocol(feat): Add MESSAGE and CONFIG_ERROR notification types
tony Dec 2, 2025
3e327a4
ControlProtocol(fix): Correct notification parsing per tmux source
tony Dec 2, 2025
dc9dea7
ControlProtocol(test[lint-timing]): fix control-mode lint and timing
tony Dec 6, 2025
674fd6f
Hooks(chore[compat]): drop tmux<3.2 guards
tony Dec 6, 2025
dc6886c
Pane(fix[capture]): trim trailing whitespace consistently
tony Dec 6, 2025
89dc215
Server(test[connect]): cover connect() under control engine
tony Dec 6, 2025
f667e7b
ControlModeEngine(test[flags]): verify set_client_flags invocation
tony Dec 6, 2025
116a114
CI(chore[control]): add control-engine pytest run
tony Dec 6, 2025
2fed89c
docs(control): add control-engine testing guidance
tony Dec 6, 2025
398d847
ControlModeEngine(test[typing]): fix lint typing for set_client_flags…
tony Dec 6, 2025
de5f7ce
ControlModeEngine(feat[flags]): expand control client helpers
tony Dec 6, 2025
b68acd4
ControlModeEngine(test[flags]): cover client flags, flow, subscriptions
tony Dec 6, 2025
6e9bab1
Docs(control): document client flags, flow, subscriptions
tony Dec 6, 2025
9ea0c4a
ControlModeEngine(test[live]): exercise flags/flow/subscriptions
tony Dec 6, 2025
1fc50af
Pane(fix[capture_pane]): Retry echo-only captures in control mode
tony Jan 1, 2026
f731b86
Pane(fix[capture_pane]): Simplify retry predicate
tony Jan 1, 2026
f2e654b
Server(fix[shutdown]): Close engine on kill-server
tony Jan 1, 2026
d23f6d8
ControlModeEngine(fix[filter_sessions]): Parse list-clients with deli…
tony Jan 1, 2026
c93bda6
Server(fix[connect]): Restore TMUX env on failure
tony Jan 1, 2026
322da2b
tests(fix[control_client_logs]): Log stdout to file
tony Jan 1, 2026
2cfe0d3
tests(fix[connect]): Stabilize env restore test
tony Jan 1, 2026
a1381ed
tests(fix[stability]): Use monotonic timing and stub cleanup
tony Jan 1, 2026
e5ac071
ControlModeEngine(fix[shutdown]): Close control-mode pipes on teardown
tony Jan 1, 2026
3e91041
ControlModeEngine(fix[can_switch_client]): Handle empty client flags
tony Jan 1, 2026
4b89b99
ControlModeEngine(fix[shutdown]): Guard teardown timeouts
tony Jan 1, 2026
c5462fd
docs(fix[control-mode]): Clarify internal session naming
tony Jan 1, 2026
7f36f5d
pytest(fix[engine-option]): Register engine CLI in plugin
tony Jan 1, 2026
cde054f
pytest(fix[doctest-filter]): Apply control doctest filter in plugin
tony Jan 1, 2026
2c399df
pytest(fix[engine-option]): Default engine when option missing
tony Jan 1, 2026
6131fd4
pytest(fix[doctest-filter]): Report deselected doctests
tony Jan 1, 2026
6399af3
tests(fix[pytest-plugin]): Assert doctest filtering
tony Jan 1, 2026
cbd3e08
Pane(fix[capture_pane]): simplify retry predicate
tony Jan 1, 2026
da38a7a
ControlModeEngine(fix[switch_client]): bootstrap before client check
tony Jan 1, 2026
b2a0a99
ControlModeEngine(fix[probe]): defer to Server on missing tmux
tony Jan 1, 2026
540b60a
ControlModeEngine(fix[shutdown]): avoid hang on shutdown
tony Jan 1, 2026
1c72c3c
test(fix[retry_until]): use monotonic clock
tony Jan 1, 2026
1ad4164
pytest(fix[fixtures]): move control fixtures into plugin
tony Jan 1, 2026
32cc6cc
chore(rebase): Fix conflict-resolution regressions
tony Jul 18, 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
7 changes: 7 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ jobs:
COV_CORE_SOURCE: .
COV_CORE_CONFIG: .coveragerc
COV_CORE_DATAFILE: .coverage.eager
- name: Test with pytest (control engine)
continue-on-error: ${{ matrix.tmux-version == 'master' }}
run: |
sudo apt install libevent-2.1-7
export PATH=$HOME/tmux-builds/tmux-${{ matrix.tmux-version }}/bin:$PATH
tmux -V
uv run py.test --engine=control -n auto --verbose
- uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand Down
55 changes: 55 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,61 @@ The 0.50.0 option/hook deprecations remain soft warnings for now:
| `set_window_option()`, `show_window_option()`, `show_window_options()` | {meth}`~libtmux.Window.set_option`, {meth}`~libtmux.Window.show_option`, {meth}`~libtmux.Window.show_options` | 0.50.0 | Window |
| `g` option parameter | `global_` on {meth}`~libtmux.options.OptionsMixin.set_option`, {meth}`~libtmux.options.OptionsMixin.show_option`, {meth}`~libtmux.options.OptionsMixin.show_options` | 0.50.0 | Options and hooks |

### Features

#### Control mode engine (experimental) (#605)
- Control-mode–first engine stack with structured `CommandResult`, notification parsing
(%layout-change, unlinked window add/close/rename, client session change/detach, paste buffer),
and stats, while keeping the public `cmd` API returning `tmux_cmd`.
- Internal control session is automatically filtered from `Server.sessions` /
`Server.has_session()`; advanced users can inspect all sessions via
`Server._sessions_all()`.
- `ControlModeEngine` accepts `internal_session_name` (default `libtmux_ctrl_XXXXXXXX`)
and `control_session` for advanced connection strategies.

Example:

```python
from libtmux._internal.engines.control_mode import ControlModeEngine
from libtmux.server import Server

engine = ControlModeEngine(command_timeout=5)
server = Server(engine=engine)
session = server.new_session(session_name="ctrl")
for notif in engine.iter_notifications(timeout=0.1):
print(notif.kind, notif.data)
```

#### Convenience APIs (#605)
- `Server.connect()` returns an existing session or creates a new detached one,
working with both subprocess and control-mode engines.

#### Diagnostics and resilience (#605)
- Bounded notification queue with drop counting; exposed via engine stats.
- Expanded exceptions: `ControlModeTimeout`, `ControlModeProtocolError`,
`ControlModeConnectionError`, `SubprocessTimeout`; documented retry/timeout behaviour.
- Control-mode capture-pane trims trailing whitespace-only lines to mirror subprocess
semantics; first capture after send-keys briefly retries to avoid prompt races.

#### Testing utilities (#605)
- Control sandbox fixture provides a hermetic control-mode tmux server (isolated HOME,
TMUX_TMPDIR, unique socket); handy for integration-style tests.
Example:

```python
@pytest.mark.engines(["control"])
def test_control_sandbox(control_sandbox):
with control_sandbox as server:
out = server.cmd("display-message", "-p", "hi")
assert out.stdout == ["hi"]
```

### Compatibility

- Control mode internal session filtering is engine-driven; callers no longer need
to manually exclude `libtmux_ctrl_*`. APIs stay unchanged; additional metadata
is attached for advanced users. (#605)

## libtmux 0.50.1 (2025-12-06)

libtmux 0.50.1 is a documentation repair release for the 0.50 option and hook
Expand Down
7 changes: 7 additions & 0 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ Active deprecations and migration guidance.

::::


:::{note}
Looking for the new control-mode engine? See {ref}`control-mode` for an
experimental, protocol-focused entry point that still preserves the public
``tmux_cmd`` return type.
:::

```{toctree}
:hidden:
:maxdepth: 1
Expand Down
47 changes: 47 additions & 0 deletions docs/api/testing/pytest-plugin/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,53 @@ This is particularly useful when testing interactions between multiple tmux serv
... monkeypatch.setenv("HOME", str(user_path))
```

## Selecting tmux engines (experimental)

Fixtures can run against different execution engines. By default the
`subprocess` engine is used. You can choose control mode globally:

```console
$ pytest --engine=control
```

Or per-test via the `engines` marker (uses parametrization) and the `engine_name`
fixture:

```python
import pytest

@pytest.mark.engines(["subprocess", "control"])
def test_my_flow(server, engine_name):
# server uses the selected engine, engine_name reflects the current one
assert engine_name in {"subprocess", "control"}
assert server.is_alive()
```

`TestServer` also respects the selected engine. Control mode is experimental and
its APIs may change between releases.

### Control sandbox fixture (experimental)

Use ``control_sandbox`` when you need a hermetic control-mode server for a test:

```python
import typing as t
import pytest
from libtmux.server import Server

@pytest.mark.engines(["control"])
def test_control_sandbox(control_sandbox: t.ContextManager[Server]):
with control_sandbox as server:
session = server.new_session(session_name="sandbox", attach=False)
out = server.cmd("display-message", "-p", "hi")
assert out.stdout == ["hi"]
```

The fixture:
- Spins up a unique socket name and isolates ``HOME`` / ``TMUX_TMPDIR``
- Clears inherited ``TMUX`` so it never attaches to the user's server
- Uses ``ControlModeEngine`` and cleans up the server on exit

[pytest]: https://docs.pytest.org/en/stable/
[pytest-tmux]: https://pytest-tmux.readthedocs.io/
[tmuxp]: https://tmuxp.git-pull.com/
65 changes: 65 additions & 0 deletions docs/pytest-plugin/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
myst:
html_meta:
description: "libtmux pytest plugin for testing tmux applications with pytest"
keywords: "tmux, pytest, plugin, testing, libtmux"
---

(pytest_plugin)=

# tmux `pytest` plugin

```{toctree}
:hidden:
:maxdepth: 2

getting-started
fixtures
usage-examples
advanced-techniques
```

libtmux provides pytest fixtures for tmux, making it easy to test tmux-related functionality with complete isolation. The plugin automatically manages setup and teardown of independent tmux servers, sessions, windows, and panes.

```{admonition} Connect with us!
:class: seealso

Do you want more flexibility? Correctness? Power? Defaults changed? [Connect with us](https://github.com/tmux-python/libtmux/discussions) on the tracker. We want to know your use case and won't stabilize APIs until we're sure everything is by the book.
```

## Benefits at a glance

- **Isolated Testing Environment**: Each test gets a fresh tmux server that won't interfere with other tests
- **Automatic Cleanup**: Servers, sessions, and other resources are automatically cleaned up after tests
- **Simplified Setup**: Common fixtures for server, session, window, and pane management
- **Reliable Testing**: Consistent environment for reproducible test results
- **Custom Configuration**: Easily test with different tmux configurations and settings

## Quick installation

Install `libtmux` via the Python package manager of your choice:

```{code-block} console
$ pip install libtmux
```

The pytest plugin will be automatically detected by pytest, and the fixtures will be available in your test environment.

## See real-world examples

View libtmux's own [tests/](https://github.com/tmux-python/libtmux/tree/master/tests) as well as tmuxp's [tests/](https://github.com/tmux-python/tmuxp/tree/master/tests) for real-world examples.

For detailed code examples and usage patterns, refer to the {ref}`usage-examples` page.

## Module reference

```{module} libtmux.pytest_plugin
```

```{eval-rst}
.. automodule:: libtmux.pytest_plugin
:members:
:inherited-members:
:private-members:
:show-inheritance:
:member-order: bysource
55 changes: 55 additions & 0 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,26 @@ equivalent to `$ tmux -L mysocket`.
{class}`~libtmux.Session`, {class}`~libtmux.Window`, and
{class}`~libtmux.Pane` objects.

### Optional: Control mode (experimental)

Control mode keeps a persistent tmux client open and streams
notifications. Enable it by injecting a control-mode engine:

```python
from libtmux._internal.engines.control_mode import ControlModeEngine
from libtmux.server import Server

engine = ControlModeEngine()
server = Server(engine=engine)
session = server.new_session(session_name="ctrl")
print(session.name)
```

See {ref}`control-mode` for details, caveats, and notification handling.

`server` is now a living object bound to the tmux server's Sessions,
Windows and Panes.

## Raw, contextual commands

New session:
Expand Down Expand Up @@ -283,6 +303,41 @@ Session($1 foo)

to give us a `session` object to play with.

## Connect to or create a session

A simpler approach is to use {meth}`Server.connect()`, which returns an existing
session or creates it if it doesn't exist:

```python
>>> session = server.connect('my_project')
>>> session.name
'my_project'

>>> # Calling again returns the same session
>>> session2 = server.connect('my_project')
>>> session2.session_id == session.session_id
True
```

This is particularly useful for:

- Development workflows where you want to reuse existing sessions
- Scripts that should create a session on first run, then reattach
- Working with both subprocess and control-mode engines transparently

Compare with the traditional approach:

```python
>>> # Traditional: check then create
>>> if server.has_session('my_project'):
... session = server.sessions.get(session_name='my_project')
... else:
... session = server.new_session('my_project')

>>> # Simpler with connect()
>>> session = server.connect('my_project')
```

## Playing with our tmux session

We now have access to `session` from above with all of the methods
Expand Down
Loading
Loading