1use std::{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 jiff::SignedDuration;
15use log::info;
16
17use serde::{Deserialize, Serialize};
18use serde_with::{DisplayFromStr, serde_as};
19
20use rustic_core::{Progress, ProgressBars, ProgressType, RusticProgress};
21
22mod constants {
23 use std::time::Duration;
24
25 pub(super) const DEFAULT_INTERVAL: Duration = Duration::from_millis(100);
26 pub(super) const DEFAULT_LOG_INTERVAL: Duration = Duration::from_secs(10);
27}
28
29#[serde_as]
31#[derive(Default, Debug, Parser, Clone, Copy, Deserialize, Serialize, Merge)]
32#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
33pub struct ProgressOptions {
34 #[clap(long, global = true, env = "RUSTIC_NO_PROGRESS")]
36 #[merge(strategy=conflate::bool::overwrite_false)]
37 pub no_progress: bool,
38
39 #[clap(
41 long,
42 global = true,
43 env = "RUSTIC_PROGRESS_INTERVAL",
44 value_name = "DURATION",
45 conflicts_with = "no_progress"
46 )]
47 #[serde_as(as = "Option<DisplayFromStr>")]
48 #[merge(strategy=conflate::option::overwrite_none)]
49 pub progress_interval: Option<SignedDuration>,
50}
51
52impl ProgressOptions {
53 fn interactive_interval(&self) -> Duration {
55 self.progress_interval
56 .map_or(constants::DEFAULT_INTERVAL, |i| {
57 i.try_into().expect("negative durations are not allowed")
58 })
59 }
60
61 fn log_interval(&self) -> Duration {
63 self.progress_interval
64 .map_or(constants::DEFAULT_LOG_INTERVAL, |i| {
65 i.try_into().expect("negative durations are not allowed")
66 })
67 }
68
69 fn create_progress(&self, prefix: &str, kind: ProgressType) -> Progress {
75 if self.no_progress {
76 return Progress::hidden();
77 }
78
79 if std::io::stderr().is_terminal() {
80 Progress::new(InteractiveProgress::new(
81 prefix,
82 kind,
83 self.interactive_interval(),
84 ))
85 } else {
86 let interval = self.log_interval();
87 if interval > Duration::ZERO {
88 Progress::new(NonInteractiveProgress::new(prefix, interval, kind))
89 } else {
90 Progress::hidden()
91 }
92 }
93 }
94}
95
96impl ProgressBars for ProgressOptions {
97 fn progress(&self, progress_kind: ProgressType, prefix: &str) -> Progress {
98 self.create_progress(prefix, progress_kind)
99 }
100}
101
102#[derive(Debug, Clone)]
105pub struct InteractiveProgress {
106 bar: ProgressBar,
107 kind: ProgressType,
108}
109
110impl InteractiveProgress {
111 fn new(prefix: &str, kind: ProgressType, tick_interval: Duration) -> Self {
112 let style = Self::initial_style(kind);
113 let bar = ProgressBar::new(0).with_style(style);
114 bar.set_prefix(prefix.to_string());
115 bar.enable_steady_tick(tick_interval);
116 Self { bar, kind }
117 }
118
119 #[allow(clippy::literal_string_with_formatting_args)]
120 fn initial_style(kind: ProgressType) -> ProgressStyle {
121 let template = match kind {
122 ProgressType::Spinner => "[{elapsed_precise}] {prefix:30} {spinner}",
123 ProgressType::Counter => "[{elapsed_precise}] {prefix:30} {bar:40.cyan/blue} {pos:>10}",
124 ProgressType::Bytes => {
125 "[{elapsed_precise}] {prefix:30} {bar:40.cyan/blue} {bytes:>10} {bytes_per_sec:12}"
126 }
127 };
128 ProgressStyle::default_bar().template(template).unwrap()
129 }
130
131 #[allow(clippy::literal_string_with_formatting_args)]
132 fn style_with_length(kind: ProgressType) -> ProgressStyle {
133 match kind {
134 ProgressType::Spinner => Self::initial_style(kind),
135 ProgressType::Counter => ProgressStyle::default_bar()
136 .template("[{elapsed_precise}] {prefix:30} {bar:40.cyan/blue} {pos:>10}/{len:10}")
137 .unwrap(),
138 ProgressType::Bytes => ProgressStyle::default_bar()
139 .with_key("my_eta", |s: &ProgressState, w: &mut dyn Write| {
140 let _ = match (s.pos(), s.len()) {
141 (pos, Some(len)) if pos != 0 && len > pos => {
142 let eta_secs = s.elapsed().as_secs() * (len - pos) / pos;
143 write!(w, "{:#}", HumanDuration(Duration::from_secs(eta_secs)))
144 }
145 _ => write!(w, "-"),
146 };
147 })
148 .template("[{elapsed_precise}] {prefix:30} {bar:40.cyan/blue} {bytes:>10}/{total_bytes:10} {bytes_per_sec:12} (ETA {my_eta})")
149 .unwrap(),
150 }
151 }
152}
153
154impl RusticProgress for InteractiveProgress {
155 fn is_hidden(&self) -> bool {
156 false
157 }
158
159 fn set_length(&self, len: u64) {
160 if matches!(self.kind, ProgressType::Bytes | ProgressType::Counter) {
161 self.bar.set_style(Self::style_with_length(self.kind));
162 }
163 self.bar.set_length(len);
164 }
165
166 fn set_title(&self, title: &str) {
167 self.bar.set_prefix(title.to_string());
168 }
169
170 fn inc(&self, inc: u64) {
171 self.bar.inc(inc);
172 }
173
174 fn finish(&self) {
175 self.bar.finish_with_message("done");
176 }
177}
178
179#[derive(Debug)]
183struct NonInteractiveState {
184 prefix: String,
185 position: u64,
186 length: Option<u64>,
187 last_log: Instant,
188}
189
190#[derive(Clone, Debug)]
193pub struct NonInteractiveProgress {
194 state: Arc<Mutex<NonInteractiveState>>,
195 start: Instant,
196 interval: Duration,
197 kind: ProgressType,
198}
199
200impl NonInteractiveProgress {
201 fn new(prefix: &str, interval: Duration, kind: ProgressType) -> Self {
202 let now = Instant::now();
203 Self {
204 state: Arc::new(Mutex::new(NonInteractiveState {
205 prefix: prefix.to_string(),
206 position: 0,
207 length: None,
208 last_log: now,
209 })),
210 start: now,
211 interval,
212 kind,
213 }
214 }
215
216 fn format_value(&self, value: u64) -> String {
217 match self.kind {
218 ProgressType::Bytes => ByteSize(value).to_string(), ProgressType::Counter | ProgressType::Spinner => value.to_string(),
220 }
221 }
222
223 fn log_progress(&self, state: &NonInteractiveState) {
224 let progress = state.length.map_or_else(
225 || self.format_value(state.position),
226 |len| {
227 format!(
228 "{} / {}",
229 self.format_value(state.position),
230 self.format_value(len)
231 )
232 },
233 );
234 info!("{}: {}", state.prefix, progress);
235 }
236}
237
238impl RusticProgress for NonInteractiveProgress {
239 fn is_hidden(&self) -> bool {
240 false
241 }
242
243 fn set_length(&self, len: u64) {
244 if let Ok(mut state) = self.state.lock() {
245 state.length = Some(len);
246 }
247 }
248
249 fn set_title(&self, title: &str) {
250 if let Ok(mut state) = self.state.lock() {
251 state.prefix = title.to_string();
252 }
253 }
254
255 fn inc(&self, inc: u64) {
256 if let Ok(mut state) = self.state.lock() {
257 state.position += inc;
258
259 if state.last_log.elapsed() >= self.interval {
260 self.log_progress(&state);
261 state.last_log = Instant::now();
262 }
263 }
264 }
265
266 fn finish(&self) {
267 let Ok(state) = self.state.lock() else {
268 return;
269 };
270
271 info!(
272 "{}: {} done in {:.2?}",
273 state.prefix,
274 self.format_value(state.position),
275 self.start.elapsed()
276 );
277 }
278}