1use std::{borrow::Cow, fmt::Write, time::Duration};
4
5use std::io::IsTerminal;
6use std::sync::{Arc, Mutex};
7use std::time::Instant;
8
9use bytesize::ByteSize;
10use indicatif::{HumanDuration, ProgressBar, ProgressState, ProgressStyle};
11
12use clap::Parser;
13use conflate::Merge;
14use log::info;
15
16use serde::{Deserialize, Serialize};
17use serde_with::{DisplayFromStr, serde_as};
18
19use rustic_core::{Progress, ProgressBars};
20
21mod constants {
22 use std::time::Duration;
23
24 pub(super) const DEFAULT_INTERVAL: Duration = Duration::from_millis(100);
25 pub(super) const DEFAULT_LOG_INTERVAL: Duration = Duration::from_secs(10);
26}
27
28#[serde_as]
30#[derive(Default, Debug, Parser, Clone, Copy, Deserialize, Serialize, Merge)]
31#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
32pub struct ProgressOptions {
33 #[clap(long, global = true, env = "RUSTIC_NO_PROGRESS")]
35 #[merge(strategy=conflate::bool::overwrite_false)]
36 pub no_progress: bool,
37
38 #[clap(
40 long,
41 global = true,
42 env = "RUSTIC_PROGRESS_INTERVAL",
43 value_name = "DURATION",
44 conflicts_with = "no_progress"
45 )]
46 #[serde_as(as = "Option<DisplayFromStr>")]
47 #[merge(strategy=conflate::option::overwrite_none)]
48 pub progress_interval: Option<humantime::Duration>,
49}
50
51impl ProgressOptions {
52 fn interactive_interval(&self) -> Duration {
54 self.progress_interval
55 .map_or(constants::DEFAULT_INTERVAL, |i| *i)
56 }
57
58 fn log_interval(&self) -> Duration {
60 self.progress_interval
61 .map_or(constants::DEFAULT_LOG_INTERVAL, |i| *i)
62 }
63
64 fn create_progress(
70 &self,
71 prefix: impl Into<Cow<'static, str>>,
72 kind: ProgressKind,
73 ) -> RusticProgress {
74 if self.no_progress {
75 return RusticProgress::Hidden(HiddenProgress);
76 }
77
78 if std::io::stderr().is_terminal() {
79 RusticProgress::Interactive(InteractiveProgress::new(
80 prefix,
81 kind,
82 self.interactive_interval(),
83 ))
84 } else {
85 let interval = self.log_interval();
86 if interval > Duration::ZERO {
87 RusticProgress::NonInteractive(NonInteractiveProgress::new(prefix, interval, kind))
88 } else {
89 RusticProgress::Hidden(HiddenProgress)
90 }
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96enum ProgressKind {
97 Spinner,
98 Counter,
99 Bytes,
100}
101
102#[derive(Debug, Clone, Copy, Default)]
106pub struct HiddenProgress;
107
108impl Progress for HiddenProgress {
109 fn is_hidden(&self) -> bool {
110 true
111 }
112
113 fn set_length(&self, _len: u64) {}
114 fn set_title(&self, _title: &'static str) {}
115 fn inc(&self, _inc: u64) {}
116 fn finish(&self) {}
117}
118
119#[derive(Debug, Clone)]
122pub struct InteractiveProgress {
123 bar: ProgressBar,
124 kind: ProgressKind,
125}
126
127impl InteractiveProgress {
128 fn new(
129 prefix: impl Into<Cow<'static, str>>,
130 kind: ProgressKind,
131 tick_interval: Duration,
132 ) -> Self {
133 let style = Self::initial_style(kind);
134 let bar = ProgressBar::new(0).with_style(style);
135 bar.set_prefix(prefix);
136 bar.enable_steady_tick(tick_interval);
137 Self { bar, kind }
138 }
139
140 #[allow(clippy::literal_string_with_formatting_args)]
141 fn initial_style(kind: ProgressKind) -> ProgressStyle {
142 let template = match kind {
143 ProgressKind::Spinner => "[{elapsed_precise}] {prefix:30} {spinner}",
144 ProgressKind::Counter => "[{elapsed_precise}] {prefix:30} {bar:40.cyan/blue} {pos:>10}",
145 ProgressKind::Bytes => {
146 "[{elapsed_precise}] {prefix:30} {bar:40.cyan/blue} {bytes:>10} {bytes_per_sec:12}"
147 }
148 };
149 ProgressStyle::default_bar().template(template).unwrap()
150 }
151
152 #[allow(clippy::literal_string_with_formatting_args)]
153 fn style_with_length(kind: ProgressKind) -> ProgressStyle {
154 match kind {
155 ProgressKind::Spinner => Self::initial_style(kind),
156 ProgressKind::Counter => ProgressStyle::default_bar()
157 .template("[{elapsed_precise}] {prefix:30} {bar:40.cyan/blue} {pos:>10}/{len:10}")
158 .unwrap(),
159 ProgressKind::Bytes => ProgressStyle::default_bar()
160 .with_key("my_eta", |s: &ProgressState, w: &mut dyn Write| {
161 let _ = match (s.pos(), s.len()) {
162 (pos, Some(len)) if pos != 0 && len > pos => {
163 let eta_secs = s.elapsed().as_secs() * (len - pos) / pos;
164 write!(w, "{:#}", HumanDuration(Duration::from_secs(eta_secs)))
165 }
166 _ => write!(w, "-"),
167 };
168 })
169 .template("[{elapsed_precise}] {prefix:30} {bar:40.cyan/blue} {bytes:>10}/{total_bytes:10} {bytes_per_sec:12} (ETA {my_eta})")
170 .unwrap(),
171 }
172 }
173}
174
175impl Progress for InteractiveProgress {
176 fn is_hidden(&self) -> bool {
177 false
178 }
179
180 fn set_length(&self, len: u64) {
181 if matches!(self.kind, ProgressKind::Bytes | ProgressKind::Counter) {
182 self.bar.set_style(Self::style_with_length(self.kind));
183 }
184 self.bar.set_length(len);
185 }
186
187 fn set_title(&self, title: &'static str) {
188 self.bar.set_prefix(title);
189 }
190
191 fn inc(&self, inc: u64) {
192 self.bar.inc(inc);
193 }
194
195 fn finish(&self) {
196 self.bar.finish_with_message("done");
197 }
198}
199
200#[derive(Debug)]
204struct NonInteractiveState {
205 prefix: Cow<'static, str>,
206 position: u64,
207 length: Option<u64>,
208 last_log: Instant,
209}
210
211#[derive(Clone, Debug)]
214pub struct NonInteractiveProgress {
215 state: Arc<Mutex<NonInteractiveState>>,
216 start: Instant,
217 interval: Duration,
218 kind: ProgressKind,
219}
220
221impl NonInteractiveProgress {
222 fn new(prefix: impl Into<Cow<'static, str>>, interval: Duration, kind: ProgressKind) -> Self {
223 let now = Instant::now();
224 Self {
225 state: Arc::new(Mutex::new(NonInteractiveState {
226 prefix: prefix.into(),
227 position: 0,
228 length: None,
229 last_log: now,
230 })),
231 start: now,
232 interval,
233 kind,
234 }
235 }
236
237 fn format_value(&self, value: u64) -> String {
238 match self.kind {
239 ProgressKind::Bytes => ByteSize(value).to_string(), ProgressKind::Counter | ProgressKind::Spinner => value.to_string(),
241 }
242 }
243
244 fn log_progress(&self, state: &NonInteractiveState) {
245 let progress = state.length.map_or_else(
246 || self.format_value(state.position),
247 |len| {
248 format!(
249 "{} / {}",
250 self.format_value(state.position),
251 self.format_value(len)
252 )
253 },
254 );
255 info!("{}: {}", state.prefix, progress);
256 }
257}
258
259impl Progress for NonInteractiveProgress {
260 fn is_hidden(&self) -> bool {
261 false
262 }
263
264 fn set_length(&self, len: u64) {
265 if let Ok(mut state) = self.state.lock() {
266 state.length = Some(len);
267 }
268 }
269
270 fn set_title(&self, title: &'static str) {
271 if let Ok(mut state) = self.state.lock() {
272 state.prefix = Cow::Borrowed(title);
273 }
274 }
275
276 fn inc(&self, inc: u64) {
277 if let Ok(mut state) = self.state.lock() {
278 state.position += inc;
279
280 if state.last_log.elapsed() >= self.interval {
281 self.log_progress(&state);
282 state.last_log = Instant::now();
283 }
284 }
285 }
286
287 fn finish(&self) {
288 let Ok(state) = self.state.lock() else {
289 return;
290 };
291
292 info!(
293 "{}: {} done in {:.2?}",
294 state.prefix,
295 self.format_value(state.position),
296 self.start.elapsed()
297 );
298 }
299}
300
301#[derive(Debug, Clone)]
306pub enum RusticProgress {
307 Hidden(HiddenProgress),
308 Interactive(InteractiveProgress),
309 NonInteractive(NonInteractiveProgress),
310}
311
312impl Progress for RusticProgress {
313 fn is_hidden(&self) -> bool {
314 match self {
315 Self::Hidden(p) => p.is_hidden(),
316 Self::Interactive(p) => p.is_hidden(),
317 Self::NonInteractive(p) => p.is_hidden(),
318 }
319 }
320
321 fn set_length(&self, len: u64) {
322 match self {
323 Self::Hidden(p) => p.set_length(len),
324 Self::Interactive(p) => p.set_length(len),
325 Self::NonInteractive(p) => p.set_length(len),
326 }
327 }
328
329 fn set_title(&self, title: &'static str) {
330 match self {
331 Self::Hidden(p) => p.set_title(title),
332 Self::Interactive(p) => p.set_title(title),
333 Self::NonInteractive(p) => p.set_title(title),
334 }
335 }
336
337 fn inc(&self, inc: u64) {
338 match self {
339 Self::Hidden(p) => p.inc(inc),
340 Self::Interactive(p) => p.inc(inc),
341 Self::NonInteractive(p) => p.inc(inc),
342 }
343 }
344
345 fn finish(&self) {
346 match self {
347 Self::Hidden(p) => p.finish(),
348 Self::Interactive(p) => p.finish(),
349 Self::NonInteractive(p) => p.finish(),
350 }
351 }
352}
353
354impl ProgressBars for ProgressOptions {
355 type P = RusticProgress;
356
357 fn progress_spinner(&self, prefix: impl Into<Cow<'static, str>>) -> RusticProgress {
358 self.create_progress(prefix, ProgressKind::Spinner)
359 }
360
361 fn progress_counter(&self, prefix: impl Into<Cow<'static, str>>) -> RusticProgress {
362 self.create_progress(prefix, ProgressKind::Counter)
363 }
364
365 fn progress_hidden(&self) -> RusticProgress {
366 RusticProgress::Hidden(HiddenProgress)
367 }
368
369 fn progress_bytes(&self, prefix: impl Into<Cow<'static, str>>) -> RusticProgress {
370 self.create_progress(prefix, ProgressKind::Bytes)
371 }
372}