Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
59 changes: 1 addition & 58 deletions src/cargo/core/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<i32>().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)]
Expand Down
41 changes: 19 additions & 22 deletions src/cargo/util/progress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -92,11 +93,11 @@ enum StatusValue {
/// Remove progress.
Remove,
/// Progress value (0-100).
Value(f64),
Value(u8),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the spec only for integer? I guess it depends on how the terminal emulator implements it.

(not a blocker because we already round it)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, there isn't as thorough of a spec for this one. In fact, some terminals have a Paused state while others call that value Warning. I've seen one library claim that some support a label but I've not seen evidence of that yet.

From https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC

set progress value to pr (number, 0-100).

I take that to mean integer.

https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences also only gives integral examples

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I saw those references and it seems to be integers. Anyway, Cargo does need floats now so we're good. If anstyle-progress starts supporting other things, it is not in Cargo's hand anymore.

/// Indeterminate state (no bar, just animation)
Indeterminate,
/// Progress value in an error state (0-100).
Error(f64),
Error(u8),
}

enum ProgressOutput {
Expand Down Expand Up @@ -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,
}
Expand All @@ -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))
}

Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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(),
};
Expand Down Expand Up @@ -697,15 +694,15 @@ 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());
}

#[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));
}