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() 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) }