|
| 1 | +# libtmux Snapshot Module |
| 2 | + |
| 3 | +The snapshot module provides a powerful way to capture the state of tmux objects (Server, Session, Window, Pane) as immutable, hierarchical snapshots. These snapshots preserve the structure and relationships between tmux objects while allowing for inspection, filtering, and serialization. |
| 4 | + |
| 5 | +## Value Proposition |
| 6 | + |
| 7 | +Snapshots provide several key benefits for tmux automation and management: |
| 8 | + |
| 9 | +- **Point-in-time Captures**: Create immutable records of tmux state at specific moments |
| 10 | +- **State Inspection**: Examine the structure of sessions, windows, and panes without modifying them |
| 11 | +- **Testing Support**: Build reliable tests with deterministic tmux state snapshots |
| 12 | +- **Comparison & Diff**: Compare configurations between different sessions or before/after changes |
| 13 | +- **State Serialization**: Convert tmux state to dictionaries for storage or analysis |
| 14 | +- **Safety & Predictability**: Work with tmux state without modifying the actual tmux server |
| 15 | +- **Content Preservation**: Optionally capture pane content to preserve terminal output |
| 16 | +- **Filtering & Searching**: Find specific components within complex tmux arrangements |
| 17 | + |
| 18 | +## Installation |
| 19 | + |
| 20 | +The snapshot module is included with libtmux: |
| 21 | + |
| 22 | +```bash |
| 23 | +pip install libtmux |
| 24 | +``` |
| 25 | + |
| 26 | +## Basic Usage |
| 27 | + |
| 28 | +Creating snapshots is straightforward using the factory functions: |
| 29 | + |
| 30 | +```python |
| 31 | +from libtmux import Server |
| 32 | +from libtmux.snapshot.factory import create_snapshot, create_snapshot_active |
| 33 | + |
| 34 | +# Connect to the tmux server |
| 35 | +server = Server() |
| 36 | + |
| 37 | +# Create a complete snapshot of the entire tmux server |
| 38 | +server_snapshot = create_snapshot(server) |
| 39 | + |
| 40 | +# Create a snapshot that only includes active sessions, windows, and panes |
| 41 | +active_snapshot = create_snapshot_active(server) |
| 42 | + |
| 43 | +# Create a snapshot with pane content captured |
| 44 | +content_snapshot = create_snapshot(server, capture_content=True) |
| 45 | + |
| 46 | +# Create a snapshot of a specific session |
| 47 | +session = server.find_where({"session_name": "dev"}) |
| 48 | +if session: |
| 49 | + session_snapshot = create_snapshot(session) |
| 50 | + |
| 51 | +# Create a snapshot of a specific window |
| 52 | +window = session.attached_window |
| 53 | +if window: |
| 54 | + window_snapshot = create_snapshot(window) |
| 55 | + |
| 56 | +# Create a snapshot of a specific pane |
| 57 | +pane = window.attached_pane |
| 58 | +if pane: |
| 59 | + pane_snapshot = create_snapshot(pane) |
| 60 | +``` |
| 61 | + |
| 62 | +## Working with Snapshots |
| 63 | + |
| 64 | +Once you have a snapshot, you can navigate its hierarchy just like regular tmux objects: |
| 65 | + |
| 66 | +```python |
| 67 | +# Inspecting the server snapshot |
| 68 | +server_snapshot = create_snapshot(server) |
| 69 | +print(f"Server has {len(server_snapshot.sessions)} sessions") |
| 70 | + |
| 71 | +# Navigate the snapshot hierarchy |
| 72 | +for session in server_snapshot.sessions: |
| 73 | + print(f"Session: {session.name} ({len(session.windows)} windows)") |
| 74 | + |
| 75 | + for window in session.windows: |
| 76 | + print(f" Window: {window.name} (index: {window.index})") |
| 77 | + |
| 78 | + for pane in window.panes: |
| 79 | + print(f" Pane: {pane.pane_id} (active: {pane.active})") |
| 80 | + |
| 81 | + # If content was captured |
| 82 | + if pane.pane_content: |
| 83 | + print(f" Content lines: {len(pane.pane_content)}") |
| 84 | +``` |
| 85 | + |
| 86 | +## Filtering Snapshots |
| 87 | + |
| 88 | +The snapshot API provides powerful filtering capabilities: |
| 89 | + |
| 90 | +```python |
| 91 | +# Filter a snapshot to only include a particular session |
| 92 | +dev_snapshot = server_snapshot.filter( |
| 93 | + lambda s: getattr(s, "name", "") == "dev" or getattr(s, "session_name", "") == "dev" |
| 94 | +) |
| 95 | + |
| 96 | +# Filter for a specific window |
| 97 | +target_window_snapshot = server_snapshot.filter( |
| 98 | + lambda s: getattr(s, "window_id", "") == "$1" |
| 99 | +) |
| 100 | + |
| 101 | +# Filter for active panes only |
| 102 | +active_panes_snapshot = server_snapshot.filter( |
| 103 | + lambda s: getattr(s, "active", False) is True |
| 104 | +) |
| 105 | + |
| 106 | +# Complex filtering: sessions with at least one window containing "test" in the name |
| 107 | +def has_test_window(obj): |
| 108 | + if hasattr(obj, "windows"): |
| 109 | + return any("test" in w.name.lower() for w in obj.windows) |
| 110 | + return "test" in getattr(obj, "name", "").lower() |
| 111 | + |
| 112 | +test_snapshot = server_snapshot.filter(has_test_window) |
| 113 | +``` |
| 114 | + |
| 115 | +## Serializing to Dictionaries |
| 116 | + |
| 117 | +Snapshots can be easily converted to dictionaries for storage or analysis: |
| 118 | + |
| 119 | +```python |
| 120 | +# Convert a snapshot to a dictionary for serialization or inspection |
| 121 | +snapshot_dict = server_snapshot.to_dict() |
| 122 | + |
| 123 | +# Pretty print the structure |
| 124 | +import json |
| 125 | +print(json.dumps(snapshot_dict, indent=2)) |
| 126 | + |
| 127 | +# Selective dictionary conversion |
| 128 | +session = server_snapshot.sessions[0] |
| 129 | +session_dict = session.to_dict() |
| 130 | +``` |
| 131 | + |
| 132 | +## Common Use Cases |
| 133 | + |
| 134 | +### Testing tmux Applications |
| 135 | + |
| 136 | +Snapshots make it easy to verify that tmux automations produce the expected state: |
| 137 | + |
| 138 | +```python |
| 139 | +def test_my_tmux_function(): |
| 140 | + # Setup |
| 141 | + server = Server() |
| 142 | + session = server.new_session("test-session") |
| 143 | + |
| 144 | + # Take a snapshot before |
| 145 | + before_snapshot = create_snapshot(server) |
| 146 | + |
| 147 | + # Run the function being tested |
| 148 | + my_tmux_function(session) |
| 149 | + |
| 150 | + # Take a snapshot after |
| 151 | + after_snapshot = create_snapshot(server) |
| 152 | + |
| 153 | + # Assert expected changes |
| 154 | + assert len(after_snapshot.sessions) == len(before_snapshot.sessions) + 1 |
| 155 | + |
| 156 | + # Find the newly created session |
| 157 | + new_session = next( |
| 158 | + (s for s in after_snapshot.sessions if s not in before_snapshot.sessions), |
| 159 | + None |
| 160 | + ) |
| 161 | + assert new_session is not None |
| 162 | + assert new_session.name == "expected-name" |
| 163 | + assert len(new_session.windows) == 3 # Expected window count |
| 164 | +``` |
| 165 | + |
| 166 | +### Creating Reattachable Sessions |
| 167 | + |
| 168 | +```python |
| 169 | +# Take a snapshot before making changes |
| 170 | +snapshot = create_snapshot(server) |
| 171 | + |
| 172 | +# Make changes to tmux |
| 173 | +# ... |
| 174 | + |
| 175 | +# Find a session from the snapshot to reattach |
| 176 | +original_session = snapshot.filter(lambda s: getattr(s, "name", "") == "main") |
| 177 | +if original_session and hasattr(original_session, "name"): |
| 178 | + # Reattach to that session using its name |
| 179 | + server.cmd("attach-session", "-t", original_session.name) |
| 180 | +``` |
| 181 | + |
| 182 | +### Comparing Window Configurations |
| 183 | + |
| 184 | +```python |
| 185 | +# Take a snapshot of two different sessions |
| 186 | +session1 = server.find_where({"session_name": "dev"}) |
| 187 | +session2 = server.find_where({"session_name": "prod"}) |
| 188 | + |
| 189 | +if session1 and session2: |
| 190 | + snapshot1 = create_snapshot(session1) |
| 191 | + snapshot2 = create_snapshot(session2) |
| 192 | + |
| 193 | + # Compare window layouts |
| 194 | + for window1 in snapshot1.windows: |
| 195 | + # Find matching window in session2 by name |
| 196 | + matching_windows = [w for w in snapshot2.windows if w.name == window1.name] |
| 197 | + if matching_windows: |
| 198 | + window2 = matching_windows[0] |
| 199 | + print(f"Window {window1.name}:") |
| 200 | + print(f" Session 1 layout: {window1.layout}") |
| 201 | + print(f" Session 2 layout: {window2.layout}") |
| 202 | + print(f" Layouts match: {window1.layout == window2.layout}") |
| 203 | +``` |
| 204 | + |
| 205 | +### Monitoring Pane Content Changes |
| 206 | + |
| 207 | +```python |
| 208 | +import time |
| 209 | + |
| 210 | +# Create a snapshot with pane content |
| 211 | +pane = server.sessions[0].attached_window.attached_pane |
| 212 | +snapshot1 = create_snapshot(pane, capture_content=True) |
| 213 | + |
| 214 | +# Wait for potential changes |
| 215 | +time.sleep(5) |
| 216 | + |
| 217 | +# Take another snapshot |
| 218 | +snapshot2 = create_snapshot(pane, capture_content=True) |
| 219 | + |
| 220 | +# Compare content |
| 221 | +if snapshot1.pane_content and snapshot2.pane_content: |
| 222 | + content1 = "\n".join(snapshot1.pane_content) |
| 223 | + content2 = "\n".join(snapshot2.pane_content) |
| 224 | + |
| 225 | + if content1 != content2: |
| 226 | + print("Content changed!") |
| 227 | + |
| 228 | + # Show a simple diff |
| 229 | + import difflib |
| 230 | + diff = difflib.unified_diff( |
| 231 | + snapshot1.pane_content, |
| 232 | + snapshot2.pane_content, |
| 233 | + fromfile="before", |
| 234 | + tofile="after", |
| 235 | + ) |
| 236 | + print("\n".join(diff)) |
| 237 | +``` |
| 238 | + |
| 239 | +### Saving and Restoring Window Arrangements |
| 240 | + |
| 241 | +```python |
| 242 | +import json |
| 243 | +import os |
| 244 | + |
| 245 | +# Save the current tmux session arrangement |
| 246 | +def save_arrangement(session_name, filepath): |
| 247 | + session = server.find_where({"session_name": session_name}) |
| 248 | + if session: |
| 249 | + snapshot = create_snapshot(session) |
| 250 | + with open(filepath, "w") as f: |
| 251 | + json.dump(snapshot.to_dict(), f, indent=2) |
| 252 | + print(f"Saved arrangement to {filepath}") |
| 253 | + else: |
| 254 | + print(f"Session '{session_name}' not found") |
| 255 | + |
| 256 | +# Example usage |
| 257 | +save_arrangement("dev", "dev_arrangement.json") |
| 258 | + |
| 259 | +# This function could be paired with a restore function that |
| 260 | +# recreates the session from the saved arrangement |
| 261 | +``` |
| 262 | + |
| 263 | +## Best Practices |
| 264 | + |
| 265 | +- **Immutability**: Remember that snapshots are immutable - methods return new objects rather than modifying the original |
| 266 | +- **Timing**: Snapshots represent the state at the time they were created - they don't update automatically |
| 267 | +- **Memory Usage**: Be cautious with `capture_content=True` on many panes, as this captures all pane content and can use significant memory |
| 268 | +- **Filtering**: The `filter()` method is powerful for finding specific objects within the snapshot hierarchy |
| 269 | +- **Type Safety**: The API uses strong typing - take advantage of type hints in your code |
| 270 | + |
| 271 | +## API Overview |
| 272 | + |
| 273 | +The snapshot module follows this structure: |
| 274 | + |
| 275 | +- Factory functions in `factory.py`: |
| 276 | + - `create_snapshot(obj)`: Create a snapshot of any tmux object |
| 277 | + - `create_snapshot_active(server)`: Create a snapshot with only active components |
| 278 | + |
| 279 | +- Snapshot classes in `models/`: |
| 280 | + - `ServerSnapshot`: Snapshot of a tmux server |
| 281 | + - `SessionSnapshot`: Snapshot of a tmux session |
| 282 | + - `WindowSnapshot`: Snapshot of a tmux window |
| 283 | + - `PaneSnapshot`: Snapshot of a tmux pane |
| 284 | + |
| 285 | +- Common methods on all snapshot classes: |
| 286 | + - `to_dict()`: Convert to a dictionary |
| 287 | + - `filter(predicate)`: Apply a filter function to this snapshot and its children |
0 commit comments