Skip to content

Commit

Permalink
feat: add taskbar progress reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
Gordon01 committed Nov 21, 2024
1 parent 5dbda8b commit 584ee00
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 17 deletions.
3 changes: 2 additions & 1 deletion src/cargo/core/compiler/job_queue/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand All @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/cargo/util/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2749,6 +2749,7 @@ pub struct TermConfig {
pub struct ProgressConfig {
pub when: ProgressWhen,
pub width: Option<usize>,
pub taskbar: Option<bool>,
}

#[derive(Debug, Default, Deserialize)]
Expand Down Expand Up @@ -2781,10 +2782,12 @@ where
"auto" => Ok(Some(ProgressConfig {
when: ProgressWhen::Auto,
width: None,
taskbar: None,
})),
"never" => Ok(Some(ProgressConfig {
when: ProgressWhen::Never,
width: None,
taskbar: None,
})),
"always" => Err(E::custom("\"always\" progress requires a `width` key")),
_ => Err(E::unknown_variant(s, &["auto", "never"])),
Expand All @@ -2806,6 +2809,7 @@ where
if let ProgressConfig {
when: ProgressWhen::Always,
width: None,
..
} = pc
{
return Err(serde::de::Error::custom(
Expand Down
167 changes: 158 additions & 9 deletions src/cargo/util/progress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,113 @@ struct Format {
style: ProgressStyle,
max_width: usize,
max_print: usize,
taskbar: TaskbarProgress,
}

/// Taskbar progressbar
///
/// Outputs ANSI codes according to the `Operating system commands`.
/// Currently only works in the Windows Terminal and ConEmu.
struct TaskbarProgress {
show: bool,
error: bool,
}

/// A taskbar progress value printable as ANSI OSC escape code
enum TaskbarValue {
/// Do not output anything
None,
/// Remove progress
Remove,
/// Progress value 0-100
Value(f64),
/// Indeterminate state (no bar, just animation)
Indeterminate,
Error,
}

enum ProgressOutput {
/// Print progress without a message
PrintNow,
/// Progress, message and taskbar progress
TextAndTaskbar(String, TaskbarValue),
/// Only taskbar progress, no message and no text progress
Taskbar(TaskbarValue),
}

impl TaskbarProgress {
/// Creates a new `TaskbarProgress` from a cargo's config system.
///
/// * `config == None` enables taskbar progress reporting on supported
/// terminal emulators (currently, Windows Terminal and ConEmu)
fn from_config(config: Option<bool>, supported_terminal: bool) -> Self {
let show = match config {
Some(v) => v,
None => supported_terminal,
};

TaskbarProgress { show, error: false }
}

fn is_supported_terminal(gctx: &GlobalContext) -> bool {
gctx.get_env("WT_SESSION").is_ok() || gctx.get_env("ConEmuANSI").ok() == Some("ON".into())
}

pub fn remove(&self) -> TaskbarValue {
if self.show {
TaskbarValue::Remove
} else {
TaskbarValue::None
}
}

pub fn value(&self, percent: f64) -> TaskbarValue {
if self.show {
if self.error {
TaskbarValue::Error
} else {
TaskbarValue::Value(percent)
}
} else {
TaskbarValue::None
}
}

pub fn indeterminate(&self) -> TaskbarValue {
if self.show {
if self.error {
TaskbarValue::Error
} else {
TaskbarValue::Indeterminate
}
} else {
TaskbarValue::None
}
}

pub fn error(&mut self) {
self.error = true;
}
}

impl std::fmt::Display for TaskbarValue {
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.
// When st is 4: set paused state, pr is optional.
let (state, progress) = match self {
Self::None => return Ok(()),
Self::Remove => (0, 0.0),
Self::Value(v) => (1, *v),
Self::Indeterminate => (3, 0.0),
Self::Error => (2, 0.0),
};
write!(f, "\x1b]9;4;{state};{progress:.0}\x1b\\")
}
}

impl<'gctx> Progress<'gctx> {
Expand Down Expand Up @@ -126,6 +233,10 @@ 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,
taskbar: TaskbarProgress::from_config(
progress_config.taskbar,
TaskbarProgress::is_supported_terminal(gctx),
),
},
name: name.to_string(),
done: false,
Expand Down Expand Up @@ -223,7 +334,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(()),
}
}
Expand All @@ -234,6 +345,12 @@ impl<'gctx> Progress<'gctx> {
s.clear();
}
}

pub fn indicate_error(&mut self) {
if let Some(s) = &mut self.state {
s.format.taskbar.error()
}
}
}

impl Throttle {
Expand Down Expand Up @@ -269,6 +386,7 @@ 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.taskbar.remove())?;
return Ok(());
}

Expand All @@ -279,22 +397,31 @@ impl<'gctx> State<'gctx> {
// Write out a pretty header, then the progress bar itself, and then
// 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)?;
if let Some(progress) = self.format.progress(cur, max) {
self.print(progress, 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, taskbar) = match progress {
ProgressOutput::PrintNow => (String::new(), None),
ProgressOutput::TextAndTaskbar(prefix, taskbar_value) => (prefix, Some(taskbar_value)),
ProgressOutput::Taskbar(taskbar_value) => (String::new(), Some(taskbar_value)),
};

// 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 taskbar progress
if let Some(tb) = taskbar {
write!(self.gctx.shell().err(), "{}\r", tb)?;
}
return Ok(());
}

let mut line = prefix.to_string();
self.format.render(&mut line, msg);
while line.len() < self.format.max_width - 15 {
line.push(' ');
Expand All @@ -305,7 +432,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) = taskbar {
write!(shell.err(), "{}{}\r", line, tb)?;
} else {
write!(shell.err(), "{}\r", line)?;
}
self.last_line = Some(line);
shell.set_needs_clear(true);
}
Expand All @@ -314,6 +445,8 @@ impl<'gctx> State<'gctx> {
}

fn clear(&mut self) {
// Always clear the taskbar progress
let _ = write!(self.gctx.shell().err(), "{}", self.format.taskbar.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();
Expand All @@ -331,7 +464,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
Expand All @@ -342,8 +475,16 @@ impl Format {
ProgressStyle::Ratio => format!(" {}/{}", cur, max),
ProgressStyle::Indeterminate => String::new(),
};
let taskbar = match self.style {
ProgressStyle::Percentage | ProgressStyle::Ratio => self.taskbar.value(pct * 100.0),
ProgressStyle::Indeterminate => self.taskbar.indeterminate(),
};

let extra_len = stats.len() + 2 /* [ and ] */ + 15 /* status header */;
let Some(display_width) = self.width().checked_sub(extra_len) else {
if self.taskbar.show {
return Some(ProgressOutput::Taskbar(taskbar));
}
return None;
};

Expand Down Expand Up @@ -371,7 +512,7 @@ impl Format {
string.push(']');
string.push_str(&stats);

Some(string)
Some(ProgressOutput::TextAndTaskbar(string, taskbar))
}

fn render(&self, string: &mut String, msg: &str) {
Expand All @@ -398,7 +539,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::TextAndTaskbar(text, _) => text,
_ => return None,
};
self.render(&mut ret, msg);
Some(ret)
}
Expand All @@ -420,6 +565,7 @@ fn test_progress_status() {
style: ProgressStyle::Ratio,
max_print: 40,
max_width: 60,
taskbar: TaskbarProgress::from_config(None, false),
};
assert_eq!(
format.progress_status(0, 4, ""),
Expand Down Expand Up @@ -493,6 +639,7 @@ fn test_progress_status_percentage() {
style: ProgressStyle::Percentage,
max_print: 40,
max_width: 60,
taskbar: TaskbarProgress::from_config(None, false),
};
assert_eq!(
format.progress_status(0, 77, ""),
Expand All @@ -518,6 +665,7 @@ fn test_progress_status_too_short() {
style: ProgressStyle::Percentage,
max_print: 25,
max_width: 25,
taskbar: TaskbarProgress::from_config(None, false),
};
assert_eq!(
format.progress_status(1, 1, ""),
Expand All @@ -528,6 +676,7 @@ fn test_progress_status_too_short() {
style: ProgressStyle::Percentage,
max_print: 24,
max_width: 24,
taskbar: TaskbarProgress::from_config(None, false),
};
assert_eq!(format.progress_status(1, 1, ""), None);
}
22 changes: 15 additions & 7 deletions src/doc/src/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.taskbar = true # whether cargo reports progress to terminal emulator
```

## Environment variables
Expand Down Expand Up @@ -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.taskbar`
* Type: bool
* Default: auto-detect
* Environment: `CARGO_TERM_PROGRESS_TASKBAR`

Report progess 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
Expand Down

0 comments on commit 584ee00

Please sign in to comment.