Description
libtmux:
The Snapshot.
class Snapshot:
created_at: datetime.utcnow()
id: tmux_id
def refresh_from_cli():
# return new object with new created_at
class Session(Snapshot):
pass
class Window(Snapshot):
pass
acronyms: Frame, tmux object
This is a state of tmux at a certain time.
You can know, based on time, the likelihood a certain object is stale. What it means is that you can audit a terminal and trail it, you can record it, you can print the terminal to text. You can export the session, window, pane's contents to a file. You can assert that certain text exists in a pane, window, session, server.
A snapshot could be pickable. It could be serializable in various other formats
This can open doors to new ways of interacting with the terminal not hitherto conceived
Appendix
Proposal
Goal: Provide frozen, read-only, hierarchical snapshots of tmux objects:
- ServerSnapshot (immutable server)
- SessionSnapshot (immutable session)
- WindowSnapshot (immutable window)
- PaneSnapshot (immutable pane)
Key Requirements:
- Preserve Type Information: Mirror fields from
Server
,Session
,Window
,Pane
. - Immutable / Read-Only: Prevent modifications or tmux command execution.
- Accurate Object Graph: Retain parent-child references (server → sessions → windows → panes).
- Minimize Overhead: Avoid copying unnecessary data.
- Optional Filtering: Provide a way to selectively keep or discard certain sessions/windows/panes in snapshots.
- Serialization: Offer a convenient way to convert snapshots to Python dictionaries (or JSON), avoiding circular references.
Design Highlights
- Inheritance: Each snapshot class inherits from the corresponding tmux object class (e.g.,
PaneSnapshot(Pane)
) so that existing code can still perform type checks likeisinstance(snapshot, Pane)
. - Frozen Dataclasses: Using
@dataclass(frozen=True)
ensures immutability. Once created, attributes cannot be changed. - Method Overrides: Methods that perform tmux commands (
cmd
,capture_pane
, etc.) are overridden to either:- Return cached data (e.g., pre-captured pane contents), or
- Raise
NotImplementedError
if the action would require writing or sending commands to tmux.
- Parent-Child Links:
PaneSnapshot
has a reference to itsWindowSnapshot
,WindowSnapshot
has a reference to itsSessionSnapshot
, etc. These references let you traverse the entire snapshot hierarchy from any node. - Filtering: A
filter_snapshot
function can traverse the snapshot hierarchy and produce a new, pruned snapshot that keeps only the objects that satisfy a user-supplied predicate (e.g., only “active” windows).
Below is the unified code that brings these elements together.
Complete Dataclass Snapshot Implementation
from __future__ import annotations
import datetime
import typing as t
from dataclasses import dataclass, field, fields
import copy
# Assume these come from libtmux or a similar library
from libtmux.server import Server
from libtmux.session import Session
from libtmux.window import Window
from libtmux.pane import Pane
@dataclass(frozen=True)
class PaneSnapshot(Pane):
"""
Immutable snapshot of a Pane. Inherits from Pane so that:
1) It's recognized as a Pane type.
2) We can easily copy fields from the original Pane.
"""
# Snapshot-specific fields
pane_content: t.Optional[t.List[str]] = None
created_at: datetime.datetime = field(
default_factory=lambda: datetime.datetime.now(datetime.timezone.utc)
)
window_snapshot: t.Optional[WindowSnapshot] = None # Link back to parent window
def cmd(self, *args, **kwargs):
"""Prevent executing tmux commands on a snapshot."""
raise NotImplementedError("PaneSnapshot is read-only and cannot execute tmux commands")
def capture_pane(self, *args, **kwargs):
"""Return the pre-captured content instead of hitting tmux."""
return self.pane_content or []
@property
def window(self) -> t.Optional[WindowSnapshot]:
"""Return the WindowSnapshot link, rather than a live Window."""
return self.window_snapshot
@classmethod
def from_pane(
cls,
pane: Pane,
*,
capture_content: bool = True,
window_snapshot: t.Optional[WindowSnapshot] = None
) -> PaneSnapshot:
"""
Factory method to create a PaneSnapshot from a live Pane.
capture_content=True to fetch the current text from the pane
window_snapshot to link this pane back to a parent WindowSnapshot
"""
# Try capturing the pane’s content
pane_content = None
if capture_content:
try:
pane_content = pane.capture_pane()
except Exception:
pass # If capturing fails, leave it None
# Gather fields from the parent Pane class
# We exclude 'window' to avoid pulling in a live reference
field_values = {}
for f in fields(pane.__class__):
if f.name not in ["window", "server"]:
if hasattr(pane, f.name):
field_values[f.name] = copy.deepcopy(getattr(pane, f.name))
# Add snapshot-specific fields
field_values["pane_content"] = pane_content
field_values["window_snapshot"] = window_snapshot
return cls(**field_values)
@dataclass(frozen=True)
class WindowSnapshot(Window):
"""
Immutable snapshot of a Window.
"""
created_at: datetime.datetime = field(
default_factory=lambda: datetime.datetime.now(datetime.timezone.utc)
)
panes_snapshot: t.List[PaneSnapshot] = field(default_factory=list)
session_snapshot: t.Optional[SessionSnapshot] = None # Link back to parent session
def cmd(self, *args, **kwargs):
raise NotImplementedError("WindowSnapshot is read-only and cannot execute tmux commands")
@property
def panes(self) -> t.List[PaneSnapshot]:
"""Return the snapshot list of panes."""
return self.panes_snapshot
@property
def session(self) -> t.Optional[SessionSnapshot]:
"""Return the SessionSnapshot link, rather than a live Session."""
return self.session_snapshot
@classmethod
def from_window(
cls,
window: Window,
*,
include_panes: bool = True,
session_snapshot: t.Optional[SessionSnapshot] = None
) -> WindowSnapshot:
"""
Create a snapshot from a live Window.
include_panes=True to also snapshot all the window's panes
session_snapshot to link this window back to a parent SessionSnapshot
"""
field_values = {}
for f in fields(window.__class__):
if f.name not in ["session", "server", "panes"]:
if hasattr(window, f.name):
field_values[f.name] = copy.deepcopy(getattr(window, f.name))
# Construct the WindowSnapshot (initially without panes)
snapshot = cls(
**field_values,
session_snapshot=session_snapshot,
panes_snapshot=[]
)
# If requested, snapshot all panes. Then fix back-references.
if include_panes:
all_panes = []
for pane in window.panes:
pane_snapshot = PaneSnapshot.from_pane(
pane,
capture_content=True,
window_snapshot=snapshot
)
all_panes.append(pane_snapshot)
object.__setattr__(snapshot, "panes_snapshot", all_panes)
return snapshot
@dataclass(frozen=True)
class SessionSnapshot(Session):
"""
Immutable snapshot of a Session.
"""
created_at: datetime.datetime = field(
default_factory=lambda: datetime.datetime.now(datetime.timezone.utc)
)
windows_snapshot: t.List[WindowSnapshot] = field(default_factory=list)
server_snapshot: t.Optional[ServerSnapshot] = None # Link back to parent server
def cmd(self, *args, **kwargs):
raise NotImplementedError("SessionSnapshot is read-only and cannot execute tmux commands")
@property
def windows(self) -> t.List[WindowSnapshot]:
"""Return the snapshot list of windows."""
return self.windows_snapshot
@property
def server(self) -> t.Optional[ServerSnapshot]:
"""Return the ServerSnapshot link, rather than a live Server."""
return self.server_snapshot
@classmethod
def from_session(
cls,
session: Session,
*,
include_windows: bool = True,
server_snapshot: t.Optional[ServerSnapshot] = None
) -> SessionSnapshot:
"""
Create a snapshot from a live Session.
include_windows=True to also snapshot all the session's windows
server_snapshot to link this session back to a parent ServerSnapshot
"""
field_values = {}
for f in fields(session.__class__):
if f.name not in ["server", "windows"]:
if hasattr(session, f.name):
field_values[f.name] = copy.deepcopy(getattr(session, f.name))
# Construct the SessionSnapshot (initially without windows)
snapshot = cls(
**field_values,
windows_snapshot=[],
server_snapshot=server_snapshot
)
# If requested, snapshot all windows. Then fix back-references.
if include_windows:
all_windows = []
for window in session.windows:
window_snapshot = WindowSnapshot.from_window(
window,
include_panes=True,
session_snapshot=snapshot
)
all_windows.append(window_snapshot)
object.__setattr__(snapshot, "windows_snapshot", all_windows)
return snapshot
@dataclass(frozen=True)
class ServerSnapshot(Server):
"""
Immutable snapshot of a Server.
"""
created_at: datetime.datetime = field(
default_factory=lambda: datetime.datetime.now(datetime.timezone.utc)
)
sessions_snapshot: t.List[SessionSnapshot] = field(default_factory=list)
def cmd(self, *args, **kwargs):
raise NotImplementedError("ServerSnapshot is read-only and cannot execute tmux commands")
@property
def sessions(self) -> t.List[SessionSnapshot]:
"""Return the snapshot list of sessions."""
return self.sessions_snapshot
@classmethod
def from_server(cls, server: Server, *, include_sessions: bool = True) -> ServerSnapshot:
"""
Create a snapshot from a live Server.
include_sessions=True to also snapshot all the server's sessions
"""
field_values = {}
for f in fields(server.__class__):
if f.name not in ["sessions"]:
if hasattr(server, f.name):
field_values[f.name] = copy.deepcopy(getattr(server, f.name))
# Construct the ServerSnapshot (initially without sessions)
snapshot = cls(
**field_values,
sessions_snapshot=[]
)
# If requested, snapshot all sessions. Then fix back-references.
if include_sessions:
all_sessions = []
for session in server.sessions:
session_snapshot = SessionSnapshot.from_session(
session,
include_windows=True,
server_snapshot=snapshot
)
all_sessions.append(session_snapshot)
object.__setattr__(snapshot, "sessions_snapshot", all_sessions)
return snapshot
# -----------------------------
# Filtering Utilities
# -----------------------------
def filter_snapshot(snapshot, filter_func) -> t.Union[
ServerSnapshot,
SessionSnapshot,
WindowSnapshot,
PaneSnapshot,
None
]:
"""
Recursively filter snapshots based on a user-supplied function.
filter_func(obj) should return True if the object should be retained;
False if it should be pruned entirely.
Returns a new snapshot with references updated, or None if everything is filtered out.
"""
# Server level
if isinstance(snapshot, ServerSnapshot):
filtered_sessions = []
for sess in snapshot.sessions_snapshot:
if filter_func(sess):
new_sess = filter_snapshot(sess, filter_func)
if new_sess is not None:
filtered_sessions.append(new_sess)
# If the server itself fails the filter, discard entirely
if not filter_func(snapshot) and not filtered_sessions:
return None
# Create a copy with filtered sessions
result = copy.deepcopy(snapshot)
object.__setattr__(result, "sessions_snapshot", filtered_sessions)
# Fix the back-reference from sessions to server
for sess_snap in filtered_sessions:
object.__setattr__(sess_snap, "server_snapshot", result)
return result
# Session level
if isinstance(snapshot, SessionSnapshot):
filtered_windows = []
for w in snapshot.windows_snapshot:
if filter_func(w):
new_w = filter_snapshot(w, filter_func)
if new_w is not None:
filtered_windows.append(new_w)
if not filter_func(snapshot) and not filtered_windows:
return None
result = copy.deepcopy(snapshot)
object.__setattr__(result, "windows_snapshot", filtered_windows)
# Fix the back-reference from windows to session
for w_snap in filtered_windows:
object.__setattr__(w_snap, "session_snapshot", result)
return result
# Window level
if isinstance(snapshot, WindowSnapshot):
filtered_panes = []
for p in snapshot.panes_snapshot:
if filter_func(p):
filtered_panes.append(p) # Pane is leaf-level except for reference to window
if not filter_func(snapshot) and not filtered_panes:
return None
result = copy.deepcopy(snapshot)
object.__setattr__(result, "panes_snapshot", filtered_panes)
# Fix the back-reference from panes to window
for p_snap in filtered_panes:
object.__setattr__(p_snap, "window_snapshot", result)
return result
# Pane level
if isinstance(snapshot, PaneSnapshot):
if filter_func(snapshot):
return snapshot
else:
return None
# Unrecognized type → pass through or None
return snapshot if filter_func(snapshot) else None
# -----------------------------
# Serialization Utility
# -----------------------------
def snapshot_to_dict(snapshot) -> dict:
"""
Recursively convert a snapshot into a dictionary,
avoiding circular references (server->session->server, etc.).
"""
# Base case: For non-snapshot objects, just return them directly
# (In practice, this is rarely triggered, so we focus on known classes.)
if not isinstance(snapshot, (ServerSnapshot, SessionSnapshot, WindowSnapshot, PaneSnapshot)):
return snapshot
result = {}
for f in fields(snapshot):
name = f.name
# If this is a parent reference field, skip it to avoid cycles
if name in ["server_snapshot", "session_snapshot", "window_snapshot"]:
continue
value = getattr(snapshot, name)
# Recurse on lists
if isinstance(value, list):
result[name] = [snapshot_to_dict(item) for item in value]
else:
# Recurse on single items
if isinstance(value, (ServerSnapshot, SessionSnapshot, WindowSnapshot, PaneSnapshot)):
result[name] = snapshot_to_dict(value)
else:
result[name] = value
return result
# -----------------------------
# Example Usage
# -----------------------------
def snapshot_active_only(server: Server) -> ServerSnapshot:
"""
Create a server snapshot that keeps only 'active' sessions, windows, and panes.
For example, if an item has the attribute .active = True, we keep it;
otherwise we prune it from the snapshot.
"""
full_snapshot = ServerSnapshot.from_server(server)
def is_active(obj):
return bool(getattr(obj, "active", False))
filtered = filter_snapshot(full_snapshot, is_active)
if filtered is None:
raise ValueError("No active objects found!")
return filtered
Key Features in This Unified Dataclass Approach
-
Inheritance from Tmux Classes
Each*Snapshot
class extends the corresponding live class (PaneSnapshot(Pane)
, etc.). This allows direct compatibility with code that expects an instance ofPane
,Window
, etc. -
True Immutability
Using@dataclass(frozen=True)
ensures that once the snapshot is constructed, its fields cannot be modified. We useobject.__setattr__
only during construction (to fill child references after the object is created). -
Optional Hierarchical Construction
ServerSnapshot.from_server(...)
can recursively build session, window, and pane snapshots in one call.- Similarly,
SessionSnapshot.from_session(...)
can skip or include windows. - This makes snapshot creation flexible depending on the user’s needs.
-
Filtering
Thefilter_snapshot
function demonstrates how to prune snapshots based on a predicate function.- Example usage: filter out non-active sessions, or remove certain windows by name, etc.
- The function returns a new snapshot graph (or
None
if everything is filtered out).
-
Serialization
snapshot_to_dict(...)
recursively converts snapshots to dictionaries, skipping parent references to avoid circular loops.- The resulting dictionary can be serialized to JSON, YAML, or any other format.
Summary of Why This Approach Works Well
- No External Dependencies: Pure Python dataclasses, which is lighter than introducing a library like Pydantic.
- Consistent with Existing Code: Inheriting from the original classes provides a natural migration path if you already have
Server
,Session
,Window
, andPane
objects in play. - Safe Read-Only API: All tmux commands are disabled, ensuring your snapshots won’t accidentally mutate or command a live tmux session.
- Flexible Filtering: You can tailor which objects remain in your snapshots.
- Clarity: The
from_*
factory methods highlight exactly how data is copied from the live object to the snapshot.
If your main focus is maximum performance with no overhead for validation or complex features, then this dataclass-based approach is sufficient. If you ever need advanced validation or dynamic model creation, you could consider Pydantic, but that remains optional.
References
- [Python Dataclasses Documentation](https://docs.python.org/3/library/dataclasses.html)
- [tmux](https://github.com/tmux/tmux)
- [libtmux GitHub](https://github.com/tmux-python/libtmux)
Use cases like filtering, partial snapshots, or specialized serialization are straightforward to layer onto these vanilla dataclasses with minimal boilerplate.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status