Skip to content

Commit 584ee00

Browse files
committed
feat: add taskbar progress reporting
1 parent 5dbda8b commit 584ee00

File tree

4 files changed

+179
-17
lines changed

4 files changed

+179
-17
lines changed

src/cargo/core/compiler/job_queue/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -854,7 +854,7 @@ impl<'gctx> DrainState<'gctx> {
854854
}
855855

856856
fn handle_error(
857-
&self,
857+
&mut self,
858858
shell: &mut Shell,
859859
err_state: &mut ErrorsDuringDrain,
860860
new_err: impl Into<ErrorToHandle>,
@@ -863,6 +863,7 @@ impl<'gctx> DrainState<'gctx> {
863863
if new_err.print_always || err_state.count == 0 {
864864
crate::display_error(&new_err.error, shell);
865865
if err_state.count == 0 && !self.active.is_empty() {
866+
self.progress.indicate_error();
866867
let _ = shell.warn("build failed, waiting for other jobs to finish...");
867868
}
868869
err_state.count += 1;

src/cargo/util/context/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2749,6 +2749,7 @@ pub struct TermConfig {
27492749
pub struct ProgressConfig {
27502750
pub when: ProgressWhen,
27512751
pub width: Option<usize>,
2752+
pub taskbar: Option<bool>,
27522753
}
27532754

27542755
#[derive(Debug, Default, Deserialize)]
@@ -2781,10 +2782,12 @@ where
27812782
"auto" => Ok(Some(ProgressConfig {
27822783
when: ProgressWhen::Auto,
27832784
width: None,
2785+
taskbar: None,
27842786
})),
27852787
"never" => Ok(Some(ProgressConfig {
27862788
when: ProgressWhen::Never,
27872789
width: None,
2790+
taskbar: None,
27882791
})),
27892792
"always" => Err(E::custom("\"always\" progress requires a `width` key")),
27902793
_ => Err(E::unknown_variant(s, &["auto", "never"])),
@@ -2806,6 +2809,7 @@ where
28062809
if let ProgressConfig {
28072810
when: ProgressWhen::Always,
28082811
width: None,
2812+
..
28092813
} = pc
28102814
{
28112815
return Err(serde::de::Error::custom(

src/cargo/util/progress.rs

Lines changed: 158 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,113 @@ struct Format {
7474
style: ProgressStyle,
7575
max_width: usize,
7676
max_print: usize,
77+
taskbar: TaskbarProgress,
78+
}
79+
80+
/// Taskbar progressbar
81+
///
82+
/// Outputs ANSI codes according to the `Operating system commands`.
83+
/// Currently only works in the Windows Terminal and ConEmu.
84+
struct TaskbarProgress {
85+
show: bool,
86+
error: bool,
87+
}
88+
89+
/// A taskbar progress value printable as ANSI OSC escape code
90+
enum TaskbarValue {
91+
/// Do not output anything
92+
None,
93+
/// Remove progress
94+
Remove,
95+
/// Progress value 0-100
96+
Value(f64),
97+
/// Indeterminate state (no bar, just animation)
98+
Indeterminate,
99+
Error,
100+
}
101+
102+
enum ProgressOutput {
103+
/// Print progress without a message
104+
PrintNow,
105+
/// Progress, message and taskbar progress
106+
TextAndTaskbar(String, TaskbarValue),
107+
/// Only taskbar progress, no message and no text progress
108+
Taskbar(TaskbarValue),
109+
}
110+
111+
impl TaskbarProgress {
112+
/// Creates a new `TaskbarProgress` from a cargo's config system.
113+
///
114+
/// * `config == None` enables taskbar progress reporting on supported
115+
/// terminal emulators (currently, Windows Terminal and ConEmu)
116+
fn from_config(config: Option<bool>, supported_terminal: bool) -> Self {
117+
let show = match config {
118+
Some(v) => v,
119+
None => supported_terminal,
120+
};
121+
122+
TaskbarProgress { show, error: false }
123+
}
124+
125+
fn is_supported_terminal(gctx: &GlobalContext) -> bool {
126+
gctx.get_env("WT_SESSION").is_ok() || gctx.get_env("ConEmuANSI").ok() == Some("ON".into())
127+
}
128+
129+
pub fn remove(&self) -> TaskbarValue {
130+
if self.show {
131+
TaskbarValue::Remove
132+
} else {
133+
TaskbarValue::None
134+
}
135+
}
136+
137+
pub fn value(&self, percent: f64) -> TaskbarValue {
138+
if self.show {
139+
if self.error {
140+
TaskbarValue::Error
141+
} else {
142+
TaskbarValue::Value(percent)
143+
}
144+
} else {
145+
TaskbarValue::None
146+
}
147+
}
148+
149+
pub fn indeterminate(&self) -> TaskbarValue {
150+
if self.show {
151+
if self.error {
152+
TaskbarValue::Error
153+
} else {
154+
TaskbarValue::Indeterminate
155+
}
156+
} else {
157+
TaskbarValue::None
158+
}
159+
}
160+
161+
pub fn error(&mut self) {
162+
self.error = true;
163+
}
164+
}
165+
166+
impl std::fmt::Display for TaskbarValue {
167+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168+
// From https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
169+
// ESC ] 9 ; 4 ; st ; pr ST
170+
// When st is 0: remove progress.
171+
// When st is 1: set progress value to pr (number, 0-100).
172+
// When st is 2: set error state in taskbar, pr is optional.
173+
// When st is 3: set indeterminate state.
174+
// When st is 4: set paused state, pr is optional.
175+
let (state, progress) = match self {
176+
Self::None => return Ok(()),
177+
Self::Remove => (0, 0.0),
178+
Self::Value(v) => (1, *v),
179+
Self::Indeterminate => (3, 0.0),
180+
Self::Error => (2, 0.0),
181+
};
182+
write!(f, "\x1b]9;4;{state};{progress:.0}\x1b\\")
183+
}
77184
}
78185

79186
impl<'gctx> Progress<'gctx> {
@@ -126,6 +233,10 @@ impl<'gctx> Progress<'gctx> {
126233
// 50 gives some space for text after the progress bar,
127234
// even on narrow (e.g. 80 char) terminals.
128235
max_print: 50,
236+
taskbar: TaskbarProgress::from_config(
237+
progress_config.taskbar,
238+
TaskbarProgress::is_supported_terminal(gctx),
239+
),
129240
},
130241
name: name.to_string(),
131242
done: false,
@@ -223,7 +334,7 @@ impl<'gctx> Progress<'gctx> {
223334
/// calling it too often.
224335
pub fn print_now(&mut self, msg: &str) -> CargoResult<()> {
225336
match &mut self.state {
226-
Some(s) => s.print("", msg),
337+
Some(s) => s.print(ProgressOutput::PrintNow, msg),
227338
None => Ok(()),
228339
}
229340
}
@@ -234,6 +345,12 @@ impl<'gctx> Progress<'gctx> {
234345
s.clear();
235346
}
236347
}
348+
349+
pub fn indicate_error(&mut self) {
350+
if let Some(s) = &mut self.state {
351+
s.format.taskbar.error()
352+
}
353+
}
237354
}
238355

239356
impl Throttle {
@@ -269,6 +386,7 @@ impl Throttle {
269386
impl<'gctx> State<'gctx> {
270387
fn tick(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
271388
if self.done {
389+
write!(self.gctx.shell().err(), "{}", self.format.taskbar.remove())?;
272390
return Ok(());
273391
}
274392

@@ -279,22 +397,31 @@ impl<'gctx> State<'gctx> {
279397
// Write out a pretty header, then the progress bar itself, and then
280398
// return back to the beginning of the line for the next print.
281399
self.try_update_max_width();
282-
if let Some(pbar) = self.format.progress(cur, max) {
283-
self.print(&pbar, msg)?;
400+
if let Some(progress) = self.format.progress(cur, max) {
401+
self.print(progress, msg)?;
284402
}
285403
Ok(())
286404
}
287405

288-
fn print(&mut self, prefix: &str, msg: &str) -> CargoResult<()> {
406+
fn print(&mut self, progress: ProgressOutput, msg: &str) -> CargoResult<()> {
289407
self.throttle.update();
290408
self.try_update_max_width();
291409

410+
let (mut line, taskbar) = match progress {
411+
ProgressOutput::PrintNow => (String::new(), None),
412+
ProgressOutput::TextAndTaskbar(prefix, taskbar_value) => (prefix, Some(taskbar_value)),
413+
ProgressOutput::Taskbar(taskbar_value) => (String::new(), Some(taskbar_value)),
414+
};
415+
292416
// make sure we have enough room for the header
293417
if self.format.max_width < 15 {
418+
// even if we don't have space we can still output taskbar progress
419+
if let Some(tb) = taskbar {
420+
write!(self.gctx.shell().err(), "{}\r", tb)?;
421+
}
294422
return Ok(());
295423
}
296424

297-
let mut line = prefix.to_string();
298425
self.format.render(&mut line, msg);
299426
while line.len() < self.format.max_width - 15 {
300427
line.push(' ');
@@ -305,7 +432,11 @@ impl<'gctx> State<'gctx> {
305432
let mut shell = self.gctx.shell();
306433
shell.set_needs_clear(false);
307434
shell.status_header(&self.name)?;
308-
write!(shell.err(), "{}\r", line)?;
435+
if let Some(tb) = taskbar {
436+
write!(shell.err(), "{}{}\r", line, tb)?;
437+
} else {
438+
write!(shell.err(), "{}\r", line)?;
439+
}
309440
self.last_line = Some(line);
310441
shell.set_needs_clear(true);
311442
}
@@ -314,6 +445,8 @@ impl<'gctx> State<'gctx> {
314445
}
315446

316447
fn clear(&mut self) {
448+
// Always clear the taskbar progress
449+
let _ = write!(self.gctx.shell().err(), "{}", self.format.taskbar.remove());
317450
// No need to clear if the progress is not currently being displayed.
318451
if self.last_line.is_some() && !self.gctx.shell().is_cleared() {
319452
self.gctx.shell().err_erase_line();
@@ -331,7 +464,7 @@ impl<'gctx> State<'gctx> {
331464
}
332465

333466
impl Format {
334-
fn progress(&self, cur: usize, max: usize) -> Option<String> {
467+
fn progress(&self, cur: usize, max: usize) -> Option<ProgressOutput> {
335468
assert!(cur <= max);
336469
// Render the percentage at the far right and then figure how long the
337470
// progress bar is
@@ -342,8 +475,16 @@ impl Format {
342475
ProgressStyle::Ratio => format!(" {}/{}", cur, max),
343476
ProgressStyle::Indeterminate => String::new(),
344477
};
478+
let taskbar = match self.style {
479+
ProgressStyle::Percentage | ProgressStyle::Ratio => self.taskbar.value(pct * 100.0),
480+
ProgressStyle::Indeterminate => self.taskbar.indeterminate(),
481+
};
482+
345483
let extra_len = stats.len() + 2 /* [ and ] */ + 15 /* status header */;
346484
let Some(display_width) = self.width().checked_sub(extra_len) else {
485+
if self.taskbar.show {
486+
return Some(ProgressOutput::Taskbar(taskbar));
487+
}
347488
return None;
348489
};
349490

@@ -371,7 +512,7 @@ impl Format {
371512
string.push(']');
372513
string.push_str(&stats);
373514

374-
Some(string)
515+
Some(ProgressOutput::TextAndTaskbar(string, taskbar))
375516
}
376517

377518
fn render(&self, string: &mut String, msg: &str) {
@@ -398,7 +539,11 @@ impl Format {
398539

399540
#[cfg(test)]
400541
fn progress_status(&self, cur: usize, max: usize, msg: &str) -> Option<String> {
401-
let mut ret = self.progress(cur, max)?;
542+
let mut ret = match self.progress(cur, max)? {
543+
// Check only the variant that contains text
544+
ProgressOutput::TextAndTaskbar(text, _) => text,
545+
_ => return None,
546+
};
402547
self.render(&mut ret, msg);
403548
Some(ret)
404549
}
@@ -420,6 +565,7 @@ fn test_progress_status() {
420565
style: ProgressStyle::Ratio,
421566
max_print: 40,
422567
max_width: 60,
568+
taskbar: TaskbarProgress::from_config(None, false),
423569
};
424570
assert_eq!(
425571
format.progress_status(0, 4, ""),
@@ -493,6 +639,7 @@ fn test_progress_status_percentage() {
493639
style: ProgressStyle::Percentage,
494640
max_print: 40,
495641
max_width: 60,
642+
taskbar: TaskbarProgress::from_config(None, false),
496643
};
497644
assert_eq!(
498645
format.progress_status(0, 77, ""),
@@ -518,6 +665,7 @@ fn test_progress_status_too_short() {
518665
style: ProgressStyle::Percentage,
519666
max_print: 25,
520667
max_width: 25,
668+
taskbar: TaskbarProgress::from_config(None, false),
521669
};
522670
assert_eq!(
523671
format.progress_status(1, 1, ""),
@@ -528,6 +676,7 @@ fn test_progress_status_too_short() {
528676
style: ProgressStyle::Percentage,
529677
max_print: 24,
530678
max_width: 24,
679+
taskbar: TaskbarProgress::from_config(None, false),
531680
};
532681
assert_eq!(format.progress_status(1, 1, ""), None);
533682
}

src/doc/src/reference/config.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,13 +191,14 @@ metadata_key1 = "value"
191191
metadata_key2 = "value"
192192

193193
[term]
194-
quiet = false # whether cargo output is quiet
195-
verbose = false # whether cargo provides verbose output
196-
color = 'auto' # whether cargo colorizes output
197-
hyperlinks = true # whether cargo inserts links into output
198-
unicode = true # whether cargo can render output using non-ASCII unicode characters
199-
progress.when = 'auto' # whether cargo shows progress bar
200-
progress.width = 80 # width of progress bar
194+
quiet = false # whether cargo output is quiet
195+
verbose = false # whether cargo provides verbose output
196+
color = 'auto' # whether cargo colorizes output
197+
hyperlinks = true # whether cargo inserts links into output
198+
unicode = true # whether cargo can render output using non-ASCII unicode characters
199+
progress.when = 'auto' # whether cargo shows progress bar
200+
progress.width = 80 # width of progress bar
201+
progress.taskbar = true # whether cargo reports progress to terminal emulator
201202
```
202203

203204
## Environment variables
@@ -1361,6 +1362,13 @@ Controls whether or not progress bar is shown in the terminal. Possible values:
13611362

13621363
Sets the width for progress bar.
13631364

1365+
#### `term.progress.taskbar`
1366+
* Type: bool
1367+
* Default: auto-detect
1368+
* Environment: `CARGO_TERM_PROGRESS_TASKBAR`
1369+
1370+
Report progess to the teminal emulator for display in places like the task bar.
1371+
13641372
[`cargo bench`]: ../commands/cargo-bench.md
13651373
[`cargo login`]: ../commands/cargo-login.md
13661374
[`cargo logout`]: ../commands/cargo-logout.md

0 commit comments

Comments
 (0)