diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs index c38fd6fd55af..a5439048711d 100644 --- a/src/cargo/util/context/mod.rs +++ b/src/cargo/util/context/mod.rs @@ -2713,6 +2713,7 @@ pub struct TermConfig { pub struct ProgressConfig { pub when: ProgressWhen, pub width: Option, + pub taskbar: Option, } #[derive(Debug, Default, Deserialize)] @@ -2745,10 +2746,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"])), @@ -2770,6 +2773,7 @@ where if let ProgressConfig { when: ProgressWhen::Always, width: None, + .. } = pc { return Err(serde::de::Error::custom( diff --git a/src/cargo/util/progress.rs b/src/cargo/util/progress.rs index 655fabcd4d98..4b8fd99d54c1 100644 --- a/src/cargo/util/progress.rs +++ b/src/cargo/util/progress.rs @@ -74,6 +74,98 @@ 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, +} + +/// 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, +} + +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, supported_terminal: bool) -> Self { + let show = match config { + Some(v) => v, + None => supported_terminal, + }; + + TaskbarProgress { show } + } + + 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 { + TaskbarValue::Value(percent) + } else { + TaskbarValue::None + } + } + + pub fn indeterminate(&self) -> TaskbarValue { + if self.show { + TaskbarValue::Indeterminate + } else { + TaskbarValue::None + } + } +} + +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::Indeterminate => (3, 0.0), + Self::Remove => (0, 0.0), + Self::Value(v) => (1, *v), + Self::None => return Ok(()), + }; + write!(f, "\x1b]9;4;{state};{progress:.0}\x1b\\") + } } impl<'gctx> Progress<'gctx> { @@ -126,6 +218,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, @@ -223,7 +319,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(()), } } @@ -269,6 +365,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(()); } @@ -279,22 +376,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(' '); @@ -305,7 +411,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); } @@ -314,6 +424,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(); @@ -331,7 +443,7 @@ impl<'gctx> State<'gctx> { } impl Format { - fn progress(&self, cur: usize, max: usize) -> Option { + fn progress(&self, cur: usize, max: usize) -> Option { assert!(cur <= max); // Render the percentage at the far right and then figure how long the // progress bar is @@ -342,8 +454,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; }; @@ -371,7 +491,7 @@ impl Format { string.push(']'); string.push_str(&stats); - Some(string) + Some(ProgressOutput::TextAndTaskbar(string, taskbar)) } fn render(&self, string: &mut String, msg: &str) { @@ -398,7 +518,11 @@ impl Format { #[cfg(test)] fn progress_status(&self, cur: usize, max: usize, msg: &str) -> Option { - 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) } @@ -420,6 +544,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, ""), @@ -493,6 +618,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, ""), @@ -518,6 +644,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, ""), @@ -528,6 +655,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); } diff --git a/src/doc/src/reference/config.md b/src/doc/src/reference/config.md index 965642cafc5b..571b8937b6ca 100644 --- a/src/doc/src/reference/config.md +++ b/src/doc/src/reference/config.md @@ -188,13 +188,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 @@ -1331,6 +1332,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