@@ -11,6 +11,53 @@ use crate::hook::Hook;
1111use crate :: printer:: Printer ;
1212use crate :: workspace;
1313
14+ const SPINNER_TICK : Duration = Duration :: from_millis ( 200 ) ;
15+
16+ // Windows VT keep-alive to prevent ANSI corruption during subprocess execution.
17+ //
18+ // Some Windows tools (uv, pip, npm) disable ENABLE_VIRTUAL_TERMINAL_PROCESSING on exit,
19+ // causing indicatif's spinner output to render as raw escape sequences. This background
20+ // thread re-enables VT mode periodically while progress bars are active.
21+ #[ cfg( windows) ]
22+ mod vt_keepalive {
23+ use std:: sync:: Arc ;
24+ use std:: sync:: atomic:: { AtomicBool , Ordering } ;
25+ use std:: thread:: { self , JoinHandle } ;
26+
27+ pub ( super ) struct VtKeepAlive {
28+ stop : Arc < AtomicBool > ,
29+ handle : Option < JoinHandle < ( ) > > ,
30+ }
31+
32+ impl VtKeepAlive {
33+ pub ( super ) fn new ( ) -> Self {
34+ let stop = Arc :: new ( AtomicBool :: new ( false ) ) ;
35+ let stop_clone = stop. clone ( ) ;
36+
37+ let handle = thread:: spawn ( move || {
38+ while !stop_clone. load ( Ordering :: Relaxed ) {
39+ let _ = anstyle_query:: windows:: enable_ansi_colors ( ) ;
40+ thread:: sleep ( super :: SPINNER_TICK ) ;
41+ }
42+ } ) ;
43+
44+ Self {
45+ stop,
46+ handle : Some ( handle) ,
47+ }
48+ }
49+ }
50+
51+ impl Drop for VtKeepAlive {
52+ fn drop ( & mut self ) {
53+ self . stop . store ( true , Ordering :: Relaxed ) ;
54+ if let Some ( handle) = self . handle . take ( ) {
55+ let _ = handle. join ( ) ;
56+ }
57+ }
58+ }
59+ }
60+
1461/// Current progress reporter used to suspend rendering while printing normal output.
1562static CURRENT_REPORTER : Mutex < Option < Weak < ProgressReporter > > > = Mutex :: new ( None ) ;
1663
@@ -53,15 +100,27 @@ struct ProgressReporter {
53100 root : ProgressBar ,
54101 state : Arc < Mutex < BarState > > ,
55102 children : MultiProgress ,
103+ #[ cfg( windows) ]
104+ _vt_keepalive : Option < vt_keepalive:: VtKeepAlive > ,
56105}
57106
58107impl ProgressReporter {
59108 fn new ( root : ProgressBar , children : MultiProgress , printer : Printer ) -> Self {
109+ // Only spawn the VT keep-alive when progress bars are visible and color is enabled.
110+ #[ cfg( windows) ]
111+ let vt_keepalive = if printer == Printer :: Default && * crate :: run:: USE_COLOR {
112+ Some ( vt_keepalive:: VtKeepAlive :: new ( ) )
113+ } else {
114+ None
115+ } ;
116+
60117 Self {
61118 printer,
62119 root,
63120 state : Arc :: default ( ) ,
64121 children,
122+ #[ cfg( windows) ]
123+ _vt_keepalive : vt_keepalive,
65124 }
66125 }
67126
@@ -101,7 +160,7 @@ impl From<Printer> for ProgressReporter {
101160 fn from ( printer : Printer ) -> Self {
102161 let multi = MultiProgress :: with_draw_target ( printer. target ( ) ) ;
103162 let root = multi. add ( ProgressBar :: with_draw_target ( None , printer. target ( ) ) ) ;
104- root. enable_steady_tick ( Duration :: from_millis ( 200 ) ) ;
163+ root. enable_steady_tick ( SPINNER_TICK ) ;
105164 root. set_style (
106165 ProgressStyle :: with_template ( "{spinner:.white} {msg:.dim}" )
107166 . unwrap ( )
@@ -206,7 +265,7 @@ impl HookRunReporter {
206265 ) ;
207266
208267 let dots = self . dots . saturating_sub ( hook. name . width ( ) ) ;
209- progress. enable_steady_tick ( Duration :: from_millis ( 200 ) ) ;
268+ progress. enable_steady_tick ( SPINNER_TICK ) ;
210269 progress. set_style (
211270 ProgressStyle :: with_template ( & format ! ( "{{msg}}{{bar:{dots}.green/dim}}" ) )
212271 . unwrap ( )
0 commit comments