Skip to content
Closed
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
2 changes: 2 additions & 0 deletions crates/pixi_progress/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod placement;
mod rolling_log_display;
pub mod style;

use std::{
Expand All @@ -12,6 +13,7 @@ use std::{
use indicatif::{HumanBytes, MultiProgress, ProgressBar, ProgressDrawTarget, ProgressState};
use parking_lot::Mutex;
pub use placement::ProgressBarPlacement;
pub use rolling_log_display::RollingLogDisplay;

/// A helper macro to print a message to the console. If a multi-progress bar
/// is currently active, this macro will suspend the progress bar, print the
Expand Down
201 changes: 201 additions & 0 deletions crates/pixi_progress/src/rolling_log_display.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};

use crate::ProgressBarPlacement;

/// A display that shows a rolling window of the last N lines of output.
///
/// This is useful for streaming output (like build logs) where you want to show
/// recent activity without cluttering the terminal. Each line is displayed as a
/// separate progress bar using `{wide_msg}` to prevent wrapping.
pub struct RollingLogDisplay {
/// Progress bars for visible lines (up to max_lines)
lines: Vec<ProgressBar>,
/// Full log buffer (for failure case)
full_log: Vec<String>,
/// MultiProgress reference
multi_progress: MultiProgress,
/// Placement strategy for the progress bars
placement: ProgressBarPlacement,
/// Maximum visible lines
max_lines: usize,
}

impl RollingLogDisplay {
/// Create a new rolling log display with default max lines (6).
///
/// # Arguments
/// * `multi_progress` - The MultiProgress instance to add progress bars to
/// * `placement` - Where to place the first progress bar
pub fn new(multi_progress: MultiProgress, placement: ProgressBarPlacement) -> Self {
Self::with_max_lines(multi_progress, placement, 6)
}

/// Create a new rolling log display with custom max lines.
///
/// # Arguments
/// * `multi_progress` - The MultiProgress instance to add progress bars to
/// * `placement` - Where to place the first progress bar
/// * `max_lines` - Maximum number of lines to display at once
pub fn with_max_lines(
multi_progress: MultiProgress,
placement: ProgressBarPlacement,
max_lines: usize,
) -> Self {
Self {
lines: Vec::new(),
full_log: Vec::new(),
multi_progress,
placement,
max_lines,
}
}

/// Push a new line to the display.
///
/// If the display is not yet at capacity, a new progress bar is created.
/// If at capacity, the oldest progress bar is updated and moved to the end.
pub fn push_line(&mut self, line: impl Into<String>) {
let line = line.into();

// Always buffer the full log
self.full_log.push(line.clone());

// If we haven't reached max capacity, create a new progress bar
if self.lines.len() < self.max_lines {
// Create as hidden first
let pb = ProgressBar::hidden();

// Add to MultiProgress using placement strategy
let pb = if self.lines.is_empty() {
self.placement.insert(self.multi_progress.clone(), pb)
} else {
let last_bar = self.lines.last().unwrap();
ProgressBarPlacement::After(last_bar.clone())
.insert(self.multi_progress.clone(), pb)
};

// Now configure the style and message (dimmed)
pb.set_style(
ProgressStyle::with_template("{wide_msg:.dim}")
.expect("failed to set progress bar template"),
);
pb.set_message(line);

self.lines.push(pb);
} else {
// At capacity - update all bars to show the last N lines in correct order
// Get the last max_lines from the full log
let start_idx = self.full_log.len().saturating_sub(self.max_lines);
for (i, pb) in self.lines.iter().enumerate() {
if let Some(msg) = self.full_log.get(start_idx + i) {
pb.set_message(msg.clone());
}
}
}
}

/// Get a reference to all buffered log lines.
///
/// This returns the complete log history, useful for displaying
/// full output on failure.
pub fn full_log(&self) -> &[String] {
&self.full_log
}

/// Consume self and return the full log buffer without copying.
///
/// This also finishes and clears all progress bars.
pub fn into_full_log(self) -> Vec<String> {
for pb in self.lines {
pb.finish_and_clear();
}
self.full_log
}

/// Clear the display and remove all progress bars, consuming self.
///
/// This finishes and clears all progress bars, removing them from the display.
pub fn finish(self) {
for pb in self.lines {
pb.finish_and_clear();
}
}

/// Clear the progress bars from display but keep the log buffer.
///
/// This removes all progress bars from the terminal but retains the full log
/// in memory for later retrieval.
pub fn clear_display(&mut self) {
for pb in self.lines.drain(..) {
pb.finish_and_clear();
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_basic_push() {
let mp = MultiProgress::new();
let mut display = RollingLogDisplay::new(mp, ProgressBarPlacement::Bottom);

display.push_line("line 1");
display.push_line("line 2");
display.push_line("line 3");

assert_eq!(display.full_log().len(), 3);
assert_eq!(display.lines.len(), 3);
assert_eq!(display.full_log(), &["line 1", "line 2", "line 3"]);
}

#[test]
fn test_max_lines() {
let mp = MultiProgress::new();
let mut display = RollingLogDisplay::with_max_lines(mp, ProgressBarPlacement::Bottom, 3);

display.push_line("line 1");
display.push_line("line 2");
display.push_line("line 3");

// At capacity
assert_eq!(display.lines.len(), 3);

display.push_line("line 4");

// Still at capacity (3 bars), but full log has all 4
assert_eq!(display.lines.len(), 3);
assert_eq!(display.full_log().len(), 4);
}

#[test]
fn test_clear_display() {
let mp = MultiProgress::new();
let mut display = RollingLogDisplay::new(mp, ProgressBarPlacement::Bottom);

display.push_line("line 1");
display.push_line("line 2");

assert_eq!(display.lines.len(), 2);

display.clear_display();

assert_eq!(display.lines.len(), 0);
assert_eq!(display.full_log().len(), 2); // Log still preserved
}

#[test]
fn test_finish() {
let mp = MultiProgress::new();
let mut display = RollingLogDisplay::new(mp, ProgressBarPlacement::Bottom);

display.push_line("line 1");
display.push_line("line 2");

let full_log_len = display.full_log().len();
display.finish();

assert_eq!(full_log_len, 2);
}
}
Loading
Loading