@@ -3,6 +3,7 @@ use std::time::{Duration, Instant};
33
44use globset:: { Glob , GlobMatcher } ;
55use rand:: RngExt ;
6+ use std:: collections:: VecDeque ;
67use unicode_width:: UnicodeWidthStr ;
78
89use crate :: git:: { CommitMetadata , DiffHunk , FileChange , FileStatus , LineChangeType } ;
@@ -61,6 +62,9 @@ const GIT_PUSH_PAUSE: f64 = 16.7; // After git push command
6162const PUSH_OUTPUT_PAUSE : f64 = 10.0 ; // Between push output lines
6263const PUSH_FINAL_PAUSE : f64 = 66.7 ; // After final push output
6364
65+ const MAX_LINE_CHECKPOINTS : usize = 200 ;
66+ const MAX_CHANGE_CHECKPOINTS : usize = 64 ;
67+
6468/// Represents the current state of the editor buffer
6569#[ derive( Debug , Clone ) ]
6670pub struct EditorBuffer {
@@ -214,6 +218,47 @@ pub enum ActivePane {
214218 Terminal ,
215219}
216220
221+ #[ derive( Debug , Clone , Copy , PartialEq ) ]
222+ pub enum StepMode {
223+ Line ,
224+ Change ,
225+ }
226+
227+ #[ derive( Clone ) ]
228+ struct ManualCheckpoint {
229+ step_index : usize ,
230+ buffer : EditorBuffer ,
231+ current_file_index : usize ,
232+ current_file_path : Option < String > ,
233+ terminal_lines : Vec < String > ,
234+ active_pane : ActivePane ,
235+ line_offset : isize ,
236+ dialog_title : Option < String > ,
237+ dialog_typing_text : String ,
238+ }
239+
240+ impl ManualCheckpoint {
241+ fn new ( engine : & AnimationEngine ) -> Self {
242+ Self {
243+ step_index : engine. current_step ,
244+ buffer : engine. buffer . clone ( ) ,
245+ current_file_index : engine. current_file_index ,
246+ current_file_path : engine. current_file_path . clone ( ) ,
247+ terminal_lines : engine. terminal_lines . clone ( ) ,
248+ active_pane : engine. active_pane . clone ( ) ,
249+ line_offset : engine. line_offset ,
250+ dialog_title : engine. dialog_title . clone ( ) ,
251+ dialog_typing_text : engine. dialog_typing_text . clone ( ) ,
252+ }
253+ }
254+ }
255+
256+ #[ derive( Clone , Copy , PartialEq ) ]
257+ enum CheckpointKind {
258+ Line ,
259+ Change ,
260+ }
261+
217262/// Main animation engine
218263pub struct AnimationEngine {
219264 pub buffer : EditorBuffer ,
@@ -253,6 +298,9 @@ pub struct AnimationEngine {
253298 pending_metadata : Option < CommitMetadata > ,
254299 /// Speed rules for different file patterns
255300 speed_rules : Vec < SpeedRule > ,
301+ paused : bool ,
302+ line_checkpoints : VecDeque < ManualCheckpoint > ,
303+ change_checkpoints : VecDeque < ManualCheckpoint > ,
256304}
257305
258306impl AnimationEngine {
@@ -289,9 +337,198 @@ impl AnimationEngine {
289337 current_metadata : None ,
290338 pending_metadata : None ,
291339 speed_rules : Vec :: new ( ) ,
340+ paused : false ,
341+ line_checkpoints : VecDeque :: new ( ) ,
342+ change_checkpoints : VecDeque :: new ( ) ,
292343 }
293344 }
294345
346+ /// Pause the animation playback.
347+ pub fn pause ( & mut self ) {
348+ self . paused = true ;
349+ }
350+
351+ /// Resume animation playback from the current position.
352+ pub fn resume ( & mut self ) {
353+ if self . paused {
354+ self . paused = false ;
355+ let now = Instant :: now ( ) ;
356+ self . last_update = now;
357+ self . last_frame = now;
358+ }
359+ }
360+
361+ /// Execute animation steps manually until the next boundary for the given mode.
362+ pub fn manual_step ( & mut self , mode : StepMode ) -> bool {
363+ if self . state != AnimationState :: Playing {
364+ return false ;
365+ }
366+
367+ if self . current_step >= self . steps . len ( ) {
368+ self . state = AnimationState :: Finished ;
369+ return false ;
370+ }
371+
372+ self . pause_until = None ;
373+ let mut executed = false ;
374+
375+ while self . current_step < self . steps . len ( ) {
376+ let step = self . steps [ self . current_step ] . clone ( ) ;
377+ self . execute_step ( step. clone ( ) ) ;
378+ self . current_step += 1 ;
379+ executed = true ;
380+
381+ if self . current_step >= self . steps . len ( ) {
382+ self . state = AnimationState :: Finished ;
383+ }
384+
385+ if Self :: is_boundary_step ( & step, mode) {
386+ break ;
387+ }
388+ }
389+
390+ if executed {
391+ let now = Instant :: now ( ) ;
392+ self . last_update = now;
393+ self . last_frame = now;
394+ }
395+
396+ executed
397+ }
398+
399+ pub fn restore_line_checkpoint ( & mut self ) -> bool {
400+ if self . line_checkpoints . len ( ) < 2 {
401+ return false ;
402+ }
403+ self . line_checkpoints . pop_back ( ) ;
404+ if let Some ( snapshot) = self . line_checkpoints . back ( ) . cloned ( ) {
405+ self . apply_checkpoint ( snapshot) ;
406+ true
407+ } else {
408+ false
409+ }
410+ }
411+
412+ pub fn restore_change_checkpoint ( & mut self ) -> bool {
413+ if self . change_checkpoints . len ( ) < 2 {
414+ return false ;
415+ }
416+ self . change_checkpoints . pop_back ( ) ;
417+ if let Some ( snapshot) = self . change_checkpoints . back ( ) . cloned ( ) {
418+ self . apply_checkpoint ( snapshot) ;
419+ true
420+ } else {
421+ false
422+ }
423+ }
424+
425+ fn apply_checkpoint ( & mut self , snapshot : ManualCheckpoint ) {
426+ self . current_step = snapshot. step_index ;
427+ self . buffer = snapshot. buffer ;
428+ self . current_file_index = snapshot. current_file_index ;
429+ self . current_file_path = snapshot. current_file_path ;
430+ self . terminal_lines = snapshot. terminal_lines ;
431+ self . active_pane = snapshot. active_pane ;
432+ self . line_offset = snapshot. line_offset ;
433+ self . dialog_title = snapshot. dialog_title ;
434+ self . dialog_typing_text = snapshot. dialog_typing_text ;
435+ self . pause_until = None ;
436+ self . paused = true ;
437+ self . state = AnimationState :: Playing ;
438+ }
439+
440+ fn is_boundary_step ( step : & AnimationStep , mode : StepMode ) -> bool {
441+ match mode {
442+ StepMode :: Line => matches ! (
443+ step,
444+ AnimationStep :: Pause { .. }
445+ | AnimationStep :: SwitchFile { .. }
446+ | AnimationStep :: TerminalPrompt
447+ | AnimationStep :: TerminalOutput { .. }
448+ | AnimationStep :: ResetState
449+ ) ,
450+ StepMode :: Change => match step {
451+ AnimationStep :: SwitchFile { .. }
452+ | AnimationStep :: TerminalPrompt
453+ | AnimationStep :: TerminalOutput { .. }
454+ | AnimationStep :: ResetState => true ,
455+ AnimationStep :: Pause { multiplier } => Self :: is_change_pause ( * multiplier) ,
456+ _ => false ,
457+ } ,
458+ }
459+ }
460+
461+ fn handle_step_checkpoint ( & mut self , step : & AnimationStep ) {
462+ match step {
463+ AnimationStep :: ResetState => {
464+ self . clear_checkpoints ( ) ;
465+ self . record_checkpoint ( CheckpointKind :: Change ) ;
466+ self . record_checkpoint ( CheckpointKind :: Line ) ;
467+ }
468+ AnimationStep :: SwitchFile { .. } => {
469+ self . line_checkpoints . clear ( ) ;
470+ self . record_checkpoint ( CheckpointKind :: Change ) ;
471+ self . record_checkpoint ( CheckpointKind :: Line ) ;
472+ }
473+ AnimationStep :: Pause { multiplier } => {
474+ if self . active_pane == ActivePane :: Editor {
475+ self . record_checkpoint ( CheckpointKind :: Line ) ;
476+ if Self :: is_change_pause ( * multiplier) {
477+ self . record_checkpoint ( CheckpointKind :: Change ) ;
478+ }
479+ }
480+ }
481+ _ => { }
482+ }
483+ }
484+
485+ fn is_change_pause ( multiplier : f64 ) -> bool {
486+ ( multiplier - HUNK_PAUSE ) . abs ( ) < f64:: EPSILON
487+ }
488+
489+ fn record_checkpoint ( & mut self , kind : CheckpointKind ) {
490+ if self . current_step == 0 {
491+ return ;
492+ }
493+
494+ let snapshot = ManualCheckpoint :: new ( self ) ;
495+ match kind {
496+ CheckpointKind :: Line => {
497+ if self
498+ . line_checkpoints
499+ . back ( )
500+ . map ( |c| c. step_index == snapshot. step_index )
501+ . unwrap_or ( false )
502+ {
503+ return ;
504+ }
505+ self . line_checkpoints . push_back ( snapshot) ;
506+ if self . line_checkpoints . len ( ) > MAX_LINE_CHECKPOINTS {
507+ self . line_checkpoints . pop_front ( ) ;
508+ }
509+ }
510+ CheckpointKind :: Change => {
511+ if self
512+ . change_checkpoints
513+ . back ( )
514+ . map ( |c| c. step_index == snapshot. step_index )
515+ . unwrap_or ( false )
516+ {
517+ return ;
518+ }
519+ self . change_checkpoints . push_back ( snapshot) ;
520+ if self . change_checkpoints . len ( ) > MAX_CHANGE_CHECKPOINTS {
521+ self . change_checkpoints . pop_front ( ) ;
522+ }
523+ }
524+ }
525+ }
526+
527+ fn clear_checkpoints ( & mut self ) {
528+ self . line_checkpoints . clear ( ) ;
529+ self . change_checkpoints . clear ( ) ;
530+ }
531+
295532 /// Set speed rules for file-specific typing speeds
296533 pub fn set_speed_rules ( & mut self , rules : Vec < SpeedRule > ) {
297534 self . speed_rules = rules;
@@ -621,6 +858,7 @@ impl AnimationEngine {
621858
622859 // Start with empty editor (no file opened yet)
623860 self . buffer = EditorBuffer :: new ( ) ;
861+ self . clear_checkpoints ( ) ;
624862 }
625863
626864 /// Generate animation steps for a file change
@@ -843,6 +1081,10 @@ impl AnimationEngine {
8431081 pub fn tick ( & mut self ) -> bool {
8441082 self . update_cursor_blink ( ) ;
8451083
1084+ if self . paused {
1085+ return true ;
1086+ }
1087+
8461088 if self . is_paused ( ) {
8471089 return true ;
8481090 }
@@ -923,6 +1165,7 @@ impl AnimationEngine {
9231165 }
9241166
9251167 fn execute_step ( & mut self , step : AnimationStep ) {
1168+ let step_clone = step. clone ( ) ;
9261169 // Calculate delay for next step with randomization for typing steps
9271170 let mut rng = rand:: rng ( ) ;
9281171 self . next_step_delay = match & step {
@@ -1072,6 +1315,8 @@ impl AnimationEngine {
10721315 }
10731316 }
10741317
1318+ self . handle_step_checkpoint ( & step_clone) ;
1319+
10751320 // Update scroll to keep cursor centered
10761321 self . update_scroll ( ) ;
10771322 }
0 commit comments