Skip to content

Commit 32d1d26

Browse files
committed
docs(control-mode): Document engine usage, errors, env reqs, sandbox
1 parent 0b8c5b9 commit 32d1d26

File tree

5 files changed

+297
-0
lines changed

5 files changed

+297
-0
lines changed

docs/api/index.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
# API Reference
66

7+
:::{note}
8+
Looking for the new control-mode engine? See {ref}`control-mode` for an
9+
experimental, protocol-focused entry point that still preserves the public
10+
``tmux_cmd`` return type.
11+
:::
12+
713
```{toctree}
814
915
properties

docs/pytest-plugin/index.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,53 @@ def set_home(
137137
monkeypatch.setenv("HOME", str(user_path))
138138
```
139139

140+
## Selecting tmux engines (experimental)
141+
142+
Fixtures can run against different execution engines. By default the
143+
`subprocess` engine is used. You can choose control mode globally:
144+
145+
```console
146+
$ pytest --engine=control
147+
```
148+
149+
Or per-test via the `engines` marker (uses parametrization) and the `engine_name`
150+
fixture:
151+
152+
```python
153+
import pytest
154+
155+
@pytest.mark.engines(["subprocess", "control"])
156+
def test_my_flow(server, engine_name):
157+
# server uses the selected engine, engine_name reflects the current one
158+
assert engine_name in {"subprocess", "control"}
159+
assert server.is_alive()
160+
```
161+
162+
`TestServer` also respects the selected engine. Control mode is experimental and
163+
its APIs may change between releases.
164+
165+
### Control sandbox fixture (experimental)
166+
167+
Use ``control_sandbox`` when you need a hermetic control-mode server for a test:
168+
169+
```python
170+
import typing as t
171+
import pytest
172+
from libtmux.server import Server
173+
174+
@pytest.mark.engines(["control"])
175+
def test_control_sandbox(control_sandbox: t.ContextManager[Server]):
176+
with control_sandbox as server:
177+
session = server.new_session(session_name="sandbox", attach=False)
178+
out = server.cmd("display-message", "-p", "hi")
179+
assert out.stdout == ["hi"]
180+
```
181+
182+
The fixture:
183+
- Spins up a unique socket name and isolates ``HOME`` / ``TMUX_TMPDIR``
184+
- Clears inherited ``TMUX`` so it never attaches to the user's server
185+
- Uses ``ControlModeEngine`` and cleans up the server on exit
186+
140187
## Fixtures
141188

142189
```{eval-rst}

docs/quickstart.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,23 @@ in your server object. `libtmux.Server(socket_name='mysocket')` is
157157
equivalent to `$ tmux -L mysocket`.
158158
:::
159159

160+
### Optional: Control mode (experimental)
161+
162+
Control mode keeps a persistent tmux client open and streams
163+
notifications. Enable it by injecting a control-mode engine:
164+
165+
```python
166+
from libtmux._internal.engines.control_mode import ControlModeEngine
167+
from libtmux.server import Server
168+
169+
engine = ControlModeEngine()
170+
server = Server(engine=engine)
171+
session = server.new_session(session_name="ctrl")
172+
print(session.name)
173+
```
174+
175+
See {ref}`control-mode` for details, caveats, and notification handling.
176+
160177
`server` is now a living object bound to the tmux server's Sessions,
161178
Windows and Panes.
162179

@@ -263,6 +280,41 @@ Session($1 foo)
263280

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

283+
## Connect to or create a session
284+
285+
A simpler approach is to use {meth}`Server.connect()`, which returns an existing
286+
session or creates it if it doesn't exist:
287+
288+
```python
289+
>>> session = server.connect('my_project')
290+
>>> session.name
291+
'my_project'
292+
293+
>>> # Calling again returns the same session
294+
>>> session2 = server.connect('my_project')
295+
>>> session2.session_id == session.session_id
296+
True
297+
```
298+
299+
This is particularly useful for:
300+
301+
- Development workflows where you want to reuse existing sessions
302+
- Scripts that should create a session on first run, then reattach
303+
- Working with both subprocess and control-mode engines transparently
304+
305+
Compare with the traditional approach:
306+
307+
```python
308+
>>> # Traditional: check then create
309+
>>> if server.has_session('my_project'):
310+
... session = server.sessions.get(session_name='my_project')
311+
... else:
312+
... session = server.new_session('my_project')
313+
314+
>>> # Simpler with connect()
315+
>>> session = server.connect('my_project')
316+
```
317+
266318
## Playing with our tmux session
267319

268320
We now have access to `session` from above with all of the methods

docs/topics/control_mode.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
---
2+
orphan: true
3+
---
4+
5+
(control-mode)=
6+
7+
# Control Mode Engine (experimental)
8+
9+
:::{warning}
10+
This is an **experimental API**. Names and behavior may change between releases.
11+
Use with caution and pin your libtmux version if you depend on it.
12+
:::
13+
14+
libtmux can drive tmux through a persistent Control Mode client. This keeps a
15+
single connection open, pipelines commands, and surfaces tmux notifications in a
16+
typed stream.
17+
18+
## Why use Control Mode?
19+
20+
- Lower overhead than spawning a tmux process per command
21+
- Access to live notifications: layout changes, window/link events, client
22+
detach/attach, paste buffer updates, and more
23+
- Structured command results with timing/flag metadata
24+
25+
## Using ControlModeEngine
26+
27+
```python
28+
from __future__ import annotations
29+
30+
from libtmux._internal.engines.control_mode import ControlModeEngine
31+
from libtmux.server import Server
32+
33+
engine = ControlModeEngine(command_timeout=5)
34+
server = Server(engine=engine)
35+
36+
session = server.new_session(session_name="ctrl-demo")
37+
print(session.name)
38+
39+
# Consume notifications (non-blocking example)
40+
for notif in engine.iter_notifications(timeout=0.1):
41+
print(notif.kind, notif.data)
42+
```
43+
44+
:::{note}
45+
Control mode creates an internal session for connection management (default name:
46+
`libtmux_control_mode`). This session is automatically filtered from
47+
`Server.sessions` and `Server.has_session()` to maintain engine transparency.
48+
:::
49+
50+
## Session management with Control Mode
51+
52+
The {meth}`Server.connect()` method works seamlessly with control mode:
53+
54+
```python
55+
from libtmux._internal.engines.control_mode import ControlModeEngine
56+
from libtmux.server import Server
57+
58+
engine = ControlModeEngine()
59+
server = Server(engine=engine)
60+
61+
# Reuses session if it exists, creates if it doesn't
62+
session = server.connect("dev-session")
63+
print(session.name)
64+
65+
# Calling again returns the same session
66+
session2 = server.connect("dev-session")
67+
assert session2.session_id == session.session_id
68+
```
69+
70+
This works transparently with both control mode and subprocess engines, making it
71+
easy to switch between them without changing your code.
72+
73+
## Advanced Configuration
74+
75+
### Custom Internal Session Name
76+
77+
For testing or advanced scenarios, you can customize the internal session name:
78+
79+
```python
80+
from libtmux._internal.engines.control_mode import ControlModeEngine
81+
from libtmux.server import Server
82+
83+
engine = ControlModeEngine(internal_session_name="my_control_session")
84+
server = Server(engine=engine)
85+
86+
# Internal session is still filtered
87+
user_session = server.new_session("my_app")
88+
len(server.sessions) # 1 (only my_app visible)
89+
90+
# But exists internally
91+
len(server._sessions_all()) # 2 (my_app + my_control_session)
92+
```
93+
94+
### Attach to Existing Session
95+
96+
For expert use cases, control mode can attach to an existing session instead of
97+
creating an internal one:
98+
99+
```python
100+
# Create a session first
101+
server.new_session("shared")
102+
103+
# Control mode attaches to it for its connection
104+
engine = ControlModeEngine(attach_to="shared")
105+
server = Server(engine=engine)
106+
107+
# The shared session is visible (not filtered)
108+
len(server.sessions) # 1 (shared session)
109+
```
110+
111+
:::{warning}
112+
Attaching to active user sessions will generate notification traffic from pane
113+
output and layout changes. This increases protocol parsing overhead and may impact
114+
performance. Use only when you need control mode notifications for a specific session.
115+
:::
116+
117+
## Parsing notifications directly
118+
119+
The protocol parser can be used without tmux to understand the wire format.
120+
121+
```python
122+
>>> from libtmux._internal.engines.control_protocol import ControlProtocol
123+
>>> proto = ControlProtocol()
124+
>>> proto.feed_line("%layout-change @1 abcd efgh 0")
125+
>>> notif = proto.get_notification()
126+
>>> notif.kind.name
127+
'WINDOW_LAYOUT_CHANGED'
128+
>>> notif.data['window_layout']
129+
'abcd'
130+
```
131+
132+
## Fallback engine
133+
134+
If control mode is unavailable, ``SubprocessEngine`` matches the same
135+
``Engine`` interface but runs one tmux process per command:
136+
137+
```python
138+
from libtmux._internal.engines.subprocess_engine import SubprocessEngine
139+
from libtmux.server import Server
140+
141+
server = Server(engine=SubprocessEngine())
142+
print(server.list_sessions()) # legacy behavior
143+
```
144+
145+
## Key behaviors
146+
147+
- Commands still return ``tmux_cmd`` objects for compatibility, but extra
148+
metadata (``exit_status``, ``cmd_id``, ``tmux_time``) is attached.
149+
- Notifications are queued; drops are counted when consumers fall behind.
150+
- Timeouts raise ``ControlModeTimeout`` and restart the control client.
151+
152+
## Errors, timeouts, and retries
153+
154+
- ``ControlModeTimeout`` — command block did not finish before
155+
``command_timeout``. The engine closes and restarts the control client.
156+
- ``ControlModeConnectionError`` — control socket died (EOF/broken pipe). The
157+
engine restarts and replays the pending command once.
158+
- ``ControlModeProtocolError`` — malformed ``%begin/%end/%error`` framing; the
159+
client is marked dead and must be restarted.
160+
- ``SubprocessTimeout`` — subprocess fallback exceeded its timeout.
161+
162+
## Notifications and backpressure
163+
164+
- Notifications are enqueued in a bounded queue (default 4096). When the queue
165+
fills, additional notifications are dropped and the drop counter is reported
166+
via :class:`~libtmux._internal.engines.base.EngineStats`.
167+
- Consume notifications via :meth:`ControlModeEngine.iter_notifications` to
168+
avoid drops; use a small timeout (e.g., 0.1s) for non-blocking loops.
169+
170+
## Environment propagation requirements
171+
172+
- tmux **3.2 or newer** is required for ``-e KEY=VAL`` on ``new-session``,
173+
``new-window``, and ``split-window``. Older tmux versions ignore ``-e``; the
174+
library emits a warning and tests skip these cases.
175+
- Environment tests and examples may wait briefly after ``send-keys`` so the
176+
shell prompt/output reaches the pane before capture.
177+
178+
## Capture-pane normalization
179+
180+
- Control mode trims trailing *whitespace-only* lines from ``capture-pane`` to
181+
match subprocess behaviour. If you request explicit ranges (``-S/-E``) or use
182+
``-N/-J``, output is left untouched.
183+
- In control mode, the first capture after ``send-keys`` can race the shell;
184+
libtmux retries briefly to ensure the prompt/output is visible.
185+
186+
## Control sandbox (tests/diagnostics)
187+
188+
The pytest fixture ``control_sandbox`` provides an isolated control-mode tmux
189+
server with a unique socket, HOME/TMUX_TMPDIR isolation, and automatic cleanup.
190+
It is used by the regression suite and can be reused in custom tests when you
191+
need a hermetic control-mode client.

docs/topics/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ Explore libtmux’s core functionalities and underlying principles at a high lev
1010
1111
context_managers
1212
traversal
13+
control_mode
1314
```

0 commit comments

Comments
 (0)