Skip to main content

valheim_mod_manager/
progress.rs

1use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
2use std::time::Duration;
3use thunderstore_engine::progress::{ProgressHandle, ProgressReporter};
4
5/// A [`ProgressReporter`] that renders progress with `indicatif`.
6///
7/// This lives in the binary so the engine stays UI-agnostic. A call without a
8/// known total gets an indeterminate spinner, as the manifest fetch does; one
9/// with a total gets a `#>-` byte bar.
10pub struct IndicatifProgress {
11  multi: MultiProgress,
12}
13
14impl IndicatifProgress {
15  /// Creates a new reporter with its own multi-progress display.
16  pub fn new() -> Self {
17    Self {
18      multi: MultiProgress::new(),
19    }
20  }
21}
22
23impl Default for IndicatifProgress {
24  fn default() -> Self {
25    Self::new()
26  }
27}
28
29impl ProgressReporter for IndicatifProgress {
30  fn begin(&self, label: &str, total: Option<u64>) -> Box<dyn ProgressHandle> {
31    let bar = match total {
32      None => {
33        // Every template must name `{msg}`: `begin` and `finish` both carry
34        // their text as the bar's message, so a template without it renders a
35        // labelless bar and swallows the completion line entirely.
36        let style = ProgressStyle::with_template("{spinner} {msg}")
37          .unwrap()
38          .tick_strings(&["-", "\\", "|", "/", ""]);
39
40        let pb = self.multi.add(ProgressBar::new_spinner());
41        pb.set_style(style);
42        pb.enable_steady_tick(Duration::from_millis(130));
43
44        pb
45      }
46      Some(total) => {
47        let style = ProgressStyle::with_template("{msg} [{bar:40}] {bytes}/{total_bytes}")
48          .unwrap()
49          .progress_chars("#>-");
50
51        let pb = self.multi.add(ProgressBar::new(total));
52        pb.set_style(style);
53
54        pb
55      }
56    };
57
58    bar.set_message(label.to_string());
59
60    Box::new(IndicatifHandle { bar })
61  }
62}
63
64/// A single `indicatif` progress bar handle.
65struct IndicatifHandle {
66  bar: ProgressBar,
67}
68
69impl ProgressHandle for IndicatifHandle {
70  fn inc(&self, delta: u64) {
71    self.bar.inc(delta);
72  }
73
74  fn set_message(&self, msg: &str) {
75    self.bar.set_message(msg.to_string());
76  }
77
78  fn finish(&self, msg: Option<&str>) {
79    match msg {
80      Some(m) => self.bar.finish_with_message(m.to_string()),
81      None => self.bar.finish(),
82    }
83  }
84}
85
86#[cfg(test)]
87mod tests {
88  use super::*;
89
90  #[test]
91  fn both_styles_are_valid_templates() {
92    let reporter = IndicatifProgress::new();
93
94    // `begin` unwraps `with_template`, so a malformed template panics on first
95    // use rather than at compile time. Exercising both branches here is what
96    // stands between a typo and a panic mid-download.
97    let spinner = reporter.begin("fetching", None);
98    let bar = reporter.begin("downloading", Some(1024));
99
100    spinner.finish(Some("fetched"));
101    bar.inc(1024);
102    bar.finish(None);
103  }
104
105  #[test]
106  fn the_default_reporter_reports() {
107    // `Default` is what a caller reaches for to build a reporter without naming
108    // `new`; it has to hand back a working display rather than merely compile.
109    let reporter = IndicatifProgress::default();
110    let bar = reporter.begin("downloading", Some(1024));
111
112    // `set_message` is the engine's way of retitling a bar mid-transfer, so it
113    // runs far more often than `begin` does and is worth exercising at least
114    // once: an `indicatif` handle that has already finished panics on a later
115    // call, and this is the ordering the engine actually uses.
116    bar.set_message("downloading ModA");
117    bar.inc(512);
118    bar.finish(Some("downloaded"));
119  }
120}