From ee33c7593e8418463fdbaa3fa39a3df79b393e10 Mon Sep 17 00:00:00 2001 From: Shaan Majid Date: Thu, 22 Jan 2026 14:01:39 -0600 Subject: [PATCH 1/2] fix(windows): re-enable VT mode after subprocesses Some Windows hook installers (uv, pip, npm, cargo) disable ENABLE_VIRTUAL_TERMINAL_PROCESSING on exit, which causes indicatif's progress output to render as raw ANSI escape sequences. Re-enable VT mode after subprocess output() and status() calls to restore console state before prek resumes its own output. --- crates/prek/src/process.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/prek/src/process.rs b/crates/prek/src/process.rs index dd0eb9133..4291817b4 100644 --- a/crates/prek/src/process.rs +++ b/crates/prek/src/process.rs @@ -37,6 +37,13 @@ use tracing::trace; use crate::git::GIT; +#[cfg(windows)] +fn reenable_vt() { + if *crate::run::USE_COLOR { + let _ = anstyle_query::windows::enable_ansi_colors(); + } +} + /// An error from executing a Command #[derive(Debug, Error)] pub enum Error { @@ -176,6 +183,12 @@ impl Cmd { summary: self.summary.clone(), cause, })?; + + // Re-enable Windows VT mode in case subprocess corrupted console mode. + // Some tools disable ENABLE_VIRTUAL_TERMINAL_PROCESSING on exit. + #[cfg(windows)] + reenable_vt(); + self.maybe_check_output(&output)?; Ok(output) } @@ -283,6 +296,8 @@ impl Cmd { summary: self.summary.clone(), cause, })?; + #[cfg(windows)] + reenable_vt(); self.maybe_check_status(status)?; Ok(status) } From 47a215d3b13a7086f10315f7a2ada73bfd77c3a4 Mon Sep 17 00:00:00 2001 From: Shaan Majid Date: Wed, 28 Jan 2026 14:01:50 -0600 Subject: [PATCH 2/2] 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. --- crates/prek/src/cli/reporter.rs | 63 +++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/crates/prek/src/cli/reporter.rs b/crates/prek/src/cli/reporter.rs index 674ee32c9..f70fe20ca 100644 --- a/crates/prek/src/cli/reporter.rs +++ b/crates/prek/src/cli/reporter.rs @@ -11,6 +11,53 @@ use crate::hook::Hook; use crate::printer::Printer; use crate::workspace; +const SPINNER_TICK: Duration = Duration::from_millis(200); + +// Windows VT keep-alive to prevent ANSI corruption during subprocess execution. +// +// Some Windows tools (uv, pip, npm) disable ENABLE_VIRTUAL_TERMINAL_PROCESSING on exit, +// causing indicatif's spinner output to render as raw escape sequences. This background +// thread re-enables VT mode periodically while progress bars are active. +#[cfg(windows)] +mod vt_keepalive { + use std::sync::Arc; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::thread::{self, JoinHandle}; + + pub(super) struct VtKeepAlive { + stop: Arc, + handle: Option>, + } + + impl VtKeepAlive { + pub(super) fn new() -> Self { + let stop = Arc::new(AtomicBool::new(false)); + let stop_clone = stop.clone(); + + let handle = thread::spawn(move || { + while !stop_clone.load(Ordering::Relaxed) { + let _ = anstyle_query::windows::enable_ansi_colors(); + thread::sleep(super::SPINNER_TICK); + } + }); + + Self { + stop, + handle: Some(handle), + } + } + } + + impl Drop for VtKeepAlive { + fn drop(&mut self) { + self.stop.store(true, Ordering::Relaxed); + if let Some(handle) = self.handle.take() { + let _ = handle.join(); + } + } + } +} + /// Current progress reporter used to suspend rendering while printing normal output. static CURRENT_REPORTER: Mutex>> = Mutex::new(None); @@ -53,15 +100,27 @@ struct ProgressReporter { root: ProgressBar, state: Arc>, children: MultiProgress, + #[cfg(windows)] + _vt_keepalive: Option, } impl ProgressReporter { fn new(root: ProgressBar, children: MultiProgress, printer: Printer) -> Self { + // Only spawn the VT keep-alive when progress bars are visible and color is enabled. + #[cfg(windows)] + let vt_keepalive = if printer == Printer::Default && *crate::run::USE_COLOR { + Some(vt_keepalive::VtKeepAlive::new()) + } else { + None + }; + Self { printer, root, state: Arc::default(), children, + #[cfg(windows)] + _vt_keepalive: vt_keepalive, } } @@ -101,7 +160,7 @@ impl From for ProgressReporter { fn from(printer: Printer) -> Self { let multi = MultiProgress::with_draw_target(printer.target()); let root = multi.add(ProgressBar::with_draw_target(None, printer.target())); - root.enable_steady_tick(Duration::from_millis(200)); + root.enable_steady_tick(SPINNER_TICK); root.set_style( ProgressStyle::with_template("{spinner:.white} {msg:.dim}") .unwrap() @@ -206,7 +265,7 @@ impl HookRunReporter { ); let dots = self.dots.saturating_sub(hook.name.width()); - progress.enable_steady_tick(Duration::from_millis(200)); + progress.enable_steady_tick(SPINNER_TICK); progress.set_style( ProgressStyle::with_template(&format!("{{msg}}{{bar:{dots}.green/dim}}")) .unwrap()