Skip to content

Commit 47a215d

Browse files
committed
fix(windows): keep VT mode enabled during progress bars
Subprocesses like uv/pip/npm can disable ENABLE_VIRTUAL_TERMINAL_PROCESSING while indicatif's spinner is actively rendering, causing raw ANSI escape sequences to appear mid-install. Add a Windows-only background thread that re-enables VT mode every 200ms (matching the spinner tick rate) while progress bars are visible. The thread is scoped to ProgressReporter lifetime and only spawns when color is enabled.
1 parent ee33c75 commit 47a215d

File tree

1 file changed

+61
-2
lines changed

1 file changed

+61
-2
lines changed

crates/prek/src/cli/reporter.rs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,53 @@ use crate::hook::Hook;
1111
use crate::printer::Printer;
1212
use 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.
1562
static 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

58107
impl 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

Comments
 (0)