You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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 like isinstance(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 its WindowSnapshot, WindowSnapshot has a reference to its SessionSnapshot, 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__ importannotationsimportdatetimeimporttypingastfromdataclassesimportdataclass, field, fieldsimportcopy# Assume these come from libtmux or a similar libraryfromlibtmux.serverimportServerfromlibtmux.sessionimportSessionfromlibtmux.windowimportWindowfromlibtmux.paneimportPane@dataclass(frozen=True)classPaneSnapshot(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 fieldspane_content: t.Optional[t.List[str]] =Nonecreated_at: datetime.datetime=field(
default_factory=lambda: datetime.datetime.now(datetime.timezone.utc)
)
window_snapshot: t.Optional[WindowSnapshot] =None# Link back to parent windowdefcmd(self, *args, **kwargs):
"""Prevent executing tmux commands on a snapshot."""raiseNotImplementedError("PaneSnapshot is read-only and cannot execute tmux commands")
defcapture_pane(self, *args, **kwargs):
"""Return the pre-captured content instead of hitting tmux."""returnself.pane_contentor []
@propertydefwindow(self) ->t.Optional[WindowSnapshot]:
"""Return the WindowSnapshot link, rather than a live Window."""returnself.window_snapshot@classmethoddeffrom_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 contentpane_content=Noneifcapture_content:
try:
pane_content=pane.capture_pane()
exceptException:
pass# If capturing fails, leave it None# Gather fields from the parent Pane class# We exclude 'window' to avoid pulling in a live referencefield_values= {}
forfinfields(pane.__class__):
iff.namenotin ["window", "server"]:
ifhasattr(pane, f.name):
field_values[f.name] =copy.deepcopy(getattr(pane, f.name))
# Add snapshot-specific fieldsfield_values["pane_content"] =pane_contentfield_values["window_snapshot"] =window_snapshotreturncls(**field_values)
@dataclass(frozen=True)classWindowSnapshot(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 sessiondefcmd(self, *args, **kwargs):
raiseNotImplementedError("WindowSnapshot is read-only and cannot execute tmux commands")
@propertydefpanes(self) ->t.List[PaneSnapshot]:
"""Return the snapshot list of panes."""returnself.panes_snapshot@propertydefsession(self) ->t.Optional[SessionSnapshot]:
"""Return the SessionSnapshot link, rather than a live Session."""returnself.session_snapshot@classmethoddeffrom_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= {}
forfinfields(window.__class__):
iff.namenotin ["session", "server", "panes"]:
ifhasattr(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.ifinclude_panes:
all_panes= []
forpaneinwindow.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)
returnsnapshot@dataclass(frozen=True)classSessionSnapshot(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 serverdefcmd(self, *args, **kwargs):
raiseNotImplementedError("SessionSnapshot is read-only and cannot execute tmux commands")
@propertydefwindows(self) ->t.List[WindowSnapshot]:
"""Return the snapshot list of windows."""returnself.windows_snapshot@propertydefserver(self) ->t.Optional[ServerSnapshot]:
"""Return the ServerSnapshot link, rather than a live Server."""returnself.server_snapshot@classmethoddeffrom_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= {}
forfinfields(session.__class__):
iff.namenotin ["server", "windows"]:
ifhasattr(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.ifinclude_windows:
all_windows= []
forwindowinsession.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)
returnsnapshot@dataclass(frozen=True)classServerSnapshot(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)
defcmd(self, *args, **kwargs):
raiseNotImplementedError("ServerSnapshot is read-only and cannot execute tmux commands")
@propertydefsessions(self) ->t.List[SessionSnapshot]:
"""Return the snapshot list of sessions."""returnself.sessions_snapshot@classmethoddeffrom_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= {}
forfinfields(server.__class__):
iff.namenotin ["sessions"]:
ifhasattr(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.ifinclude_sessions:
all_sessions= []
forsessioninserver.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)
returnsnapshot# -----------------------------# Filtering Utilities# -----------------------------deffilter_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 levelifisinstance(snapshot, ServerSnapshot):
filtered_sessions= []
forsessinsnapshot.sessions_snapshot:
iffilter_func(sess):
new_sess=filter_snapshot(sess, filter_func)
ifnew_sessisnotNone:
filtered_sessions.append(new_sess)
# If the server itself fails the filter, discard entirelyifnotfilter_func(snapshot) andnotfiltered_sessions:
returnNone# Create a copy with filtered sessionsresult=copy.deepcopy(snapshot)
object.__setattr__(result, "sessions_snapshot", filtered_sessions)
# Fix the back-reference from sessions to serverforsess_snapinfiltered_sessions:
object.__setattr__(sess_snap, "server_snapshot", result)
returnresult# Session levelifisinstance(snapshot, SessionSnapshot):
filtered_windows= []
forwinsnapshot.windows_snapshot:
iffilter_func(w):
new_w=filter_snapshot(w, filter_func)
ifnew_wisnotNone:
filtered_windows.append(new_w)
ifnotfilter_func(snapshot) andnotfiltered_windows:
returnNoneresult=copy.deepcopy(snapshot)
object.__setattr__(result, "windows_snapshot", filtered_windows)
# Fix the back-reference from windows to sessionforw_snapinfiltered_windows:
object.__setattr__(w_snap, "session_snapshot", result)
returnresult# Window levelifisinstance(snapshot, WindowSnapshot):
filtered_panes= []
forpinsnapshot.panes_snapshot:
iffilter_func(p):
filtered_panes.append(p) # Pane is leaf-level except for reference to windowifnotfilter_func(snapshot) andnotfiltered_panes:
returnNoneresult=copy.deepcopy(snapshot)
object.__setattr__(result, "panes_snapshot", filtered_panes)
# Fix the back-reference from panes to windowforp_snapinfiltered_panes:
object.__setattr__(p_snap, "window_snapshot", result)
returnresult# Pane levelifisinstance(snapshot, PaneSnapshot):
iffilter_func(snapshot):
returnsnapshotelse:
returnNone# Unrecognized type → pass through or Nonereturnsnapshotiffilter_func(snapshot) elseNone# -----------------------------# Serialization Utility# -----------------------------defsnapshot_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.)ifnotisinstance(snapshot, (ServerSnapshot, SessionSnapshot, WindowSnapshot, PaneSnapshot)):
returnsnapshotresult= {}
forfinfields(snapshot):
name=f.name# If this is a parent reference field, skip it to avoid cyclesifnamein ["server_snapshot", "session_snapshot", "window_snapshot"]:
continuevalue=getattr(snapshot, name)
# Recurse on listsifisinstance(value, list):
result[name] = [snapshot_to_dict(item) foriteminvalue]
else:
# Recurse on single itemsifisinstance(value, (ServerSnapshot, SessionSnapshot, WindowSnapshot, PaneSnapshot)):
result[name] =snapshot_to_dict(value)
else:
result[name] =valuereturnresult# -----------------------------# Example Usage# -----------------------------defsnapshot_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)
defis_active(obj):
returnbool(getattr(obj, "active", False))
filtered=filter_snapshot(full_snapshot, is_active)
iffilteredisNone:
raiseValueError("No active objects found!")
returnfiltered
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 of Pane, Window, etc.
True Immutability
Using @dataclass(frozen=True) ensures that once the snapshot is constructed, its fields cannot be modified. We use object.__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
The filter_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, and Pane 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.
Use cases like filtering, partial snapshots, or specialized serialization are straightforward to layer onto these vanilla dataclasses with minimal boilerplate.
The text was updated successfully, but these errors were encountered:
libtmux:
The Snapshot.
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:
Key Requirements:
Server
,Session
,Window
,Pane
.Design Highlights
PaneSnapshot(Pane)
) so that existing code can still perform type checks likeisinstance(snapshot, Pane)
.@dataclass(frozen=True)
ensures immutability. Once created, attributes cannot be changed.cmd
,capture_pane
, etc.) are overridden to either:NotImplementedError
if the action would require writing or sending commands to tmux.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.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
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.SessionSnapshot.from_session(...)
can skip or include windows.Filtering
The
filter_snapshot
function demonstrates how to prune snapshots based on a predicate function.None
if everything is filtered out).Serialization
snapshot_to_dict(...)
recursively converts snapshots to dictionaries, skipping parent references to avoid circular loops.Summary of Why This Approach Works Well
Server
,Session
,Window
, andPane
objects in play.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
Use cases like filtering, partial snapshots, or specialized serialization are straightforward to layer onto these vanilla dataclasses with minimal boilerplate.
The text was updated successfully, but these errors were encountered: