use std::{
collections::VecDeque,
fmt,
sync::{Arc, Mutex, PoisonError},
time::Duration,
};
pub const MAX_PROGRESS_LINE_BYTES: usize = 200;
pub const MAX_PROGRESS_EVENT_BYTES: usize = 512;
pub(crate) const PROGRESS_INTERVAL: Duration = Duration::from_millis(500);
#[derive(Clone, Default)]
pub struct ProgressSink {
pending: Option<Arc<Mutex<PendingProgress>>>,
}
impl fmt::Debug for ProgressSink {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ProgressSink")
.field("enabled", &self.is_enabled())
.finish()
}
}
impl ProgressSink {
pub fn buffered() -> Self {
Self {
pending: Some(Arc::default()),
}
}
pub fn is_enabled(&self) -> bool {
self.pending.is_some()
}
pub fn report(&self, text: &str) {
let Some(pending) = &self.pending else {
return;
};
let line = progress_line(text);
if !line.is_empty() {
pending
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(line);
}
}
pub fn take(&self) -> Option<String> {
self.pending
.as_ref()?
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take()
}
}
const PROGRESS_ELIDED: &str = "…";
#[derive(Debug, Default)]
struct PendingProgress {
lines: VecDeque<String>,
bytes: usize,
dropped: bool,
}
impl PendingProgress {
fn push(&mut self, line: String) {
self.bytes += line.len() + usize::from(!self.lines.is_empty());
self.lines.push_back(line);
let budget = MAX_PROGRESS_EVENT_BYTES - PROGRESS_ELIDED.len() - 1;
while self.bytes > budget && self.lines.len() > 1 {
if let Some(oldest) = self.lines.pop_front() {
self.bytes -= oldest.len() + 1;
self.dropped = true;
}
}
}
fn take(&mut self) -> Option<String> {
if self.lines.is_empty() {
return None;
}
let mut text = String::with_capacity(self.bytes + PROGRESS_ELIDED.len() + 1);
if std::mem::take(&mut self.dropped) {
text.push_str(PROGRESS_ELIDED);
text.push('\n');
}
for (index, line) in self.lines.drain(..).enumerate() {
if index > 0 {
text.push('\n');
}
text.push_str(&line);
}
self.bytes = 0;
Some(text)
}
}
fn progress_line(text: &str) -> String {
let mut line = String::new();
for word in text
.split(|character: char| character.is_whitespace() || character.is_control())
.filter(|word| !word.is_empty())
{
if !line.is_empty() {
line.push(' ');
}
line.push_str(word);
if line.len() > MAX_PROGRESS_LINE_BYTES {
break;
}
}
if line.len() <= MAX_PROGRESS_LINE_BYTES {
return line;
}
let mut end = MAX_PROGRESS_LINE_BYTES - PROGRESS_ELIDED.len();
while !line.is_char_boundary(end) {
end -= 1;
}
line.truncate(end);
line.push_str(PROGRESS_ELIDED);
line
}
#[cfg(test)]
mod tests;