1use crate::color::{Color, ColorSystem, ColorTriplet};
10use crate::console::{monotonic, Console, ConsoleOptions};
11use crate::measure::Measurement;
12use crate::protocol::Renderable;
13use crate::segment::Segment;
14use crate::style::{Style, StyleType};
15
16const PULSE_SIZE: usize = 20;
18
19pub struct ProgressBar {
21 total: Option<f64>,
23 completed: f64,
24 width: Option<usize>,
25 pulse: bool,
26 animation_time: Option<f64>,
27 style: StyleType,
28 complete_style: StyleType,
29 finished_style: StyleType,
30 pulse_style: StyleType,
31}
32
33impl ProgressBar {
34 pub fn new(total: f64, completed: f64) -> Self {
36 ProgressBar {
37 total: Some(total),
38 completed,
39 width: None,
40 pulse: false,
41 animation_time: None,
42 style: "bar.back".into(),
43 complete_style: "bar.complete".into(),
44 finished_style: "bar.finished".into(),
45 pulse_style: "bar.pulse".into(),
46 }
47 }
48
49 pub fn indeterminate() -> Self {
51 ProgressBar {
52 total: None,
53 ..ProgressBar::new(100.0, 0.0)
54 }
55 }
56
57 pub fn width(mut self, width: usize) -> Self {
59 self.width = Some(width);
60 self
61 }
62
63 pub fn style(mut self, style: impl Into<StyleType>) -> Self {
65 self.style = style.into();
66 self
67 }
68
69 pub fn complete_style(mut self, style: impl Into<StyleType>) -> Self {
71 self.complete_style = style.into();
72 self
73 }
74
75 pub fn finished_style(mut self, style: impl Into<StyleType>) -> Self {
77 self.finished_style = style.into();
78 self
79 }
80
81 pub fn pulse_style(mut self, style: impl Into<StyleType>) -> Self {
83 self.pulse_style = style.into();
84 self
85 }
86
87 pub fn pulse(mut self, pulse: bool) -> Self {
89 self.pulse = pulse;
90 self
91 }
92
93 pub fn animation_time(mut self, time: f64) -> Self {
96 self.animation_time = Some(time);
97 self
98 }
99
100 fn pulse_segments(
102 fore: &Style,
103 back: &Style,
104 color_system: Option<ColorSystem>,
105 no_color: bool,
106 ascii: bool,
107 ) -> Vec<Segment> {
108 let bar = if ascii { "-" } else { "\u{2501}" };
109 let colourful = matches!(
113 color_system,
114 Some(ColorSystem::Standard | ColorSystem::Truecolor)
115 );
116 if !colourful || no_color {
117 let fore_count = PULSE_SIZE / 2;
118 let mut segments = vec![Segment::new(bar, Some(fore.clone())); fore_count];
119 let back_bar = if no_color { " " } else { bar };
120 segments.extend(vec![
121 Segment::new(back_bar, Some(back.clone()));
122 PULSE_SIZE - fore_count
123 ]);
124 return segments;
125 }
126 let triplet = |style: &Style, fallback: ColorTriplet| {
127 style
128 .color()
129 .and_then(Color::get_truecolor)
130 .unwrap_or(fallback)
131 };
132 let fore_color = triplet(fore, ColorTriplet::new(255, 0, 255));
133 let back_color = triplet(back, ColorTriplet::new(0, 0, 0));
134 (0..PULSE_SIZE)
135 .map(|index| {
136 let position = index as f64 / PULSE_SIZE as f64;
137 let fade = 0.5 + (position * std::f64::consts::PI * 2.0).cos() / 2.0;
138 let color = blend_rgb(fore_color, back_color, fade);
139 Segment::new(
140 bar,
141 Some(Style::new().with_color(Color::from_rgb(
142 color.red,
143 color.green,
144 color.blue,
145 ))),
146 )
147 })
148 .collect()
149 }
150
151 fn render_pulse(&self, console: &Console, width: usize, ascii: bool) -> Vec<Segment> {
153 let fore = style_or(console, &self.pulse_style, "white");
154 let back = style_or(console, &self.style, "black");
155 let pulse = Self::pulse_segments(
156 &fore,
157 &back,
158 console.color_system(),
159 console.no_color(),
160 ascii,
161 );
162 let count = pulse.len();
163 let time = self.animation_time.unwrap_or_else(monotonic);
164 let offset = ((-time * 15.0) as i64).rem_euclid(count as i64) as usize;
166 pulse
167 .iter()
168 .cycle()
169 .skip(offset)
170 .take(width)
171 .cloned()
172 .collect()
173 }
174}
175
176fn style_or(console: &Console, style: &StyleType, default: &str) -> Style {
178 console
179 .get_style(style)
180 .unwrap_or_else(|_| Style::parse(default).expect("valid default style"))
181}
182
183fn blend_rgb(first: ColorTriplet, second: ColorTriplet, cross_fade: f64) -> ColorTriplet {
185 let mix = |a: u8, b: u8| (f64::from(a) + (f64::from(b) - f64::from(a)) * cross_fade) as u8;
186 ColorTriplet::new(
187 mix(first.red, second.red),
188 mix(first.green, second.green),
189 mix(first.blue, second.blue),
190 )
191}
192
193impl Renderable for ProgressBar {
194 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
197 match self.width {
198 Some(width) => Measurement::new(width, width),
199 None => Measurement::new(4, options.max_width),
200 }
201 }
202
203 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
204 let width = self
205 .width
206 .filter(|width| *width > 0)
207 .unwrap_or(options.max_width)
208 .min(options.max_width);
209 let ascii = console.legacy_windows() || console.ascii_only();
210 if self.pulse || self.total.is_none() {
211 return self.render_pulse(console, width, ascii);
212 }
213 let total = self.total.unwrap_or(0.0);
214 let completed = total.min(self.completed.max(0.0));
215
216 let (bar, half_bar_right, half_bar_left) = if ascii {
217 ("-", " ", " ")
218 } else {
219 ("\u{2501}", "\u{2578}", "\u{257a}")
220 };
221 let complete_halves = if total != 0.0 {
227 ((width as f64 * 2.0 * completed / total) as usize).min(width * 2)
228 } else {
229 width * 2
230 };
231 let bar_count = complete_halves / 2;
232 let half_bar_count = complete_halves % 2;
233 let back = style_or(console, &self.style, "none");
234 let is_finished = self.completed >= total;
235 let complete = style_or(
236 console,
237 if is_finished {
238 &self.finished_style
239 } else {
240 &self.complete_style
241 },
242 "none",
243 );
244
245 let mut segments: Vec<Segment> = Vec::new();
246 if bar_count > 0 {
247 segments.push(Segment::new(bar.repeat(bar_count), Some(complete.clone())));
248 }
249 if half_bar_count > 0 {
250 segments.push(Segment::new(
251 half_bar_right.repeat(half_bar_count),
252 Some(complete),
253 ));
254 }
255 if !console.no_color() && console.color_system().is_some() {
258 let mut remaining = width.saturating_sub(bar_count + half_bar_count);
259 if remaining > 0 {
260 if half_bar_count == 0 && bar_count > 0 {
261 segments.push(Segment::new(half_bar_left, Some(back.clone())));
262 remaining -= 1;
263 }
264 if remaining > 0 {
265 segments.push(Segment::new(bar.repeat(remaining), Some(back)));
266 }
267 }
268 }
269 segments
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use crate::color::ColorSystem;
277
278 fn render(completed: f64) -> String {
279 let console = Console::builder()
280 .force_terminal(true)
281 .color_system(Some(ColorSystem::Truecolor))
282 .width(20)
283 .build();
284 console.render_to_string(&ProgressBar::new(100.0, completed).width(20))
285 }
286
287 #[test]
288 fn huge_totals_do_not_overflow_the_bar_width() {
289 let console = Console::builder()
293 .force_terminal(true)
294 .color_system(Some(ColorSystem::Truecolor))
295 .width(20)
296 .build();
297 for (total, completed) in [(1e308, 1e308), (f64::MAX, f64::MAX), (1e308, 5e307)] {
298 let got = console.render_to_string(&ProgressBar::new(total, completed).width(20));
299 assert!(got.contains('\u{2501}'), "{total}/{completed}: {got:?}");
300 }
301 assert_eq!(render(100.0), {
302 let console = Console::builder()
303 .force_terminal(true)
304 .color_system(Some(ColorSystem::Truecolor))
305 .width(20)
306 .build();
307 console.render_to_string(&ProgressBar::new(1e308, 1e308).width(20))
308 });
309 }
310
311 #[test]
312 fn empty_bar_is_all_background() {
313 assert_eq!(
314 render(0.0),
315 format!("\x1b[38;5;237m{}\x1b[0m", "\u{2501}".repeat(20))
316 );
317 }
318
319 #[test]
320 fn full_bar_uses_finished_style() {
321 assert_eq!(
322 render(100.0),
323 format!("\x1b[38;2;114;156;31m{}\x1b[0m", "\u{2501}".repeat(20))
324 );
325 }
326
327 #[test]
328 fn half_bar_has_background_half_cell() {
329 assert_eq!(
331 render(50.0),
332 "\x1b[38;2;249;38;114m━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━\x1b[0m"
333 );
334 }
335}