Skip to content

Pane.capture_pane helpers #568

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ servers
sessions
windows
panes
snapshot
constants
common
exceptions
Expand Down
111 changes: 111 additions & 0 deletions docs/api/snapshot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
(snapshot)=

# Snapshots

The snapshot module provides functionality for capturing and analyzing the state of tmux panes.

## Core Classes

```{eval-rst}
.. autoclass:: libtmux.snapshot.PaneSnapshot
:members:
:inherited-members:
:show-inheritance:
:member-order: bysource

.. autoclass:: libtmux.snapshot.PaneRecording
:members:
:inherited-members:
:show-inheritance:
:member-order: bysource
```

## Output Adapters

```{eval-rst}
.. autoclass:: libtmux.snapshot.SnapshotOutputAdapter
:members:
:inherited-members:
:show-inheritance:
:member-order: bysource

.. autoclass:: libtmux.snapshot.TerminalOutputAdapter
:members:
:inherited-members:
:show-inheritance:
:member-order: bysource

.. autoclass:: libtmux.snapshot.CLIOutputAdapter
:members:
:inherited-members:
:show-inheritance:
:member-order: bysource

.. autoclass:: libtmux.snapshot.PytestDiffAdapter
:members:
:inherited-members:
:show-inheritance:
:member-order: bysource

.. autoclass:: libtmux.snapshot.SyrupySnapshotAdapter
:members:
:inherited-members:
:show-inheritance:
:member-order: bysource
```

## Examples

### Basic Snapshot

```python
>>> pane = session.active_window.active_pane
>>> snapshot = pane.snapshot()
>>> print(snapshot.content_str)
$ echo "Hello World"
Hello World
$
```

### Recording Activity

```python
>>> recording = pane.record()
>>> recording.add_snapshot(pane)
>>> pane.send_keys("echo 'Hello'")
>>> recording.add_snapshot(pane)
>>> print(recording.latest.content_str)
$ echo 'Hello'
Hello
$
```

### Using Output Adapters

```python
>>> from libtmux.snapshot import TerminalOutputAdapter
>>> print(snapshot.format(TerminalOutputAdapter()))
=== Pane Snapshot ===
Pane: %1
Window: @1
Session: $1
Server: default
Timestamp: 2024-01-01T12:00:00Z
=== Content ===
$ echo "Hello World"
Hello World
$
```

### Custom Adapter

```python
>>> from libtmux.snapshot import SnapshotOutputAdapter
>>> class MyAdapter(SnapshotOutputAdapter):
... def format(self, snapshot):
... return f"Content: {snapshot.content_str}"
>>> print(snapshot.format(MyAdapter()))
Content: $ echo "Hello World"
Hello World
$
```
1 change: 1 addition & 0 deletions docs/topics/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ Explore libtmux’s core functionalities and underlying principles at a high lev

context_managers
traversal
snapshots
```
162 changes: 162 additions & 0 deletions docs/topics/snapshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
(snapshots)=

# Snapshots and Recordings

libtmux provides functionality to capture and analyze the state of tmux panes through snapshots and recordings.

## Taking Snapshots

A snapshot captures the content and metadata of a pane at a specific point in time:

```python
>>> pane = session.active_window.active_pane
>>> snapshot = pane.snapshot()
>>> print(snapshot.content_str)
$ echo "Hello World"
Hello World
$
```

Snapshots are immutable and include:
- Pane content
- Timestamp (in UTC)
- Pane, window, session, and server IDs
- All tmux pane metadata

You can also capture specific ranges of the pane history:

```python
>>> # Capture lines 1-3 only
>>> snapshot = pane.snapshot(start=1, end=3)

>>> # Capture from start of history
>>> snapshot = pane.snapshot(start="-")

>>> # Capture up to current view
>>> snapshot = pane.snapshot(end="-")
```

## Recording Pane Activity

To track changes in a pane over time, use recordings:

```python
>>> recording = pane.record()
>>> recording.add_snapshot(pane)
>>> pane.send_keys("echo 'Hello'")
>>> recording.add_snapshot(pane)
>>> pane.send_keys("echo 'World'")
>>> recording.add_snapshot(pane)

>>> # Access snapshots
>>> print(recording[0].content_str) # First snapshot
>>> print(recording.latest.content_str) # Most recent

>>> # Filter by time
>>> recent = recording.get_snapshots_between(
... start_time=datetime.datetime.now() - datetime.timedelta(minutes=5),
... end_time=datetime.datetime.now(),
... )
```

## Output Formats

Snapshots can be formatted in different ways for various use cases:

### Terminal Output

```python
>>> from libtmux.snapshot import TerminalOutputAdapter
>>> print(snapshot.format(TerminalOutputAdapter()))
=== Pane Snapshot ===
Pane: %1
Window: @1
Session: $1
Server: default
Timestamp: 2024-01-01T12:00:00Z
=== Content ===
$ echo "Hello World"
Hello World
$
```

### CLI Output (No Colors)

```python
>>> from libtmux.snapshot import CLIOutputAdapter
>>> print(snapshot.format(CLIOutputAdapter()))
=== Pane Snapshot ===
Pane: %1
Window: @1
Session: $1
Server: default
Timestamp: 2024-01-01T12:00:00Z
=== Content ===
$ echo "Hello World"
Hello World
$
```

### Pytest Assertion Diffs

```python
>>> from libtmux.snapshot import PytestDiffAdapter
>>> expected = """
... PaneSnapshot(
... pane_id='%1',
... window_id='@1',
... session_id='$1',
... server_name='default',
... timestamp='2024-01-01T12:00:00Z',
... content=[
... '$ echo "Hello World"',
... 'Hello World',
... '$',
... ],
... metadata={
... 'pane_height': '24',
... 'pane_width': '80',
... },
... )
... """
>>> assert snapshot.format(PytestDiffAdapter()) == expected
```

### Syrupy Snapshot Testing

```python
>>> from libtmux.snapshot import SyrupySnapshotAdapter
>>> snapshot.format(SyrupySnapshotAdapter())
{
"pane_id": "%1",
"window_id": "@1",
"session_id": "$1",
"server_name": "default",
"timestamp": "2024-01-01T12:00:00Z",
"content": [
"$ echo \"Hello World\"",
"Hello World",
"$"
],
"metadata": {
"pane_height": "24",
"pane_width": "80"
}
}
```

## Custom Output Formats

You can create custom output formats by implementing the `SnapshotOutputAdapter` interface:

```python
from libtmux.snapshot import SnapshotOutputAdapter

class MyCustomAdapter(SnapshotOutputAdapter):
def format(self, snapshot: PaneSnapshot) -> str:
# Format snapshot data as needed
return f"Custom format: {snapshot.content_str}"

# Use custom adapter
print(snapshot.format(MyCustomAdapter()))
```
38 changes: 38 additions & 0 deletions src/libtmux/pane.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
)
from libtmux.formats import FORMAT_SEPARATOR
from libtmux.neo import Obj, fetch_obj
from libtmux.snapshot import PaneRecording, PaneSnapshot

from . import exc

Expand Down Expand Up @@ -342,6 +343,43 @@ def capture_pane(
cmd.extend(["-E", str(end)])
return self.cmd(*cmd).stdout

def snapshot(
self,
start: t.Literal["-"] | int | None = None,
end: t.Literal["-"] | int | None = None,
) -> PaneSnapshot:
"""Create a snapshot of the pane's current state.

This is a convenience method that creates a :class:`PaneSnapshot` instance
from the current pane state.

Parameters
----------
start : int | "-" | None
Start line for capture_pane
end : int | "-" | None
End line for capture_pane

Returns
-------
PaneSnapshot
A frozen snapshot of the pane's current state
"""
return PaneSnapshot.from_pane(self, start=start, end=end)

def record(self) -> PaneRecording:
"""Create a new recording for this pane.

This is a convenience method that creates a :class:`PaneRecording` instance
for recording snapshots of this pane.

Returns
-------
PaneRecording
A new recording instance for this pane
"""
return PaneRecording()

def send_keys(
self,
cmd: str,
Expand Down
Loading
Loading