rustic_rs/config/
progress_options.rs

1//! Progress Bar Config
2
3use 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/// Progress Bar Config
29#[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    /// Don't show any progress bar
34    #[clap(long, global = true, env = "RUSTIC_NO_PROGRESS")]
35    #[merge(strategy=conflate::bool::overwrite_false)]
36    pub no_progress: bool,
37
38    /// Interval to update progress bars (default: 100ms)
39    #[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    /// Get interval for interactive progress bars
53    fn interactive_interval(&self) -> Duration {
54        self.progress_interval
55            .map_or(constants::DEFAULT_INTERVAL, |i| *i)
56    }
57
58    /// Get interval for non-interactive logging
59    fn log_interval(&self) -> Duration {
60        self.progress_interval
61            .map_or(constants::DEFAULT_LOG_INTERVAL, |i| *i)
62    }
63
64    /// Factory Pattern: Create progress indicator based on terminal capabilities
65    ///
66    /// * `Hidden`: If --no-progress is set.
67    /// * `Interactive`: If running in a TTY.
68    /// * `NonInteractive`: If running in a pipe/service (logs to stderr).
69    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// ================ Hidden ================
103
104/// No-op progress implementation; when progress is disabled; --no-progress
105#[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// ================ Interactive ================
120/// Wrapper around `indicatif::ProgressBar` for interactive terminal usage
121#[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// ================ Non-Interactive ================
201
202/// Store state for non-interactive progress
203#[derive(Debug)]
204struct NonInteractiveState {
205    prefix: Cow<'static, str>,
206    position: u64,
207    length: Option<u64>,
208    last_log: Instant,
209}
210
211/// Periodic logger for non-interactive environments (i.e. systemd)
212/// Implemented thread-safe and decouples logging logic from indicatif
213#[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(), // delegate bytesize handling
240            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// ================ Wrap all  ================
302
303/// Unified Progress Bar Type that dispatches to appropriate implementation
304/// based on the current environment (terminal, non-terminal, no progress)
305#[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}