1use crate::color::Color;
11use crate::console::{Console, ConsoleOptions};
12use crate::protocol::Renderable;
13use crate::segment::Segment;
14use crate::style::Style;
15
16const BAR: &str = "━"; const HALF_BAR_RIGHT: &str = "╸"; const HALF_BAR_LEFT: &str = "╺"; pub struct ProgressBar {
22 total: f64,
23 completed: f64,
24 width: Option<usize>,
25 complete_style: Style,
26 finished_style: Style,
27 back_style: Style,
28}
29
30impl ProgressBar {
31 pub fn new(total: f64, completed: f64) -> Self {
33 let color = |spec: &str| Style::new().with_color(Color::parse(spec).expect("valid color"));
34 ProgressBar {
35 total,
36 completed,
37 width: None,
38 complete_style: color("rgb(249,38,114)"), finished_style: color("rgb(114,156,31)"), back_style: color("color(237)"), }
42 }
43
44 pub fn width(mut self, width: usize) -> Self {
46 self.width = Some(width);
47 self
48 }
49}
50
51impl Renderable for ProgressBar {
52 fn rich_render(&self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
53 let width = self
54 .width
55 .unwrap_or(options.max_width)
56 .min(options.max_width);
57 if width == 0 || self.total <= 0.0 {
58 return vec![Segment::new(
59 BAR.repeat(width),
60 Some(self.back_style.clone()),
61 )];
62 }
63
64 let completed = self.completed.clamp(0.0, self.total);
65 let complete_halves = (width as f64 * 2.0 * completed / self.total) as usize;
68 let bar_count = complete_halves / 2;
69 let half_bar_count = complete_halves % 2;
70
71 let is_finished = completed >= self.total;
72 let complete_style = if is_finished {
73 &self.finished_style
74 } else {
75 &self.complete_style
76 };
77
78 let mut segments: Vec<Segment> = Vec::new();
79 if bar_count > 0 {
80 segments.push(Segment::new(
81 BAR.repeat(bar_count),
82 Some(complete_style.clone()),
83 ));
84 }
85 if half_bar_count > 0 {
86 segments.push(Segment::new(
87 HALF_BAR_RIGHT.repeat(half_bar_count),
88 Some(complete_style.clone()),
89 ));
90 }
91
92 let mut remaining = width - bar_count - half_bar_count;
94 if remaining > 0 {
95 if half_bar_count == 0 && bar_count > 0 {
96 segments.push(Segment::new(
97 HALF_BAR_LEFT.to_string(),
98 Some(self.back_style.clone()),
99 ));
100 remaining -= 1;
101 }
102 if remaining > 0 {
103 segments.push(Segment::new(
104 BAR.repeat(remaining),
105 Some(self.back_style.clone()),
106 ));
107 }
108 }
109 segments
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::color::ColorSystem;
117
118 fn render(completed: f64) -> String {
119 let console = Console::builder()
120 .force_terminal(true)
121 .color_system(Some(ColorSystem::Truecolor))
122 .width(20)
123 .build();
124 console.render_to_string(&ProgressBar::new(100.0, completed).width(20))
125 }
126
127 #[test]
128 fn empty_bar_is_all_background() {
129 assert_eq!(
130 render(0.0),
131 format!("\x1b[38;5;237m{}\x1b[0m", BAR.repeat(20))
132 );
133 }
134
135 #[test]
136 fn full_bar_uses_finished_style() {
137 assert_eq!(
138 render(100.0),
139 format!("\x1b[38;2;114;156;31m{}\x1b[0m", BAR.repeat(20))
140 );
141 }
142
143 #[test]
144 fn half_bar_has_background_half_cell() {
145 assert_eq!(
147 render(50.0),
148 "\x1b[38;2;249;38;114m━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━\x1b[0m"
149 );
150 }
151}