1use std::{
4 collections::VecDeque,
5 fmt,
6 sync::{Arc, Mutex, PoisonError},
7 time::Duration,
8};
9
10pub const MAX_PROGRESS_LINE_BYTES: usize = 200;
12pub const MAX_PROGRESS_EVENT_BYTES: usize = 512;
15pub(crate) const PROGRESS_INTERVAL: Duration = Duration::from_millis(500);
17
18#[derive(Clone, Default)]
24pub struct ProgressSink {
25 pending: Option<Arc<Mutex<PendingProgress>>>,
26}
27
28impl fmt::Debug for ProgressSink {
29 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30 formatter
31 .debug_struct("ProgressSink")
32 .field("enabled", &self.is_enabled())
33 .finish()
34 }
35}
36
37impl ProgressSink {
38 pub fn buffered() -> Self {
40 Self {
41 pending: Some(Arc::default()),
42 }
43 }
44
45 pub fn is_enabled(&self) -> bool {
46 self.pending.is_some()
47 }
48
49 pub fn report(&self, text: &str) {
52 let Some(pending) = &self.pending else {
53 return;
54 };
55 let line = progress_line(text);
56 if !line.is_empty() {
57 pending
58 .lock()
59 .unwrap_or_else(PoisonError::into_inner)
60 .push(line);
61 }
62 }
63
64 pub fn take(&self) -> Option<String> {
67 self.pending
68 .as_ref()?
69 .lock()
70 .unwrap_or_else(PoisonError::into_inner)
71 .take()
72 }
73}
74
75const PROGRESS_ELIDED: &str = "…";
77
78#[derive(Debug, Default)]
79struct PendingProgress {
80 lines: VecDeque<String>,
81 bytes: usize,
83 dropped: bool,
84}
85
86impl PendingProgress {
87 fn push(&mut self, line: String) {
88 self.bytes += line.len() + usize::from(!self.lines.is_empty());
89 self.lines.push_back(line);
90 let budget = MAX_PROGRESS_EVENT_BYTES - PROGRESS_ELIDED.len() - 1;
92 while self.bytes > budget && self.lines.len() > 1 {
93 if let Some(oldest) = self.lines.pop_front() {
94 self.bytes -= oldest.len() + 1;
95 self.dropped = true;
96 }
97 }
98 }
99
100 fn take(&mut self) -> Option<String> {
101 if self.lines.is_empty() {
102 return None;
103 }
104 let mut text = String::with_capacity(self.bytes + PROGRESS_ELIDED.len() + 1);
105 if std::mem::take(&mut self.dropped) {
106 text.push_str(PROGRESS_ELIDED);
107 text.push('\n');
108 }
109 for (index, line) in self.lines.drain(..).enumerate() {
110 if index > 0 {
111 text.push('\n');
112 }
113 text.push_str(&line);
114 }
115 self.bytes = 0;
116 Some(text)
117 }
118}
119
120fn progress_line(text: &str) -> String {
123 let mut line = String::new();
124 for word in text
125 .split(|character: char| character.is_whitespace() || character.is_control())
126 .filter(|word| !word.is_empty())
127 {
128 if !line.is_empty() {
129 line.push(' ');
130 }
131 line.push_str(word);
132 if line.len() > MAX_PROGRESS_LINE_BYTES {
133 break;
134 }
135 }
136 if line.len() <= MAX_PROGRESS_LINE_BYTES {
137 return line;
138 }
139 let mut end = MAX_PROGRESS_LINE_BYTES - PROGRESS_ELIDED.len();
140 while !line.is_char_boundary(end) {
141 end -= 1;
142 }
143 line.truncate(end);
144 line.push_str(PROGRESS_ELIDED);
145 line
146}
147
148#[cfg(test)]
149mod tests;