mirror of
https://github.com/rust-lang/cargo.git
synced 2025-10-01 11:30:39 +00:00
feat: add taskbar progress reporting
This commit is contained in:
parent
4a8fd9b5df
commit
a0624eaf53
@ -854,7 +854,7 @@ impl<'gctx> DrainState<'gctx> {
|
||||
}
|
||||
|
||||
fn handle_error(
|
||||
&self,
|
||||
&mut self,
|
||||
shell: &mut Shell,
|
||||
err_state: &mut ErrorsDuringDrain,
|
||||
new_err: impl Into<ErrorToHandle>,
|
||||
@ -863,6 +863,7 @@ impl<'gctx> DrainState<'gctx> {
|
||||
if new_err.print_always || err_state.count == 0 {
|
||||
crate::display_error(&new_err.error, shell);
|
||||
if err_state.count == 0 && !self.active.is_empty() {
|
||||
self.progress.indicate_error();
|
||||
let _ = shell.warn("build failed, waiting for other jobs to finish...");
|
||||
}
|
||||
err_state.count += 1;
|
||||
|
@ -56,6 +56,7 @@ impl Shell {
|
||||
stderr_tty: std::io::stderr().is_terminal(),
|
||||
stdout_unicode: supports_unicode(&std::io::stdout()),
|
||||
stderr_unicode: supports_unicode(&std::io::stderr()),
|
||||
stderr_term_integration: supports_term_integration(&std::io::stderr()),
|
||||
},
|
||||
verbosity: Verbosity::Verbose,
|
||||
needs_clear: false,
|
||||
@ -122,6 +123,18 @@ impl Shell {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_err_term_integration_available(&self) -> bool {
|
||||
if let ShellOut::Stream {
|
||||
stderr_term_integration,
|
||||
..
|
||||
} = self.output
|
||||
{
|
||||
stderr_term_integration
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a reference to the underlying stdout writer.
|
||||
pub fn out(&mut self) -> &mut dyn Write {
|
||||
if self.needs_clear {
|
||||
@ -426,6 +439,7 @@ enum ShellOut {
|
||||
hyperlinks: bool,
|
||||
stdout_unicode: bool,
|
||||
stderr_unicode: bool,
|
||||
stderr_term_integration: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@ -575,6 +589,16 @@ fn supports_hyperlinks() -> bool {
|
||||
supports_hyperlinks::supports_hyperlinks()
|
||||
}
|
||||
|
||||
/// Determines whether the terminal supports ANSI OSC 9;4.
|
||||
#[allow(clippy::disallowed_methods)] // Read environment variables to detect terminal
|
||||
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());
|
||||
|
||||
(windows_terminal || conemu || wezterm) && stream.is_terminal()
|
||||
}
|
||||
|
||||
pub struct Hyperlink<D: fmt::Display> {
|
||||
url: Option<D>,
|
||||
}
|
||||
|
@ -2832,6 +2832,8 @@ pub struct TermConfig {
|
||||
pub struct ProgressConfig {
|
||||
pub when: ProgressWhen,
|
||||
pub width: Option<usize>,
|
||||
/// Communicate progress status with a terminal
|
||||
pub term_integration: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
@ -2864,10 +2866,12 @@ where
|
||||
"auto" => Ok(Some(ProgressConfig {
|
||||
when: ProgressWhen::Auto,
|
||||
width: None,
|
||||
term_integration: None,
|
||||
})),
|
||||
"never" => Ok(Some(ProgressConfig {
|
||||
when: ProgressWhen::Never,
|
||||
width: None,
|
||||
term_integration: None,
|
||||
})),
|
||||
"always" => Err(E::custom("\"always\" progress requires a `width` key")),
|
||||
_ => Err(E::unknown_variant(s, &["auto", "never"])),
|
||||
@ -2889,6 +2893,7 @@ where
|
||||
if let ProgressConfig {
|
||||
when: ProgressWhen::Always,
|
||||
width: None,
|
||||
..
|
||||
} = pc
|
||||
{
|
||||
return Err(serde::de::Error::custom(
|
||||
|
@ -74,6 +74,108 @@ struct Format {
|
||||
style: ProgressStyle,
|
||||
max_width: usize,
|
||||
max_print: usize,
|
||||
term_integration: TerminalIntegration,
|
||||
}
|
||||
|
||||
/// Controls terminal progress integration via OSC sequences.
|
||||
struct TerminalIntegration {
|
||||
enabled: bool,
|
||||
error: bool,
|
||||
}
|
||||
|
||||
/// A progress status value printable as an ANSI OSC 9;4 escape code.
|
||||
#[cfg_attr(test, derive(PartialEq, Debug))]
|
||||
enum StatusValue {
|
||||
/// No output.
|
||||
None,
|
||||
/// Remove progress.
|
||||
Remove,
|
||||
/// Progress value (0-100).
|
||||
Value(f64),
|
||||
/// Indeterminate state (no bar, just animation)
|
||||
Indeterminate,
|
||||
/// Progress value in an error state (0-100).
|
||||
Error(f64),
|
||||
}
|
||||
|
||||
enum ProgressOutput {
|
||||
/// Print progress without a message
|
||||
PrintNow,
|
||||
/// Progress, message and progress report
|
||||
TextAndReport(String, StatusValue),
|
||||
/// Only progress report, no message and no text progress
|
||||
Report(StatusValue),
|
||||
}
|
||||
|
||||
impl TerminalIntegration {
|
||||
#[cfg(test)]
|
||||
fn new(enabled: bool) -> Self {
|
||||
Self {
|
||||
enabled,
|
||||
error: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `TerminalIntegration` from Cargo's configuration.
|
||||
/// Autodetect support if not explicitly enabled or disabled.
|
||||
fn from_config(gctx: &GlobalContext) -> Self {
|
||||
let enabled = gctx
|
||||
.progress_config()
|
||||
.term_integration
|
||||
.unwrap_or_else(|| gctx.shell().is_err_term_integration_available());
|
||||
|
||||
Self {
|
||||
enabled,
|
||||
error: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn progress_state(&self, value: StatusValue) -> StatusValue {
|
||||
match (self.enabled, self.error) {
|
||||
(true, false) => value,
|
||||
(true, true) => match value {
|
||||
StatusValue::Value(v) => StatusValue::Error(v),
|
||||
_ => StatusValue::Error(100.0),
|
||||
},
|
||||
(false, _) => StatusValue::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&self) -> StatusValue {
|
||||
self.progress_state(StatusValue::Remove)
|
||||
}
|
||||
|
||||
pub fn value(&self, percent: f64) -> StatusValue {
|
||||
self.progress_state(StatusValue::Value(percent))
|
||||
}
|
||||
|
||||
pub fn indeterminate(&self) -> StatusValue {
|
||||
self.progress_state(StatusValue::Indeterminate)
|
||||
}
|
||||
|
||||
pub fn error(&mut self) {
|
||||
self.error = true;
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
write!(f, "\x1b]9;4;{state};{progress:.0}\x1b\\")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gctx> Progress<'gctx> {
|
||||
@ -126,6 +228,7 @@ impl<'gctx> Progress<'gctx> {
|
||||
// 50 gives some space for text after the progress bar,
|
||||
// even on narrow (e.g. 80 char) terminals.
|
||||
max_print: 50,
|
||||
term_integration: TerminalIntegration::from_config(gctx),
|
||||
},
|
||||
name: name.to_string(),
|
||||
done: false,
|
||||
@ -223,7 +326,7 @@ impl<'gctx> Progress<'gctx> {
|
||||
/// calling it too often.
|
||||
pub fn print_now(&mut self, msg: &str) -> CargoResult<()> {
|
||||
match &mut self.state {
|
||||
Some(s) => s.print("", msg),
|
||||
Some(s) => s.print(ProgressOutput::PrintNow, msg),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
@ -234,6 +337,13 @@ impl<'gctx> Progress<'gctx> {
|
||||
s.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the progress reporter to the error state.
|
||||
pub fn indicate_error(&mut self) {
|
||||
if let Some(s) = &mut self.state {
|
||||
s.format.term_integration.error()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Throttle {
|
||||
@ -269,6 +379,11 @@ impl Throttle {
|
||||
impl<'gctx> State<'gctx> {
|
||||
fn tick(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
|
||||
if self.done {
|
||||
write!(
|
||||
self.gctx.shell().err(),
|
||||
"{}",
|
||||
self.format.term_integration.remove()
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@ -280,21 +395,30 @@ impl<'gctx> State<'gctx> {
|
||||
// return back to the beginning of the line for the next print.
|
||||
self.try_update_max_width();
|
||||
if let Some(pbar) = self.format.progress(cur, max) {
|
||||
self.print(&pbar, msg)?;
|
||||
self.print(pbar, msg)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print(&mut self, prefix: &str, msg: &str) -> CargoResult<()> {
|
||||
fn print(&mut self, progress: ProgressOutput, msg: &str) -> CargoResult<()> {
|
||||
self.throttle.update();
|
||||
self.try_update_max_width();
|
||||
|
||||
let (mut line, report) = match progress {
|
||||
ProgressOutput::PrintNow => (String::new(), None),
|
||||
ProgressOutput::TextAndReport(prefix, report) => (prefix, Some(report)),
|
||||
ProgressOutput::Report(report) => (String::new(), Some(report)),
|
||||
};
|
||||
|
||||
// make sure we have enough room for the header
|
||||
if self.format.max_width < 15 {
|
||||
// even if we don't have space we can still output progress report
|
||||
if let Some(tb) = report {
|
||||
write!(self.gctx.shell().err(), "{tb}\r")?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut line = prefix.to_string();
|
||||
self.format.render(&mut line, msg);
|
||||
while line.len() < self.format.max_width - 15 {
|
||||
line.push(' ');
|
||||
@ -305,7 +429,11 @@ impl<'gctx> State<'gctx> {
|
||||
let mut shell = self.gctx.shell();
|
||||
shell.set_needs_clear(false);
|
||||
shell.status_header(&self.name)?;
|
||||
write!(shell.err(), "{}\r", line)?;
|
||||
if let Some(tb) = report {
|
||||
write!(shell.err(), "{line}{tb}\r")?;
|
||||
} else {
|
||||
write!(shell.err(), "{line}\r")?;
|
||||
}
|
||||
self.last_line = Some(line);
|
||||
shell.set_needs_clear(true);
|
||||
}
|
||||
@ -314,6 +442,12 @@ impl<'gctx> State<'gctx> {
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
// Always clear the progress report
|
||||
let _ = write!(
|
||||
self.gctx.shell().err(),
|
||||
"{}",
|
||||
self.format.term_integration.remove()
|
||||
);
|
||||
// No need to clear if the progress is not currently being displayed.
|
||||
if self.last_line.is_some() && !self.gctx.shell().is_cleared() {
|
||||
self.gctx.shell().err_erase_line();
|
||||
@ -331,7 +465,7 @@ impl<'gctx> State<'gctx> {
|
||||
}
|
||||
|
||||
impl Format {
|
||||
fn progress(&self, cur: usize, max: usize) -> Option<String> {
|
||||
fn progress(&self, cur: usize, max: usize) -> Option<ProgressOutput> {
|
||||
assert!(cur <= max);
|
||||
// Render the percentage at the far right and then figure how long the
|
||||
// progress bar is
|
||||
@ -339,11 +473,21 @@ impl Format {
|
||||
let pct = if !pct.is_finite() { 0.0 } else { pct };
|
||||
let stats = match self.style {
|
||||
ProgressStyle::Percentage => format!(" {:6.02}%", pct * 100.0),
|
||||
ProgressStyle::Ratio => format!(" {}/{}", cur, max),
|
||||
ProgressStyle::Ratio => format!(" {cur}/{max}"),
|
||||
ProgressStyle::Indeterminate => String::new(),
|
||||
};
|
||||
let report = match self.style {
|
||||
ProgressStyle::Percentage | ProgressStyle::Ratio => {
|
||||
self.term_integration.value(pct * 100.0)
|
||||
}
|
||||
ProgressStyle::Indeterminate => self.term_integration.indeterminate(),
|
||||
};
|
||||
|
||||
let extra_len = stats.len() + 2 /* [ and ] */ + 15 /* status header */;
|
||||
let Some(display_width) = self.width().checked_sub(extra_len) else {
|
||||
if self.term_integration.enabled {
|
||||
return Some(ProgressOutput::Report(report));
|
||||
}
|
||||
return None;
|
||||
};
|
||||
|
||||
@ -371,7 +515,7 @@ impl Format {
|
||||
string.push(']');
|
||||
string.push_str(&stats);
|
||||
|
||||
Some(string)
|
||||
Some(ProgressOutput::TextAndReport(string, report))
|
||||
}
|
||||
|
||||
fn render(&self, string: &mut String, msg: &str) {
|
||||
@ -398,7 +542,11 @@ impl Format {
|
||||
|
||||
#[cfg(test)]
|
||||
fn progress_status(&self, cur: usize, max: usize, msg: &str) -> Option<String> {
|
||||
let mut ret = self.progress(cur, max)?;
|
||||
let mut ret = match self.progress(cur, max)? {
|
||||
// Check only the variant that contains text.
|
||||
ProgressOutput::TextAndReport(text, _) => text,
|
||||
_ => return None,
|
||||
};
|
||||
self.render(&mut ret, msg);
|
||||
Some(ret)
|
||||
}
|
||||
@ -420,6 +568,7 @@ fn test_progress_status() {
|
||||
style: ProgressStyle::Ratio,
|
||||
max_print: 40,
|
||||
max_width: 60,
|
||||
term_integration: TerminalIntegration::new(false),
|
||||
};
|
||||
assert_eq!(
|
||||
format.progress_status(0, 4, ""),
|
||||
@ -493,6 +642,7 @@ fn test_progress_status_percentage() {
|
||||
style: ProgressStyle::Percentage,
|
||||
max_print: 40,
|
||||
max_width: 60,
|
||||
term_integration: TerminalIntegration::new(false),
|
||||
};
|
||||
assert_eq!(
|
||||
format.progress_status(0, 77, ""),
|
||||
@ -518,6 +668,7 @@ fn test_progress_status_too_short() {
|
||||
style: ProgressStyle::Percentage,
|
||||
max_print: 25,
|
||||
max_width: 25,
|
||||
term_integration: TerminalIntegration::new(false),
|
||||
};
|
||||
assert_eq!(
|
||||
format.progress_status(1, 1, ""),
|
||||
@ -528,6 +679,25 @@ fn test_progress_status_too_short() {
|
||||
style: ProgressStyle::Percentage,
|
||||
max_print: 24,
|
||||
max_width: 24,
|
||||
term_integration: TerminalIntegration::new(false),
|
||||
};
|
||||
assert_eq!(format.progress_status(1, 1, ""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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.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));
|
||||
report.error();
|
||||
assert_eq!(report.value(50.0), StatusValue::Error(50.0));
|
||||
}
|
||||
|
@ -191,13 +191,14 @@ metadata_key1 = "value"
|
||||
metadata_key2 = "value"
|
||||
|
||||
[term]
|
||||
quiet = false # whether cargo output is quiet
|
||||
verbose = false # whether cargo provides verbose output
|
||||
color = 'auto' # whether cargo colorizes output
|
||||
hyperlinks = true # whether cargo inserts links into output
|
||||
unicode = true # whether cargo can render output using non-ASCII unicode characters
|
||||
progress.when = 'auto' # whether cargo shows progress bar
|
||||
progress.width = 80 # width of progress bar
|
||||
quiet = false # whether cargo output is quiet
|
||||
verbose = false # whether cargo provides verbose output
|
||||
color = 'auto' # whether cargo colorizes output
|
||||
hyperlinks = true # whether cargo inserts links into output
|
||||
unicode = true # whether cargo can render output using non-ASCII unicode characters
|
||||
progress.when = 'auto' # whether cargo shows progress bar
|
||||
progress.width = 80 # width of progress bar
|
||||
progress.term-integration = true # whether cargo reports progress to terminal emulator
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
@ -1361,6 +1362,13 @@ Controls whether or not progress bar is shown in the terminal. Possible values:
|
||||
|
||||
Sets the width for progress bar.
|
||||
|
||||
#### `term.progress.term-integration`
|
||||
* Type: bool
|
||||
* Default: auto-detect
|
||||
* Environment: `CARGO_TERM_PROGRESS_TERM_INTEGRATION`
|
||||
|
||||
Report progress to the teminal emulator for display in places like the task bar.
|
||||
|
||||
[`cargo bench`]: ../commands/cargo-bench.md
|
||||
[`cargo login`]: ../commands/cargo-login.md
|
||||
[`cargo logout`]: ../commands/cargo-logout.md
|
||||
|
Loading…
x
Reference in New Issue
Block a user