Skip to content

Commit 22bfe4e

Browse files
committed
docs(topics): Add pane interaction guide with doctests
why: Core pane automation patterns needed for agentic workflow users what: - Add send_keys, capture_pane, display_message examples - Add output waiting and polling patterns - Add resize and clear operations - Add practical recipes for command execution - All examples are runnable doctests
1 parent 31787f8 commit 22bfe4e

File tree

1 file changed

+354
-0
lines changed

1 file changed

+354
-0
lines changed

docs/topics/pane_interaction.md

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
(pane-interaction)=
2+
3+
# Pane Interaction
4+
5+
libtmux provides powerful methods for interacting with tmux panes programmatically.
6+
This is especially useful for automation, testing, and orchestrating terminal-based
7+
workflows.
8+
9+
Open two terminals:
10+
11+
Terminal one: start tmux in a separate terminal:
12+
13+
```console
14+
$ tmux
15+
```
16+
17+
Terminal two, `python` or `ptpython` if you have it:
18+
19+
```console
20+
$ python
21+
```
22+
23+
## Sending Commands
24+
25+
The {meth}`~libtmux.Pane.send_keys` method sends text to a pane, optionally pressing
26+
Enter to execute it.
27+
28+
### Basic command execution
29+
30+
```python
31+
>>> pane = window.split(shell='sh')
32+
33+
>>> pane.send_keys('echo "Hello from libtmux"')
34+
35+
>>> import time; time.sleep(0.1) # Allow command to execute
36+
37+
>>> output = pane.capture_pane()
38+
>>> 'Hello from libtmux' in '\\n'.join(output)
39+
True
40+
```
41+
42+
### Send without pressing Enter
43+
44+
Use `enter=False` to type text without executing:
45+
46+
```python
47+
>>> pane.send_keys('echo "waiting"', enter=False)
48+
49+
>>> # Text is typed but not executed
50+
>>> output = pane.capture_pane()
51+
>>> 'waiting' in '\\n'.join(output)
52+
True
53+
```
54+
55+
Press Enter separately with {meth}`~libtmux.Pane.enter`:
56+
57+
```python
58+
>>> import time
59+
60+
>>> # First type something without pressing Enter
61+
>>> pane.send_keys('echo "execute me"', enter=False)
62+
63+
>>> pane.enter() # doctest: +ELLIPSIS
64+
Pane(%... Window(@... ..., Session($... ...)))
65+
66+
>>> time.sleep(0.2)
67+
68+
>>> output = pane.capture_pane()
69+
>>> 'execute me' in '\\n'.join(output)
70+
True
71+
```
72+
73+
### Literal mode for special characters
74+
75+
Use `literal=True` to send special characters without interpretation:
76+
77+
```python
78+
>>> import time
79+
80+
>>> pane.send_keys('echo "Tab:\\tNewline:\\n"', literal=True)
81+
82+
>>> time.sleep(0.1)
83+
```
84+
85+
### Suppress shell history
86+
87+
Use `suppress_history=True` to prepend a space (prevents command from being
88+
saved in shell history):
89+
90+
```python
91+
>>> import time
92+
93+
>>> pane.send_keys('echo "secret command"', suppress_history=True)
94+
95+
>>> time.sleep(0.1)
96+
```
97+
98+
## Capturing Output
99+
100+
The {meth}`~libtmux.Pane.capture_pane` method captures text from a pane's buffer.
101+
102+
### Basic capture
103+
104+
```python
105+
>>> import time
106+
107+
>>> pane.send_keys('echo "Line 1"; echo "Line 2"; echo "Line 3"')
108+
109+
>>> time.sleep(0.1)
110+
111+
>>> output = pane.capture_pane()
112+
>>> isinstance(output, list)
113+
True
114+
>>> any('Line 2' in line for line in output)
115+
True
116+
```
117+
118+
### Capture with line ranges
119+
120+
Capture specific line ranges using `start` and `end` parameters:
121+
122+
```python
123+
>>> # Capture last 5 lines of visible pane
124+
>>> recent = pane.capture_pane(start=-5, end='-')
125+
>>> isinstance(recent, list)
126+
True
127+
128+
>>> # Capture from start of history to current
129+
>>> full_history = pane.capture_pane(start='-', end='-')
130+
>>> len(full_history) >= 0
131+
True
132+
```
133+
134+
## Waiting for Output
135+
136+
A common pattern in automation is waiting for a command to complete.
137+
138+
### Polling for completion marker
139+
140+
```python
141+
>>> import time
142+
143+
>>> pane.send_keys('sleep 0.2; echo "TASK_COMPLETE"')
144+
145+
>>> # Poll for completion
146+
>>> for _ in range(30):
147+
... output = pane.capture_pane()
148+
... if 'TASK_COMPLETE' in '\\n'.join(output):
149+
... break
150+
... time.sleep(0.1)
151+
152+
>>> 'TASK_COMPLETE' in '\\n'.join(output)
153+
True
154+
```
155+
156+
### Helper function for waiting
157+
158+
```python
159+
>>> import time
160+
161+
>>> def wait_for_text(pane, text, timeout=5.0):
162+
... """Wait for text to appear in pane output."""
163+
... start = time.time()
164+
... while time.time() - start < timeout:
165+
... output = pane.capture_pane()
166+
... if text in '\\n'.join(output):
167+
... return True
168+
... time.sleep(0.1)
169+
... return False
170+
171+
>>> pane.send_keys('echo "READY"')
172+
>>> wait_for_text(pane, 'READY', timeout=2.0)
173+
True
174+
```
175+
176+
## Querying Pane State
177+
178+
The {meth}`~libtmux.Pane.display_message` method queries tmux format variables.
179+
180+
### Get pane dimensions
181+
182+
```python
183+
>>> width = pane.display_message('#{pane_width}', get_text=True)
184+
>>> isinstance(width, list) and len(width) > 0
185+
True
186+
187+
>>> height = pane.display_message('#{pane_height}', get_text=True)
188+
>>> isinstance(height, list) and len(height) > 0
189+
True
190+
```
191+
192+
### Get pane information
193+
194+
```python
195+
>>> # Current working directory
196+
>>> cwd = pane.display_message('#{pane_current_path}', get_text=True)
197+
>>> isinstance(cwd, list)
198+
True
199+
200+
>>> # Pane ID
201+
>>> pane_id = pane.display_message('#{pane_id}', get_text=True)
202+
>>> pane_id[0].startswith('%')
203+
True
204+
```
205+
206+
### Common format variables
207+
208+
| Variable | Description |
209+
|----------|-------------|
210+
| `#{pane_width}` | Pane width in characters |
211+
| `#{pane_height}` | Pane height in characters |
212+
| `#{pane_current_path}` | Current working directory |
213+
| `#{pane_pid}` | PID of the pane's shell |
214+
| `#{pane_id}` | Unique pane ID (e.g., `%0`) |
215+
| `#{pane_index}` | Pane index in window |
216+
217+
## Resizing Panes
218+
219+
The {meth}`~libtmux.Pane.resize` method adjusts pane dimensions.
220+
221+
### Resize by specific dimensions
222+
223+
```python
224+
>>> # Make pane larger
225+
>>> pane.resize(height=20, width=80) # doctest: +ELLIPSIS
226+
Pane(%... Window(@... ..., Session($... ...)))
227+
```
228+
229+
### Resize by adjustment
230+
231+
```python
232+
>>> from libtmux.constants import ResizeAdjustmentDirection
233+
234+
>>> # Increase height by 5 rows
235+
>>> pane.resize(adjustment_direction=ResizeAdjustmentDirection.Up, adjustment=5) # doctest: +ELLIPSIS
236+
Pane(%... Window(@... ..., Session($... ...)))
237+
238+
>>> # Decrease width by 10 columns
239+
>>> pane.resize(adjustment_direction=ResizeAdjustmentDirection.Left, adjustment=10) # doctest: +ELLIPSIS
240+
Pane(%... Window(@... ..., Session($... ...)))
241+
```
242+
243+
### Zoom toggle
244+
245+
```python
246+
>>> # Zoom pane to fill window
247+
>>> pane.resize(zoom=True) # doctest: +ELLIPSIS
248+
Pane(%... Window(@... ..., Session($... ...)))
249+
250+
>>> # Unzoom
251+
>>> pane.resize(zoom=True) # doctest: +ELLIPSIS
252+
Pane(%... Window(@... ..., Session($... ...)))
253+
```
254+
255+
## Clearing the Pane
256+
257+
The {meth}`~libtmux.Pane.clear` method clears the pane's screen:
258+
259+
```python
260+
>>> pane.clear() # doctest: +ELLIPSIS
261+
Pane(%... Window(@... ..., Session($... ...)))
262+
```
263+
264+
## Killing Panes
265+
266+
The {meth}`~libtmux.Pane.kill` method destroys a pane:
267+
268+
```python
269+
>>> # Create a temporary pane
270+
>>> temp_pane = pane.split()
271+
>>> temp_pane in window.panes
272+
True
273+
274+
>>> # Kill it
275+
>>> temp_pane.kill()
276+
>>> temp_pane not in window.panes
277+
True
278+
```
279+
280+
### Kill all except current
281+
282+
```python
283+
>>> # Setup: create multiple panes
284+
>>> pane.window.resize(height=60, width=120) # doctest: +ELLIPSIS
285+
Window(@... ...)
286+
287+
>>> keep_pane = pane.split()
288+
>>> extra1 = pane.split()
289+
>>> extra2 = pane.split()
290+
291+
>>> # Kill all except keep_pane
292+
>>> keep_pane.kill(all_except=True)
293+
294+
>>> keep_pane in window.panes
295+
True
296+
>>> extra1 not in window.panes
297+
True
298+
>>> extra2 not in window.panes
299+
True
300+
301+
>>> # Cleanup
302+
>>> keep_pane.kill()
303+
```
304+
305+
## Practical Recipes
306+
307+
### Recipe: Run command and capture output
308+
309+
```python
310+
>>> import time
311+
312+
>>> def run_and_capture(pane, command, marker='__DONE__', timeout=5.0):
313+
... """Run a command and return its output."""
314+
... pane.send_keys(f'{command}; echo {marker}')
315+
... start = time.time()
316+
... while time.time() - start < timeout:
317+
... output = pane.capture_pane()
318+
... output_str = '\\n'.join(output)
319+
... if marker in output_str:
320+
... return output # Return all captured output
321+
... time.sleep(0.1)
322+
... raise TimeoutError(f'Command did not complete within {timeout}s')
323+
324+
>>> result = run_and_capture(pane, 'echo "captured text"', timeout=2.0)
325+
>>> 'captured text' in '\\n'.join(result)
326+
True
327+
```
328+
329+
### Recipe: Check for error patterns
330+
331+
```python
332+
>>> import time
333+
334+
>>> def check_for_errors(pane, error_patterns=None):
335+
... """Check pane output for error patterns."""
336+
... if error_patterns is None:
337+
... error_patterns = ['error:', 'Error:', 'ERROR', 'failed', 'FAILED']
338+
... output = '\\n'.join(pane.capture_pane())
339+
... for pattern in error_patterns:
340+
... if pattern in output:
341+
... return True
342+
... return False
343+
344+
>>> pane.send_keys('echo "All good"')
345+
>>> time.sleep(0.1)
346+
>>> check_for_errors(pane)
347+
False
348+
```
349+
350+
:::{seealso}
351+
- {ref}`api` for the full API reference
352+
- {class}`~libtmux.Pane` for all pane methods
353+
- {ref}`automation-patterns` for advanced orchestration patterns
354+
:::

0 commit comments

Comments
 (0)