diff --git a/crates/pixi_progress/src/lib.rs b/crates/pixi_progress/src/lib.rs index 664bc43f20..2faf5d62d1 100644 --- a/crates/pixi_progress/src/lib.rs +++ b/crates/pixi_progress/src/lib.rs @@ -1,3 +1,4 @@ +pub mod osc; mod placement; pub mod style; diff --git a/crates/pixi_progress/src/osc.rs b/crates/pixi_progress/src/osc.rs new file mode 100644 index 0000000000..07109c77b1 --- /dev/null +++ b/crates/pixi_progress/src/osc.rs @@ -0,0 +1,46 @@ +use std::io::{IsTerminal, Write}; +use std::sync::LazyLock; + +use parking_lot::Mutex; + +/// Cached check for whether stdout is a terminal. +static IS_TERMINAL: LazyLock = LazyLock::new(|| std::io::stdout().is_terminal()); + +/// Last emitted percentage, used to avoid redundant writes. +static LAST_PCT: Mutex> = Mutex::new(None); + +/// Emit an OSC 9;4 progress sequence to stdout. +/// +/// Terminal emulators that support OSC 9;4 (Windows Terminal, iTerm2, +/// WezTerm, ConEmu, etc.) display this as progress in the title bar +/// or taskbar icon. +/// +/// Uses ST (ESC \) as the string terminator for broad compatibility. +pub fn set_progress(position: u64, length: u64) { + if !*IS_TERMINAL || length == 0 || crate::global_multi_progress().is_hidden() { + return; + } + let pct = (position * 100 / length).min(100) as u8; + let mut last = LAST_PCT.lock(); + if *last != Some(pct) { + *last = Some(pct); + let seq = format!("\x1b]9;4;1;{pct}\x1b\\"); + let mut stdout = std::io::stdout().lock(); + let _ = stdout.write_all(seq.as_bytes()); + let _ = stdout.flush(); + } +} + +/// Clear the OSC 9;4 progress indicator. +pub fn clear_progress() { + if !*IS_TERMINAL || crate::global_multi_progress().is_hidden() { + return; + } + let mut last = LAST_PCT.lock(); + if last.is_some() { + *last = None; + let mut stdout = std::io::stdout().lock(); + let _ = stdout.write_all(b"\x1b]9;4;0;0\x1b\\"); + let _ = stdout.flush(); + } +} diff --git a/crates/pixi_reporters/src/main_progress_bar.rs b/crates/pixi_reporters/src/main_progress_bar.rs index 2cab78e920..0a93b2c9f1 100644 --- a/crates/pixi_reporters/src/main_progress_bar.rs +++ b/crates/pixi_reporters/src/main_progress_bar.rs @@ -29,6 +29,9 @@ struct State { /// The items that are being tracked by this progress bar. tracker: Arc>>>, next_tracker_id: usize, + + /// Whether to emit OSC 9;4 terminal progress reporting. + osc_report: bool, } /// A trait for something that can be tracked by the [`MainProgressBar`]. @@ -72,10 +75,17 @@ impl MainProgressBar { title: Some(title), tracker: Arc::new(RwLock::new(HashMap::new())), next_tracker_id: 0, + osc_report: false, })), } } + /// Enable OSC 9;4 terminal progress reporting on this bar. + pub fn with_osc_report(self) -> Self { + self.inner.write().osc_report = true; + self + } + /// Called when an item is queued for processing. pub fn queued(&self, tracker: T) -> usize { let mut state = self.inner.write(); @@ -120,6 +130,9 @@ impl State { // Clear or update the progress bar. if is_empty { + if self.osc_report { + pixi_progress::osc::clear_progress(); + } // We cannot clear the progress bar and restart it later, so replacing it with a // new hidden one is currently the only option. self.title = Some(self.pb.prefix()); @@ -225,6 +238,10 @@ impl State { state.set_pos(position); }); self.pb.set_message(wide_msg); + + if self.osc_report { + pixi_progress::osc::set_progress(position, length); + } } } diff --git a/crates/pixi_reporters/src/sync_reporter.rs b/crates/pixi_reporters/src/sync_reporter.rs index da3c6f40f2..0f643603ff 100644 --- a/crates/pixi_reporters/src/sync_reporter.rs +++ b/crates/pixi_reporters/src/sync_reporter.rs @@ -256,7 +256,8 @@ impl CombinedInstallReporterInner { multi_progress.clone(), ProgressBarPlacement::After(preparing_progress_bar.progress_bar()), "installing".to_owned(), - ); + ) + .with_osc_report(); Self { next_id: std::sync::atomic::AtomicUsize::new(0),