1use std::f64::consts::PI;
6
7use crate::Console;
8use crate::Renderable;
9use crate::color::{Color, ColorSystem, ColorTriplet, SimpleColor, blend_rgb};
10use crate::console::ConsoleOptions;
11use crate::measure::Measurement;
12use crate::segment::{Segment, Segments};
13use crate::style::Style;
14
15const PULSE_SIZE: usize = 20;
16
17#[derive(Debug, Clone)]
18pub struct ProgressBar {
19 pub total: Option<f64>,
20 pub completed: f64,
21 pub width: Option<usize>,
22 pub pulse: bool,
23 pub style: String,
24 pub complete_style: String,
25 pub finished_style: String,
26 pub pulse_style: String,
27 pub animation_time: Option<f64>,
28}
29
30impl Default for ProgressBar {
31 fn default() -> Self {
32 Self {
33 total: Some(100.0),
34 completed: 0.0,
35 width: None,
36 pulse: false,
37 style: "bar.back".to_string(),
38 complete_style: "bar.complete".to_string(),
39 finished_style: "bar.finished".to_string(),
40 pulse_style: "bar.pulse".to_string(),
41 animation_time: None,
42 }
43 }
44}
45
46impl ProgressBar {
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn percentage_completed(&self) -> f64 {
55 let Some(total) = self.total else {
56 return 0.0;
57 };
58 if total <= 0.0 {
59 return 0.0;
60 }
61 ((self.completed / total) * 100.0).clamp(0.0, 100.0)
62 }
63
64 pub fn update(&mut self, completed: f64, total: Option<f64>) {
65 self.completed = completed;
66 if let Some(t) = total {
67 self.total = Some(t);
68 }
69 }
70
71 fn resolve_style(options: &ConsoleOptions, name: &str) -> Style {
72 options.get_style(name).unwrap_or_default()
73 }
74
75 fn truecolor_from_style(style: Style, foreground: bool) -> ColorTriplet {
76 let simple = if foreground {
77 style.color.unwrap_or(SimpleColor::Default)
78 } else {
79 style.bgcolor.unwrap_or(SimpleColor::Default)
80 };
81 Color::from(simple).get_truecolor(foreground)
82 }
83
84 fn get_pulse_segments(
85 options: &ConsoleOptions,
86 fore_style: Style,
87 back_style: Style,
88 ascii: bool,
89 ) -> Vec<Segment> {
90 let bar = if ascii { "-" } else { "━" };
91
92 let color_system = options.color_system.unwrap_or(ColorSystem::Standard);
93 let no_color = options.color_system.is_none();
94 if !matches!(
95 color_system,
96 ColorSystem::Standard | ColorSystem::EightBit | ColorSystem::TrueColor
97 ) || no_color
98 {
99 let mut segments: Vec<Segment> = Vec::new();
100 segments
101 .extend(std::iter::repeat(Segment::styled(bar, fore_style)).take(PULSE_SIZE / 2));
102 let back_char = if no_color { " " } else { bar };
103 segments.extend(
104 std::iter::repeat(Segment::styled(back_char, back_style))
105 .take(PULSE_SIZE - (PULSE_SIZE / 2)),
106 );
107 return segments;
108 }
109
110 let fore_color = Self::truecolor_from_style(fore_style, true);
112 let back_color = Self::truecolor_from_style(back_style, true);
113 let mut segments: Vec<Segment> = Vec::with_capacity(PULSE_SIZE);
114 for index in 0..PULSE_SIZE {
115 let position = index as f64 / (PULSE_SIZE - 1).max(1) as f64;
116 let fade = (1.0 - (position * 2.0 - 1.0).abs()).powf(1.0);
117 let cross_fade = (fade * PI).cos() * -0.5 + 0.5;
118 let blended = blend_rgb(fore_color, back_color, cross_fade);
119 let style = Style::new().with_color(SimpleColor::Rgb {
120 r: blended.red,
121 g: blended.green,
122 b: blended.blue,
123 });
124 segments.push(Segment::styled(bar, style));
125 }
126 segments
127 }
128
129 fn render_pulse(&self, options: &ConsoleOptions, width: usize, ascii: bool) -> Segments {
130 let fore_style = Self::resolve_style(options, &self.pulse_style);
131 let back_style = Self::resolve_style(options, &self.style);
132 let pulse_segments = Self::get_pulse_segments(options, fore_style, back_style, ascii);
133 let segment_count = pulse_segments.len().max(1);
134
135 let current_time = self.animation_time.unwrap_or(0.0);
136 let mut segments: Vec<Segment> = Vec::new();
137 let repeats = (width / segment_count) + 2;
138 for _ in 0..repeats {
139 segments.extend(pulse_segments.iter().cloned());
140 }
141 let offset = ((-current_time * 15.0) as isize).rem_euclid(segment_count as isize) as usize;
142 let slice = &segments[offset..offset + width];
143 Segments::from_iter(slice.iter().cloned())
144 }
145}
146
147impl Renderable for ProgressBar {
148 fn render(&self, _console: &Console, options: &ConsoleOptions) -> Segments {
149 let width = (self.width.unwrap_or(options.max_width))
150 .min(options.max_width)
151 .max(1);
152 let ascii = options.legacy_windows || !options.encoding.to_lowercase().starts_with("utf");
153 let should_pulse = self.pulse || self.total.is_none();
154 if should_pulse {
155 return self.render_pulse(options, width, ascii);
156 }
157
158 let total = self.total.unwrap_or(0.0);
159 let completed = self.completed.clamp(0.0, total.max(0.0));
160
161 let bar = if ascii { "-" } else { "━" };
162 let half_bar_right = if ascii { " " } else { "╸" };
163 let half_bar_left = if ascii { " " } else { "╺" };
164
165 let complete_halves = if total > 0.0 {
166 ((width as f64) * 2.0 * completed / total).floor() as usize
167 } else {
168 width * 2
169 };
170 let bar_count = complete_halves / 2;
171 let half_bar_count = complete_halves % 2;
172
173 let style = Self::resolve_style(options, &self.style);
174 let is_finished = self.total.is_none() || self.completed >= total;
175 let complete_style = Self::resolve_style(
176 options,
177 if is_finished {
178 &self.finished_style
179 } else {
180 &self.complete_style
181 },
182 );
183
184 let mut out = Segments::new();
185 if bar_count > 0 {
186 out.push(Segment::styled(bar.repeat(bar_count), complete_style));
187 }
188 if half_bar_count > 0 {
189 out.push(Segment::styled(half_bar_right.to_string(), complete_style));
190 }
191
192 if options.color_system.is_none() {
195 let remaining = width.saturating_sub(bar_count + half_bar_count);
196 if remaining > 0 {
197 out.push(Segment::new(" ".repeat(remaining)));
198 }
199 return out;
200 }
201
202 if options.color_system.is_some() {
204 let mut remaining_bars = width.saturating_sub(bar_count + half_bar_count);
205 if remaining_bars > 0 && half_bar_count == 0 && bar_count > 0 {
206 out.push(Segment::styled(half_bar_left.to_string(), style));
207 remaining_bars = remaining_bars.saturating_sub(1);
208 }
209 if remaining_bars > 0 {
210 out.push(Segment::styled(bar.repeat(remaining_bars), style));
211 }
212 }
213
214 out
215 }
216
217 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
218 if let Some(w) = self.width {
219 Measurement::new(w, w)
220 } else {
221 Measurement::new(4, options.max_width)
222 }
223 }
224}