agpm_cli/utils/
progress.rs1use crate::manifest::Manifest;
40use indicatif::{ProgressBar as IndicatifBar, ProgressStyle as IndicatifStyle};
41use std::sync::{Arc, Mutex};
42use std::time::Duration;
43
44#[deprecated(since = "0.3.0", note = "Use MultiPhaseProgress instead")]
46pub use indicatif::ProgressBar;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum InstallationPhase {
51 SyncingSources,
53 ResolvingDependencies,
55 Installing,
57 InstallingResources,
59 Finalizing,
61}
62
63impl InstallationPhase {
64 pub const fn description(&self) -> &'static str {
66 match self {
67 Self::SyncingSources => "Syncing sources",
68 Self::ResolvingDependencies => "Resolving dependencies",
69 Self::Installing => "Installing resources",
70 Self::InstallingResources => "Installing resources",
71 Self::Finalizing => "Finalizing installation",
72 }
73 }
74
75 pub const fn spinner_prefix(&self) -> &'static str {
77 match self {
78 Self::SyncingSources => "⏳",
79 Self::ResolvingDependencies => "🔍",
80 Self::Installing => "📦",
81 Self::InstallingResources => "📦",
82 Self::Finalizing => "✨",
83 }
84 }
85}
86
87#[derive(Clone)]
90pub struct MultiPhaseProgress {
91 multi: Arc<indicatif::MultiProgress>,
93 current_bar: Arc<Mutex<Option<IndicatifBar>>>,
95 enabled: bool,
97}
98
99impl MultiPhaseProgress {
100 pub fn new(enabled: bool) -> Self {
102 Self {
103 multi: Arc::new(indicatif::MultiProgress::new()),
104 current_bar: Arc::new(Mutex::new(None)),
105 enabled,
106 }
107 }
108
109 pub fn start_phase(&self, phase: InstallationPhase, message: Option<&str>) {
111 if !self.enabled {
112 if !self.enabled {
114 return;
115 }
116 let phase_msg = if let Some(msg) = message {
117 format!("{} {} {}", phase.spinner_prefix(), phase.description(), msg)
118 } else {
119 format!("{} {}", phase.spinner_prefix(), phase.description())
120 };
121 println!("{phase_msg}");
122 return;
123 }
124
125 if let Ok(mut guard) = self.current_bar.lock() {
128 *guard = None;
129 }
130
131 let spinner = self.multi.add(IndicatifBar::new_spinner());
133
134 let phase_msg =
136 format!("{} {} {}", phase.spinner_prefix(), phase.description(), message.unwrap_or(""));
137
138 let style = IndicatifStyle::default_spinner()
140 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ")
141 .template("{spinner} {msg}")
142 .unwrap();
143
144 spinner.set_style(style);
145 spinner.set_message(phase_msg);
146 spinner.enable_steady_tick(Duration::from_millis(100));
147
148 *self.current_bar.lock().unwrap() = Some(spinner);
150 }
151
152 pub fn start_phase_with_progress(&self, phase: InstallationPhase, total: usize) {
154 if !self.enabled {
155 if !self.enabled {
157 return;
158 }
159 println!("{} {} (0/{})", phase.spinner_prefix(), phase.description(), total);
160 return;
161 }
162
163 if let Ok(mut guard) = self.current_bar.lock() {
166 *guard = None;
167 }
168
169 let progress_bar = self.multi.add(IndicatifBar::new(total as u64));
171
172 let style = IndicatifStyle::default_bar()
174 .template(&format!(
175 "{} {{msg}} [{{bar:40.cyan/blue}}] {{pos}}/{{len}}",
176 phase.spinner_prefix()
177 ))
178 .unwrap()
179 .progress_chars("=>-");
180
181 progress_bar.set_style(style);
182 progress_bar.set_message(phase.description());
183
184 *self.current_bar.lock().unwrap() = Some(progress_bar);
186 }
187
188 pub fn update_message(&self, message: String) {
190 if let Ok(guard) = self.current_bar.lock()
191 && let Some(ref bar) = *guard
192 {
193 bar.set_message(message);
194 }
195 }
196
197 pub fn update_current_message(&self, message: &str) {
199 if let Ok(guard) = self.current_bar.lock()
200 && let Some(ref bar) = *guard
201 {
202 bar.set_message(message.to_string());
203 }
204 }
205
206 pub fn increment_progress(&self, delta: u64) {
208 if let Ok(guard) = self.current_bar.lock()
209 && let Some(ref bar) = *guard
210 {
211 bar.inc(delta);
212 }
213 }
214
215 pub fn set_progress(&self, pos: usize) {
217 if let Ok(guard) = self.current_bar.lock()
218 && let Some(ref bar) = *guard
219 {
220 bar.set_position(pos as u64);
221 }
222 }
223
224 pub fn complete_phase(&self, message: Option<&str>) {
226 if !self.enabled {
227 if !self.enabled {
229 return;
230 }
231 if let Some(msg) = message {
232 println!("✓ {msg}");
233 }
234 return;
235 }
236
237 if let Ok(mut guard) = self.current_bar.lock()
239 && let Some(bar) = guard.take()
240 {
241 bar.disable_steady_tick();
243
244 let final_message = if let Some(msg) = message {
246 format!("✓ {msg}")
247 } else {
248 "✓ Phase complete".to_string()
249 };
250
251 bar.finish_and_clear();
253
254 self.multi.suspend(|| {
257 println!("{final_message}");
258 });
259 }
260 }
261
262 pub fn clear(&self) {
264 if let Ok(mut guard) = self.current_bar.lock()
266 && let Some(bar) = guard.take()
267 {
268 bar.finish_and_clear();
269 }
270 self.multi.clear().ok();
271 }
272
273 pub fn add_progress_bar(&self, total: u64) -> Option<IndicatifBar> {
275 if !self.enabled {
276 return None;
277 }
278
279 let pb = self.multi.add(IndicatifBar::new(total));
280 let style = IndicatifStyle::default_bar()
281 .template(" {msg} [{bar:40.cyan/blue}] {pos}/{len}")
282 .unwrap()
283 .progress_chars("=>-");
284 pb.set_style(style);
285 Some(pb)
286 }
287}
288
289pub fn collect_dependency_names(manifest: &Manifest) -> Vec<String> {
291 manifest.all_dependencies().iter().map(|(name, _)| (*name).to_string()).collect()
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn test_installation_phase_description() {
300 assert_eq!(InstallationPhase::SyncingSources.description(), "Syncing sources");
301 assert_eq!(
302 InstallationPhase::ResolvingDependencies.description(),
303 "Resolving dependencies"
304 );
305 assert_eq!(InstallationPhase::Installing.description(), "Installing resources");
306 assert_eq!(InstallationPhase::InstallingResources.description(), "Installing resources");
307 assert_eq!(InstallationPhase::Finalizing.description(), "Finalizing installation");
308 }
309
310 #[test]
311 fn test_installation_phase_spinner_prefix() {
312 assert_eq!(InstallationPhase::SyncingSources.spinner_prefix(), "⏳");
313 assert_eq!(InstallationPhase::ResolvingDependencies.spinner_prefix(), "🔍");
314 assert_eq!(InstallationPhase::Installing.spinner_prefix(), "📦");
315 assert_eq!(InstallationPhase::InstallingResources.spinner_prefix(), "📦");
316 assert_eq!(InstallationPhase::Finalizing.spinner_prefix(), "✨");
317 }
318
319 #[test]
320 fn test_multi_phase_progress_new() {
321 let progress = MultiPhaseProgress::new(true);
322
323 progress.start_phase(InstallationPhase::SyncingSources, Some("test message"));
325 progress.update_current_message("updated message");
326 progress.complete_phase(Some("completed"));
327 progress.clear();
328 }
329
330 #[test]
331 fn test_multi_phase_progress_with_progress_bar() {
332 let progress = MultiPhaseProgress::new(true);
333
334 progress.start_phase_with_progress(InstallationPhase::Installing, 10);
335 progress.increment_progress(5);
336 progress.set_progress(8);
337 progress.complete_phase(Some("Installation completed"));
338 }
339
340 #[test]
341 fn test_multi_phase_progress_disabled() {
342 let progress = MultiPhaseProgress::new(false);
343
344 progress.start_phase(InstallationPhase::SyncingSources, None);
346 progress.complete_phase(Some("test"));
347 progress.clear();
348 }
349
350 #[test]
351 fn test_collect_dependency_names() {
352 }
358}