@@ -16,6 +16,7 @@ use ratatui::widgets::ListState;
16
16
use ratatui:: widgets:: Padding ;
17
17
use ratatui:: widgets:: Paragraph ;
18
18
use std:: sync:: OnceLock ;
19
+ use std:: time:: Instant ;
19
20
20
21
use crate :: app:: App ;
21
22
use crate :: app:: AttemptView ;
@@ -229,7 +230,7 @@ fn draw_list(frame: &mut Frame, area: Rect, app: &mut App) {
229
230
230
231
// In-box spinner during initial/refresh loads
231
232
if app. refresh_inflight {
232
- draw_centered_spinner ( frame, inner, & mut app. throbber , "Loading tasks…" ) ;
233
+ draw_centered_spinner ( frame, inner, & mut app. spinner_start , "Loading tasks…" ) ;
233
234
}
234
235
}
235
236
@@ -291,7 +292,7 @@ fn draw_footer(frame: &mut Frame, area: Rect, app: &mut App) {
291
292
|| app. apply_preflight_inflight
292
293
|| app. apply_inflight
293
294
{
294
- draw_inline_spinner ( frame, top[ 1 ] , & mut app. throbber , "Loading…" ) ;
295
+ draw_inline_spinner ( frame, top[ 1 ] , & mut app. spinner_start , "Loading…" ) ;
295
296
} else {
296
297
frame. render_widget ( Clear , top[ 1 ] ) ;
297
298
}
@@ -449,7 +450,12 @@ fn draw_diff_overlay(frame: &mut Frame, area: Rect, app: &mut App) {
449
450
. map ( |o| o. sd . wrapped_lines ( ) . is_empty ( ) )
450
451
. unwrap_or ( true ) ;
451
452
if app. details_inflight && raw_empty {
452
- draw_centered_spinner ( frame, content_area, & mut app. throbber , "Loading details…" ) ;
453
+ draw_centered_spinner (
454
+ frame,
455
+ content_area,
456
+ & mut app. spinner_start ,
457
+ "Loading details…" ,
458
+ ) ;
453
459
} else {
454
460
let scroll = app
455
461
. diff_overlay
@@ -494,11 +500,11 @@ pub fn draw_apply_modal(frame: &mut Frame, area: Rect, app: &mut App) {
494
500
frame. render_widget ( header, rows[ 0 ] ) ;
495
501
// Body: spinner while preflight/apply runs; otherwise show result message and path lists
496
502
if app. apply_preflight_inflight {
497
- draw_centered_spinner ( frame, rows[ 1 ] , & mut app. throbber , "Checking…" ) ;
503
+ draw_centered_spinner ( frame, rows[ 1 ] , & mut app. spinner_start , "Checking…" ) ;
498
504
} else if app. apply_inflight {
499
- draw_centered_spinner ( frame, rows[ 1 ] , & mut app. throbber , "Applying…" ) ;
505
+ draw_centered_spinner ( frame, rows[ 1 ] , & mut app. spinner_start , "Applying…" ) ;
500
506
} else if m. result_message . is_none ( ) {
501
- draw_centered_spinner ( frame, rows[ 1 ] , & mut app. throbber , "Loading…" ) ;
507
+ draw_centered_spinner ( frame, rows[ 1 ] , & mut app. spinner_start , "Loading…" ) ;
502
508
} else if let Some ( msg) = & m. result_message {
503
509
let mut body_lines: Vec < Line > = Vec :: new ( ) ;
504
510
let first = match m. result_level {
@@ -859,29 +865,29 @@ fn format_relative_time(ts: chrono::DateTime<Utc>) -> String {
859
865
fn draw_inline_spinner (
860
866
frame : & mut Frame ,
861
867
area : Rect ,
862
- state : & mut throbber_widgets_tui :: ThrobberState ,
868
+ spinner_start : & mut Option < Instant > ,
863
869
label : & str ,
864
870
) {
865
- use ratatui:: style :: Style ;
866
- use throbber_widgets_tui :: BRAILLE_EIGHT ;
867
- use throbber_widgets_tui :: Throbber ;
868
- use throbber_widgets_tui :: WhichUse ;
869
- let w = Throbber :: default ( )
870
- . label ( label )
871
- . style ( Style :: default ( ) . cyan ( ) )
872
- . throbber_style ( Style :: default ( ) . magenta ( ) . bold ( ) )
873
- . throbber_set ( BRAILLE_EIGHT )
874
- . use_type ( WhichUse :: Spin ) ;
875
- frame. render_stateful_widget ( w , area, state ) ;
871
+ use ratatui:: widgets :: Paragraph ;
872
+ let start = spinner_start . get_or_insert_with ( Instant :: now ) ;
873
+ let blink_on = ( start . elapsed ( ) . as_millis ( ) / 600 ) . is_multiple_of ( 2 ) ;
874
+ let dot = if blink_on {
875
+ "• " . into ( )
876
+ } else {
877
+ "◦ " . dim ( )
878
+ } ;
879
+ let label = label . cyan ( ) ;
880
+ let line = Line :: from ( vec ! [ dot , label ] ) ;
881
+ frame. render_widget ( Paragraph :: new ( line ) , area) ;
876
882
}
877
883
878
884
fn draw_centered_spinner (
879
885
frame : & mut Frame ,
880
886
area : Rect ,
881
- state : & mut throbber_widgets_tui :: ThrobberState ,
887
+ spinner_start : & mut Option < Instant > ,
882
888
label : & str ,
883
889
) {
884
- // Center a 1xN throbber within the given rect
890
+ // Center a 1xN spinner within the given rect
885
891
let rows = Layout :: default ( )
886
892
. direction ( Direction :: Vertical )
887
893
. constraints ( [
@@ -898,7 +904,7 @@ fn draw_centered_spinner(
898
904
Constraint :: Percentage ( 50 ) ,
899
905
] )
900
906
. split ( rows[ 1 ] ) ;
901
- draw_inline_spinner ( frame, cols[ 1 ] , state , label) ;
907
+ draw_inline_spinner ( frame, cols[ 1 ] , spinner_start , label) ;
902
908
}
903
909
904
910
// Styling helpers for diff rendering live inline where used.
@@ -918,7 +924,12 @@ pub fn draw_env_modal(frame: &mut Frame, area: Rect, app: &mut App) {
918
924
let content = overlay_content ( inner) ;
919
925
920
926
if app. env_loading {
921
- draw_centered_spinner ( frame, content, & mut app. throbber , "Loading environments…" ) ;
927
+ draw_centered_spinner (
928
+ frame,
929
+ content,
930
+ & mut app. spinner_start ,
931
+ "Loading environments…" ,
932
+ ) ;
922
933
return ;
923
934
}
924
935
0 commit comments