1111- Respects user's last saved version as fallback
1212"""
1313
14- import hashlib
1514import logging
1615from typing import Optional , Callable
17- from pathlib import Path
1816
1917try :
2018 from PySide6 .QtCore import QTimer , QObject , Signal
19+
2120 HAS_PYSIDE = True
2221except ImportError :
2322 HAS_PYSIDE = False
3433class AutosaveManager (QObject if HAS_PYSIDE else object ):
3534 """
3635 Manages automatic saving of projects with intelligent change detection.
37-
36+
3837 Never saves unnecessarily — only when state actually changes.
3938 Uses debouncing to prevent rapid successive saves.
4039 """
41-
40+
4241 if HAS_PYSIDE :
4342 autosave_triggered = Signal (str ) # (reason)
44-
43+
4544 def __init__ (self ):
4645 if HAS_PYSIDE :
4746 super ().__init__ ()
48-
47+
4948 self ._save_callback : Optional [Callable [[], None ]] = None
5049 self ._timer : Optional [QTimer ] = None
51-
50+
5251 # Hash tracking
5352 self ._last_code_hash = ""
5453 self ._last_graph_hash = ""
5554 self ._last_assets_hash = ""
5655 self ._last_keybindings_hash = ""
57-
56+
5857 # Compute functions (will be set by EfficientManimWindow)
5958 self ._compute_code_hash : Optional [Callable [[], str ]] = None
6059 self ._compute_graph_hash : Optional [Callable [[], str ]] = None
6160 self ._compute_assets_hash : Optional [Callable [[], str ]] = None
6261 self ._compute_keybindings_hash : Optional [Callable [[], str ]] = None
63-
62+
6463 self ._enabled = False
65-
64+
6665 if HAS_PYSIDE :
6766 self ._setup_timer ()
68-
67+
6968 def _setup_timer (self ) -> None :
7069 """Initialize the autosave timer."""
7170 self ._timer = QTimer ()
7271 self ._timer .setSingleShot (True ) # Debounce: only fire once per interval
7372 self ._timer .setInterval (AUTOSAVE_INTERVAL_MS )
7473 self ._timer .timeout .connect (self ._on_timer_timeout )
7574 LOGGER .debug (f"Autosave timer configured: { AUTOSAVE_INTERVAL_MS } ms interval" )
76-
75+
7776 def set_save_callback (self , callback : Callable [[], None ]) -> None :
7877 """Register the save function to call on autosave."""
7978 self ._save_callback = callback
8079 LOGGER .debug ("Autosave callback registered" )
81-
80+
8281 def set_hash_computers (
8382 self ,
8483 code_fn : Callable [[], str ],
@@ -92,56 +91,64 @@ def set_hash_computers(
9291 self ._compute_assets_hash = assets_fn
9392 self ._compute_keybindings_hash = keybindings_fn
9493 LOGGER .debug ("Hash computer functions registered" )
95-
94+
9695 def enable (self ) -> None :
9796 """Enable autosave."""
9897 self ._enabled = True
9998 LOGGER .info ("Autosave enabled" )
100-
99+
101100 def disable (self ) -> None :
102101 """Disable autosave."""
103102 self ._enabled = False
104103 if HAS_PYSIDE and self ._timer :
105104 self ._timer .stop ()
106105 LOGGER .info ("Autosave disabled" )
107-
106+
108107 def trigger_autosave (self , reason : str = "change detected" ) -> None :
109108 """
110109 Request autosave with debouncing.
111-
110+
112111 Won't save immediately — will wait for quiet period,
113112 then check if state actually changed before saving.
114113 """
115114 if not self ._enabled :
116115 return
117-
116+
118117 if HAS_PYSIDE and self ._timer :
119118 self ._timer .stop ()
120119 self ._timer .start () # Reset debounce timer
121120 LOGGER .debug (f"Autosave scheduled ({ reason } )" )
122-
121+
123122 def _on_timer_timeout (self ) -> None :
124123 """Timer fired — check if state changed, then save."""
125124 if not self ._enabled or not self ._save_callback :
126125 return
127-
126+
128127 try :
129128 # Compute current hashes
130129 code_hash = self ._compute_code_hash () if self ._compute_code_hash else ""
131130 graph_hash = self ._compute_graph_hash () if self ._compute_graph_hash else ""
132- assets_hash = self ._compute_assets_hash () if self ._compute_assets_hash else ""
133- keybindings_hash = self ._compute_keybindings_hash () if self ._compute_keybindings_hash else ""
134-
131+ assets_hash = (
132+ self ._compute_assets_hash () if self ._compute_assets_hash else ""
133+ )
134+ keybindings_hash = (
135+ self ._compute_keybindings_hash ()
136+ if self ._compute_keybindings_hash
137+ else ""
138+ )
139+
135140 # Check if anything changed
136- code_changed = (code_hash != self ._last_code_hash )
137- graph_changed = (graph_hash != self ._last_graph_hash )
138- assets_changed = (assets_hash != self ._last_assets_hash )
139- keybindings_changed = (keybindings_hash != self ._last_keybindings_hash )
140-
141- if not (code_changed or graph_changed or assets_changed or keybindings_changed ):
141+ code_changed = code_hash != self ._last_code_hash
142+ graph_changed = graph_hash != self ._last_graph_hash
143+ assets_changed = assets_hash != self ._last_assets_hash
144+ keybindings_changed = keybindings_hash != self ._last_keybindings_hash
145+
146+ if not (
147+ code_changed or graph_changed or assets_changed or keybindings_changed
148+ ):
142149 LOGGER .debug ("No changes detected, skipping autosave" )
143150 return
144-
151+
145152 # State changed — save now
146153 reason_parts = []
147154 if code_changed :
@@ -152,26 +159,26 @@ def _on_timer_timeout(self) -> None:
152159 reason_parts .append ("assets" )
153160 if keybindings_changed :
154161 reason_parts .append ("keybindings" )
155-
162+
156163 reason = f"autosave ({ ', ' .join (reason_parts )} )"
157-
164+
158165 # Update hashes for next check
159166 self ._last_code_hash = code_hash
160167 self ._last_graph_hash = graph_hash
161168 self ._last_assets_hash = assets_hash
162169 self ._last_keybindings_hash = keybindings_hash
163-
170+
164171 # Perform the save
165172 self ._save_callback ()
166-
173+
167174 if HAS_PYSIDE :
168175 self .autosave_triggered .emit (reason )
169-
176+
170177 LOGGER .info (f"✓ Autosave completed ({ ', ' .join (reason_parts )} )" )
171-
178+
172179 except Exception as e :
173180 LOGGER .error (f"Autosave failed: { e } " )
174-
181+
175182 def force_save (self ) -> None :
176183 """Force immediate save (bypass debounce)."""
177184 if HAS_PYSIDE and self ._timer :
0 commit comments