feat: add taskbar progress reporting

This commit is contained in:
Alexander Sergeev 2024-05-12 00:58:56 +03:00
parent 4a8fd9b5df
commit a0624eaf53
5 changed files with 225 additions and 17 deletions

View File

@ -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;

View File

@ -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>,
}

View File

@ -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(

View File

@ -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));
}

View File

@ -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