Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/progress/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ impl ProgressJobBuilder {
operations_total: Mutex::new(None),
operation_index: Mutex::new(0),
operation_start: Mutex::new(Instant::now()),
last_text_output: Mutex::new(None),
}
}

Expand Down Expand Up @@ -205,6 +206,9 @@ pub struct ProgressJob {
pub(crate) operation_index: Mutex<usize>,
/// Start time of the current operation (for ETA calculation after next_operation)
pub(crate) operation_start: Mutex<Instant>,
/// Last rendered text-mode line. Used to suppress consecutive identical
/// emissions when multiple props are updated in quick succession.
pub(crate) last_text_output: Mutex<Option<String>>,
}

impl ProgressJob {
Expand Down Expand Up @@ -441,6 +445,14 @@ impl ProgressJob {
// Advance operation index after clearing progress values
*self.operation_index.lock().unwrap() += 1;

// Clear the text-mode dedup cache so the first render of the new
// operation always reaches the wire, even if the rendered template
// happens to be byte-identical to the last line of the previous
// operation (e.g. a body that only shows `message` and message
// hasn't changed yet). Without this, the state-change event would
// be silently swallowed in CI logs.
*self.last_text_output.lock().unwrap() = None;

self.update();
}

Expand Down
17 changes: 16 additions & 1 deletion src/progress/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,13 @@ pub fn refresh() -> Result<bool> {
}

/// Performs one refresh cycle without loop control.
///
/// In `ProgressOutput::Text` mode this is a no-op: text mode emits a fresh
/// line for each job update, so a full-frame redraw would only repeat content
/// already on the wire (and emit cursor-movement escape codes that look like
/// garbage in non-TTY logs such as CI).
pub fn refresh_once() -> Result<()> {
if is_disabled() || output() == ProgressOutput::Quiet {
if is_disabled() || matches!(output(), ProgressOutput::Quiet | ProgressOutput::Text) {
return Ok(());
}
let _refresh_guard = REFRESH_LOCK.lock().unwrap();
Expand Down Expand Up @@ -300,6 +305,16 @@ pub fn render_text_mode(job: &ProgressJob) -> Result<()> {
} else {
output
};
// Skip writing if this job's last text-mode line was identical. Callers
// often update several props in a row (e.g. `message` then `cur`); each
// call hits this path, but if the rendered line is unchanged there's no
// information to add — emitting it again just makes CI logs noisier.
let mut last = job.last_text_output.lock().unwrap();
if last.as_deref() == Some(final_output.as_str()) {
return Ok(());
}
*last = Some(final_output.clone());
drop(last);
let _guard = TERM_LOCK.lock().unwrap();
term().write_line(&final_output)?;
drop(_guard);
Expand Down