1#[cfg(feature = "ui")]
4use std::io::IsTerminal;
5
6#[cfg(feature = "ui")]
8fn is_interactive() -> bool {
9 if !std::io::stderr().is_terminal() {
11 return false;
12 }
13
14 if std::env::var("NO_COLOR").is_ok() {
16 return false;
17 }
18 if std::env::var("TOKMD_NO_PROGRESS").is_ok() {
19 return false;
20 }
21
22 true
23}
24
25#[cfg(feature = "ui")]
26mod ui_impl {
27 use super::is_interactive;
28 use indicatif::{ProgressBar, ProgressStyle};
29 use std::time::{Duration, Instant};
30
31 pub struct Progress {
33 bar: Option<ProgressBar>,
34 }
35
36 impl Progress {
37 pub fn new(enabled: bool) -> Self {
45 let should_show = enabled && is_interactive();
46
47 let bar = if should_show {
48 let pb = ProgressBar::new_spinner();
49 pb.set_style(
50 ProgressStyle::with_template("{spinner:.cyan} {msg}")
51 .expect("progress template is static and must be valid")
52 .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏", " "]),
53 );
54 pb.enable_steady_tick(Duration::from_millis(80));
55 Some(pb)
56 } else {
57 None
58 };
59
60 Self { bar }
61 }
62
63 pub fn set_message(&self, msg: impl Into<String>) {
65 if let Some(bar) = &self.bar {
66 bar.set_message(msg.into());
67 }
68 }
69
70 pub fn finish_and_clear(&self) {
72 if let Some(bar) = &self.bar {
73 bar.finish_and_clear();
74 }
75 }
76 }
77
78 impl Drop for Progress {
79 fn drop(&mut self) {
80 if let Some(bar) = &self.bar {
81 bar.finish_and_clear();
82 }
83 }
84 }
85
86 #[allow(dead_code)]
88 pub struct ProgressBarWithEta {
89 bar: Option<indicatif::ProgressBar>,
90 start_time: Option<Instant>,
91 }
92
93 #[allow(dead_code)]
94 impl ProgressBarWithEta {
95 pub fn new(enabled: bool, total: u64, message: &str) -> Self {
97 let should_show = enabled && is_interactive();
98
99 let (bar, start_time) = if should_show {
100 let pb = indicatif::ProgressBar::new(total);
101 pb.set_style(
102 ProgressStyle::with_template(
103 "{spinner:.cyan} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta}) {msg}",
104 )
105 .expect("progress template is static and must be valid"),
106 );
107 pb.set_message(message.to_string());
108 pb.enable_steady_tick(Duration::from_millis(100));
109 (Some(pb), Some(Instant::now()))
110 } else {
111 (None, None)
112 };
113
114 Self { bar, start_time }
115 }
116
117 pub fn inc(&self) {
119 if let Some(bar) = &self.bar {
120 bar.inc(1);
121 }
122 }
123
124 pub fn inc_by(&self, delta: u64) {
126 if let Some(bar) = &self.bar {
127 bar.inc(delta);
128 }
129 }
130
131 pub fn set_position(&self, pos: u64) {
133 if let Some(bar) = &self.bar {
134 bar.set_position(pos);
135 }
136 }
137
138 pub fn set_message(&self, msg: &str) {
140 if let Some(bar) = &self.bar {
141 bar.set_message(msg.to_string());
142 }
143 }
144
145 pub fn set_length(&self, len: u64) {
147 if let Some(bar) = &self.bar {
148 bar.set_length(len);
149 }
150 }
151
152 pub fn finish_with_message(&self, msg: &str) {
154 if let Some(bar) = &self.bar {
155 bar.finish_with_message(msg.to_string());
156 }
157 }
158
159 pub fn finish_and_clear(&self) {
161 if let Some(bar) = &self.bar {
162 bar.finish_and_clear();
163 }
164 }
165
166 #[allow(dead_code)]
168 pub fn elapsed(&self) -> Option<Duration> {
169 self.start_time.map(|t| t.elapsed())
170 }
171 }
172
173 impl Drop for ProgressBarWithEta {
174 fn drop(&mut self) {
175 if let Some(bar) = &self.bar {
176 bar.finish_and_clear();
177 }
178 }
179 }
180}
181
182#[cfg(not(feature = "ui"))]
183mod ui_impl {
184 pub struct Progress;
186
187 impl Progress {
188 pub fn new(_enabled: bool) -> Self {
190 Self
191 }
192
193 pub fn set_message(&self, _msg: impl Into<String>) {}
195
196 pub fn finish_and_clear(&self) {}
198 }
199
200 #[allow(dead_code)]
202 pub struct ProgressBarWithEta;
203
204 #[allow(dead_code)]
205 impl ProgressBarWithEta {
206 pub fn new(_enabled: bool, _total: u64, _message: &str) -> Self {
208 Self
209 }
210
211 pub fn inc(&self) {}
213
214 pub fn inc_by(&self, _delta: u64) {}
216
217 pub fn set_position(&self, _pos: u64) {}
219
220 pub fn set_message(&self, _msg: &str) {}
222
223 pub fn set_length(&self, _len: u64) {}
225
226 pub fn finish_with_message(&self, _msg: &str) {}
228
229 pub fn finish_and_clear(&self) {}
231 }
232}
233
234pub use ui_impl::{Progress, ProgressBarWithEta};
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn progress_methods_do_not_panic_when_disabled() {
242 let progress = Progress::new(false);
243 progress.set_message("test");
244 progress.finish_and_clear();
245 }
246
247 #[test]
248 fn progress_bar_methods_do_not_panic_when_disabled() {
249 let progress = ProgressBarWithEta::new(false, 10, "scan");
250 progress.inc();
251 progress.inc_by(2);
252 progress.set_position(3);
253 progress.set_message("updated");
254 progress.set_length(20);
255 progress.finish_with_message("done");
256 progress.finish_and_clear();
257 }
258}