diff --git a/Cargo.lock b/Cargo.lock index b65445ae7ec..c6b6480de1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,6 +131,12 @@ dependencies = [ "utf8parse", ] +[[package]] +name = "anstyle-progress" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee65f3323c89eac2de51015f125682e9a88c3bf7a10d0c39b3768ff6af654ff7" + [[package]] name = "anstyle-query" version = "1.1.5" @@ -366,6 +372,7 @@ dependencies = [ "anstream 1.0.0", "anstyle", "anstyle-hyperlink", + "anstyle-progress", "anyhow", "base64", "blake3", diff --git a/Cargo.toml b/Cargo.toml index dd2053dd1ce..e408e8e39bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ annotate-snippets = { version = "0.12.12", features = ["simd"] } anstream = "1.0.0" anstyle = "1.0.13" anstyle-hyperlink = "1.0.1" +anstyle-progress = "0.1.0" anyhow = "1.0.102" base64 = "0.22.1" blake3 = "1.8.3" @@ -161,6 +162,7 @@ annotate-snippets.workspace = true anstream.workspace = true anstyle.workspace = true anstyle-hyperlink = { workspace = true, features = ["file"] } +anstyle-progress.workspace = true anyhow.workspace = true base64.workspace = true blake3.workspace = true diff --git a/src/cargo/core/shell.rs b/src/cargo/core/shell.rs index 8d7ed28878a..a99245835a6 100644 --- a/src/cargo/core/shell.rs +++ b/src/cargo/core/shell.rs @@ -612,65 +612,8 @@ fn supports_hyperlinks() -> bool { } /// Determines whether the terminal supports ANSI OSC 9;4. -#[expect( - clippy::disallowed_methods, - reason = "reading the state of the system, not config" -)] fn supports_term_integration(stream: &dyn IsTerminal) -> bool { - let windows_terminal = std::env::var("WT_SESSION").is_ok(); - let conemu = std::env::var("ConEmuANSI").ok() == Some("ON".into()); - let wezterm = std::env::var("TERM_PROGRAM").ok() == Some("WezTerm".into()); - let ghostty = std::env::var("TERM_PROGRAM").ok() == Some("ghostty".into()); - // iTerm added OSC 9;4 support in v3.6.6, which we can check for. - // For context: https://github.com/rust-lang/cargo/pull/16506#discussion_r2706584034 - let iterm = std::env::var("TERM_PROGRAM").ok() == Some("iTerm.app".into()) - && std::env::var("TERM_FEATURES") - .ok() - .is_some_and(|v| term_features_has_progress(&v)); - // Ptyxis added OSC 9;4 support in 48.0. - // See https://gitlab.gnome.org/chergert/ptyxis/-/issues/305 - let ptyxis = std::env::var("PTYXIS_VERSION") - .ok() - .and_then(|version| version.split(".").next()?.parse::().ok()) - .is_some_and(|major_version| major_version >= 48); - - (windows_terminal || conemu || wezterm || ghostty || iterm || ptyxis) && stream.is_terminal() -} - -// For iTerm, the TERM_FEATURES value "P" indicates OSC 9;4 support. -// Context: https://iterm2.com/feature-reporting/ -fn term_features_has_progress(value: &str) -> bool { - let mut current = String::new(); - - for ch in value.chars() { - if !ch.is_ascii_alphanumeric() { - break; - } - if ch.is_ascii_uppercase() { - if current == "P" { - return true; - } - current.clear(); - current.push(ch); - } else { - current.push(ch); - } - } - current == "P" -} - -#[cfg(test)] -mod tests { - use super::term_features_has_progress; - - #[test] - fn term_features_progress_detection() { - // With PROGRESS feature ("P") - assert!(term_features_has_progress("MBT2ScP")); - - // Without PROGRESS feature - assert!(!term_features_has_progress("MBT2Sc")); - } + anstyle_progress::supports_term_progress(stream.is_terminal()) } #[cfg(unix)] diff --git a/src/cargo/util/progress.rs b/src/cargo/util/progress.rs index c79f9656dbc..4a796a95f57 100644 --- a/src/cargo/util/progress.rs +++ b/src/cargo/util/progress.rs @@ -6,6 +6,7 @@ use std::time::{Duration, Instant}; use crate::core::shell::Verbosity; use crate::util::context::ProgressWhen; use crate::util::{CargoResult, GlobalContext}; +use anstyle_progress::TermProgress; use cargo_util::is_ci; use unicode_width::UnicodeWidthChar; @@ -92,11 +93,11 @@ enum StatusValue { /// Remove progress. Remove, /// Progress value (0-100). - Value(f64), + Value(u8), /// Indeterminate state (no bar, just animation) Indeterminate, /// Progress value in an error state (0-100). - Error(f64), + Error(u8), } enum ProgressOutput { @@ -136,7 +137,7 @@ impl TerminalIntegration { (true, false) => value, (true, true) => match value { StatusValue::Value(v) => StatusValue::Error(v), - _ => StatusValue::Error(100.0), + _ => StatusValue::Error(100), }, (false, _) => StatusValue::None, } @@ -146,7 +147,7 @@ impl TerminalIntegration { self.progress_state(StatusValue::Remove) } - pub fn value(&self, percent: f64) -> StatusValue { + pub fn value(&self, percent: u8) -> StatusValue { self.progress_state(StatusValue::Value(percent)) } @@ -161,21 +162,15 @@ impl TerminalIntegration { impl std::fmt::Display for StatusValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // From https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC - // ESC ] 9 ; 4 ; st ; pr ST - // When st is 0: remove progress. - // When st is 1: set progress value to pr (number, 0-100). - // When st is 2: set error state in taskbar, pr is optional. - // When st is 3: set indeterminate state, pr is ignored. - // When st is 4: set paused state, pr is optional. - let (state, progress) = match self { - Self::None => return Ok(()), // No output - Self::Remove => (0, 0.0), - Self::Value(v) => (1, *v), - Self::Indeterminate => (3, 0.0), - Self::Error(v) => (2, *v), + let progress = match self { + Self::None => TermProgress::none(), + Self::Remove => TermProgress::remove(), + Self::Value(v) => TermProgress::start().percent(*v), + Self::Indeterminate => TermProgress::start(), + Self::Error(v) => TermProgress::error().percent(*v), }; - write!(f, "\x1b]9;4;{state};{progress:.0}\x1b\\") + + progress.fmt(f) } } @@ -479,7 +474,9 @@ impl Format { }; let report = match self.style { ProgressStyle::Percentage | ProgressStyle::Ratio => { - self.term_integration.value(pct * 100.0) + let pct = (pct * 100.0) as u8; + let pct = pct.clamp(0, 100); + self.term_integration.value(pct) } ProgressStyle::Indeterminate => self.term_integration.indeterminate(), }; @@ -697,7 +694,7 @@ fn test_term_integration_disabled() { let report = TerminalIntegration::new(false); let mut out = String::new(); out.push_str(&report.remove().to_string()); - out.push_str(&report.value(10.0).to_string()); + out.push_str(&report.value(10).to_string()); out.push_str(&report.indeterminate().to_string()); assert!(out.is_empty()); } @@ -705,7 +702,7 @@ fn test_term_integration_disabled() { #[test] fn test_term_integration_error_state() { let mut report = TerminalIntegration::new(true); - assert_eq!(report.value(10.0), StatusValue::Value(10.0)); + assert_eq!(report.value(10), StatusValue::Value(10)); report.error(); - assert_eq!(report.value(50.0), StatusValue::Error(50.0)); + assert_eq!(report.value(50), StatusValue::Error(50)); }