1#![warn(missing_docs)]
46
47use std::{
48 collections::BTreeMap,
49 sync::{
50 atomic::{AtomicBool, AtomicUsize},
51 Arc, Mutex, Weak,
52 },
53};
54
55mod template;
56mod ticker;
57pub mod writer;
58use template::{Template, TemplatePart};
59use termsize::get_width;
60use ticker::Ticker;
61mod termsize;
62
63const CLEAR_ANSI: &str = "\r\x1b[K";
64const UP_ANSI: &str = "\x1b[F";
65
66pub(crate) struct BarState {
67 len: u64,
68 pos: u64,
69 message: String,
70 template: Template,
71 created_at: std::time::Instant,
72 visible: bool,
73 need_redraw: bool,
75}
76
77fn duration_to_human(duration: std::time::Duration) -> String {
78 let elapsed = duration.as_secs();
79 let hours = elapsed / 3600;
80 let minutes = (elapsed % 3600) / 60;
81 let seconds = elapsed % 60;
82 format!("{}:{:02}:{:02}", hours, minutes, seconds)
83}
84
85fn bytes_to_human(bytes: u64) -> String {
86 const KB: u64 = 1024;
87 const MB: u64 = KB * 1024;
88 const GB: u64 = MB * 1024;
89 const TB: u64 = GB * 1024;
90
91 if bytes < KB {
92 format!("{} B", bytes)
93 } else if bytes < MB {
94 format!("{:.2} KiB", bytes as f64 / KB as f64)
95 } else if bytes < GB {
96 format!("{:.2} MiB", bytes as f64 / MB as f64)
97 } else if bytes < TB {
98 format!("{:.2} GiB", bytes as f64 / GB as f64)
99 } else {
100 format!("{:.2} TiB", bytes as f64 / TB as f64)
101 }
102}
103
104fn string_width(s: &str) -> usize {
105 #[cfg(feature = "unicode")]
106 {
107 unicode_width::UnicodeWidthStr::width(s)
108 }
109
110 #[cfg(not(feature = "unicode"))]
111 {
112 s.chars().count()
113 }
114}
115
116impl BarState {
117 pub fn render(&self) -> String {
118 let mut result = String::new();
119 let elapsed = std::time::Instant::now() - self.created_at;
120 let bytes_per_second = self.pos as f64 / elapsed.as_secs_f64();
121 for part in self.template.parts.iter() {
122 match part {
123 TemplatePart::Text(text) => {
124 result.push_str(text);
125 }
126 TemplatePart::Newline => {
127 result.push('\n');
128 }
129 TemplatePart::Message => {
130 result.push_str(&self.message);
131 }
132 TemplatePart::Elapsed => {
133 result.push_str(&duration_to_human(elapsed));
134 }
135 TemplatePart::Bytes => {
136 result.push_str(&bytes_to_human(self.pos));
137 }
138 TemplatePart::Pos => {
139 result.push_str(&self.pos.to_string());
140 }
141 TemplatePart::TotalBytes => {
142 result.push_str(&bytes_to_human(self.len));
143 }
144 TemplatePart::Total => {
145 result.push_str(&self.len.to_string());
146 }
147 TemplatePart::BytesPerSecond => {
148 result.push_str(&format!("{}/s", bytes_to_human(bytes_per_second as u64)));
149 }
150 TemplatePart::Eta => {
151 if self.pos == 0 {
152 result.push_str("Unknown");
153 } else {
154 let eta = (self.len - self.pos) as f64 / bytes_per_second;
155 result.push_str(&duration_to_human(std::time::Duration::from_secs(
156 eta as u64,
157 )));
158 }
159 }
160 TemplatePart::Bar(size) => {
161 let filled = (self.pos as f64 / self.len as f64 * *size as f64) as usize;
162 if *size >= filled {
163 let empty = *size - filled;
164 result.push('[');
165 for _ in 0..filled {
166 result.push('=');
167 }
168 for _ in 0..empty {
169 result.push(' ');
170 }
171 result.push(']');
172 } else {
173 let overflowed = filled - *size;
174 result.push('[');
175 for _ in 0..*size {
176 result.push('=');
177 }
178 for _ in 0..overflowed {
179 result.push('!');
180 }
181 }
182 }
183 TemplatePart::StateEmoji => {
184 if self.pos == self.len {
185 result.push('✅');
186 } else if self.pos == 0 {
187 result.push('🆕');
188 } else if self.pos > self.len {
189 result.push('💥');
190 } else {
191 result.push('⏳');
193 }
194 }
195 }
196 }
197 result
198 }
199}
200
201pub struct Bar {
203 id: usize,
204 manager: Weak<ManagerInner>,
205}
206
207pub(crate) struct ManagerInner {
212 states: Mutex<BTreeMap<usize, Arc<Mutex<BarState>>>>,
213 ansi: Mutex<Option<bool>>,
214 interval: std::time::Duration,
215 pub(crate) out: Arc<Mutex<Box<dyn Out>>>,
216 ticker: Mutex<Option<Ticker>>,
217 force_when_finished: AtomicBool,
218
219 next_id: AtomicUsize,
221 last_draw: Mutex<std::time::Instant>,
222 last_lines: AtomicUsize,
223 need_redraw: AtomicBool,
224}
225
226impl ManagerInner {
227 pub(crate) fn is_ticker_enabled(&self) -> bool {
228 self.ticker.lock().unwrap().is_some()
229 }
230
231 pub(crate) fn clear_existing(&self, out: &mut Box<dyn Out>) {
233 for _ in 0..self.last_lines.load(std::sync::atomic::Ordering::Acquire) {
234 let _ = out.write_all(format!("{}{}", UP_ANSI, CLEAR_ANSI).as_bytes());
235 }
236 }
237
238 pub(crate) fn is_terminal(&self, out: &mut Box<dyn Out>) -> bool {
239 let ansi = self.ansi.lock().unwrap();
240 match *ansi {
241 None => out.is_terminal(),
242 Some(force) => force,
243 }
244 }
245
246 pub(crate) fn draw_inner(
247 &self,
248 states: &BTreeMap<usize, Arc<Mutex<BarState>>>,
249 out: &mut Box<dyn Out>,
250 is_terminal: bool,
251 ) {
252 let mut newlines = 0;
253 for state in states.values() {
254 let mut state = state.lock().unwrap();
255 if !state.visible {
256 continue;
257 }
258 if !is_terminal && !state.need_redraw {
259 continue;
260 }
261 let outstr = format!("{}\n", state.render());
262 if is_terminal {
263 let splits = outstr.split('\n');
264 let term_col = get_width(out.as_ref()) as usize;
265 for i in splits {
266 let width = string_width(i);
267 newlines += width / term_col;
268 if width % term_col != 0 {
269 newlines += 1;
270 }
271 }
272 }
273 let _ = out.write_all(outstr.as_bytes());
274 state.need_redraw = false;
275 }
276 if is_terminal {
277 self.last_lines
278 .store(newlines, std::sync::atomic::Ordering::Release);
279 }
280 }
281
282 pub(crate) fn mark_redraw(&self) {
283 self.need_redraw
284 .store(true, std::sync::atomic::Ordering::Release);
285 }
286
287 pub(crate) fn draw(&self, force: bool) {
288 if !force && self.is_ticker_enabled() {
289 return;
290 }
291 let now = std::time::Instant::now();
292 let mut last_draw = self.last_draw.lock().unwrap();
293 if !force && now - *last_draw < self.interval {
294 return;
295 }
296
297 if !self
298 .need_redraw
299 .swap(false, std::sync::atomic::Ordering::AcqRel)
300 {
301 return;
302 }
303 let mut out = self.out.lock().unwrap();
304 let states = self.states.lock().unwrap();
305 let is_terminal = self.is_terminal(&mut out);
306 if is_terminal && states.len() > 0 {
307 self.clear_existing(&mut out);
309 }
310
311 self.draw_inner(&states, &mut out, is_terminal);
312
313 *last_draw = now;
314 }
315
316 pub(crate) fn suspend<F: FnOnce(&mut Box<dyn Out>) -> R, R>(&self, f: F) -> R {
317 let mut out = self.out.lock().unwrap();
318 let is_terminal = self.is_terminal(&mut out);
319 if is_terminal {
320 self.clear_existing(&mut out);
321 }
322 let result = f(&mut out);
323 if is_terminal {
324 let states = self.states.lock().unwrap();
325 self.draw_inner(&states, &mut out, is_terminal);
326 }
327 result
328 }
329}
330
331#[cfg(all(unix, feature = "console_width"))]
334pub trait Out: std::io::Write + std::io::IsTerminal + std::os::fd::AsRawFd + Send + Sync {}
335#[cfg(all(unix, feature = "console_width"))]
336impl<T: std::io::Write + std::io::IsTerminal + std::os::fd::AsRawFd + Send + Sync> Out for T {}
337
338#[cfg(all(windows, feature = "console_width"))]
341pub trait Out:
342 std::io::Write + std::io::IsTerminal + std::os::windows::io::AsRawHandle + Send + Sync
343{
344}
345#[cfg(all(windows, feature = "console_width"))]
346impl<T: std::io::Write + std::io::IsTerminal + std::os::windows::io::AsRawHandle + Send + Sync> Out
347 for T
348{
349}
350
351#[cfg(not(any(
354 all(windows, feature = "console_width"),
355 all(unix, feature = "console_width")
356)))]
357pub trait Out: std::io::Write + std::io::IsTerminal + Send + Sync {}
358#[cfg(not(any(
359 all(windows, feature = "console_width"),
360 all(unix, feature = "console_width")
361)))]
362impl<T: std::io::Write + std::io::IsTerminal + Send + Sync> Out for T {}
363
364pub struct Manager {
369 inner: Arc<ManagerInner>,
370}
371
372impl Manager {
373 pub fn new(interval: std::time::Duration) -> Self {
377 Manager {
378 inner: Arc::new(ManagerInner {
379 states: Mutex::new(BTreeMap::new()),
380 next_id: AtomicUsize::new(0),
381 interval,
382 out: Arc::new(Mutex::new(Box::new(std::io::stdout()))),
383 last_draw: Mutex::new(std::time::Instant::now() - interval),
384 last_lines: AtomicUsize::new(0),
385 ansi: Mutex::new(None),
386 need_redraw: AtomicBool::new(false),
387 ticker: Mutex::new(None),
388 force_when_finished: AtomicBool::new(true),
389 }),
390 }
391 }
392
393 fn mark_redraw(&self) {
394 self.inner.mark_redraw();
395 }
396
397 pub fn with_stdout(self) -> Self {
399 *self.inner.out.lock().unwrap() = Box::new(std::io::stdout());
400 self.mark_redraw();
401 self
402 }
403
404 pub fn with_stderr(self) -> Self {
406 *self.inner.out.lock().unwrap() = Box::new(std::io::stderr());
407 self.mark_redraw();
408 self
409 }
410
411 pub fn with_file(self, file: std::fs::File) -> Self {
413 *self.inner.out.lock().unwrap() = Box::new(file);
414 self.mark_redraw();
415 self
416 }
417
418 pub fn auto_ansi(self) -> Self {
420 *self.inner.ansi.lock().unwrap() = None;
421 self.mark_redraw();
422 self
423 }
424
425 pub fn force_ansi(self, force: bool) -> Self {
427 *self.inner.ansi.lock().unwrap() = Some(force);
428 self.mark_redraw();
429 self
430 }
431
432 pub fn set_ticker(&self, set_ticker: bool) {
436 let mut ticker = self.inner.ticker.lock().unwrap();
437 if set_ticker && ticker.is_none() {
438 *ticker = Some(Ticker::new(self.inner.clone()));
439 } else if !set_ticker && ticker.is_some() {
440 *ticker = None;
441 }
442 }
443
444 pub fn force_draw_when_finished(&self, force: bool) {
448 self.inner
449 .force_when_finished
450 .store(force, std::sync::atomic::Ordering::Release);
451 }
452
453 pub fn create_bar(&self, len: u64, message: &str, template: &str, visible: bool) -> Bar {
462 let id = self
463 .inner
464 .next_id
465 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
466 let bar_state = Arc::new(Mutex::new(BarState {
467 len,
468 pos: 0,
469 message: message.to_string(),
470 template: Template::new(template),
471 created_at: std::time::Instant::now(),
472 visible,
473 need_redraw: true,
474 }));
475
476 self.inner
477 .states
478 .lock()
479 .unwrap()
480 .insert(id, bar_state.clone());
481
482 if visible {
483 self.mark_redraw();
484 self.draw(true);
485 }
486
487 Bar {
488 manager: Arc::downgrade(&self.inner),
489 id,
490 }
491 }
492
493 pub fn draw(&self, force: bool) {
503 self.inner.draw(force);
504 }
505
506 pub fn suspend<F: FnOnce(&mut Box<dyn Out>) -> R, R>(&self, f: F) -> R {
512 self.inner.suspend(f)
513 }
514
515 pub fn create_writer(&self) -> writer::KyuriWriter {
517 writer::KyuriWriter::new(self.inner.clone())
518 }
519}
520
521impl Drop for ManagerInner {
522 fn drop(&mut self) {
524 self.draw(true);
525 }
526}
527
528impl Bar {
529 fn get_manager_and_state(&self) -> Option<(Arc<ManagerInner>, Arc<Mutex<BarState>>)> {
530 let manager = self.manager.upgrade()?;
531 let state = manager.states.lock().unwrap().get(&self.id)?.clone();
532 Some((manager, state))
533 }
534
535 fn check_if_force_draw(&self, manager: Arc<ManagerInner>, pos: u64, len: u64) {
536 if pos == len && manager
537 .force_when_finished
538 .load(std::sync::atomic::Ordering::Acquire)
539 {
540 manager.draw(true);
541 } else {
542 manager.draw(false);
543 }
544 }
545
546 pub fn inc(&self, n: u64) {
548 if let Some((manager, state)) = self.get_manager_and_state() {
549 let mut state = state.lock().unwrap();
550 state.pos += n;
551 state.need_redraw = true;
552 let pos = state.pos;
553 let len = state.len;
554 std::mem::drop(state);
556 manager.mark_redraw();
557 self.check_if_force_draw(manager, pos, len);
558 }
559 }
560
561 pub fn set_pos(&self, pos: u64) {
563 if let Some((manager, state)) = self.get_manager_and_state() {
564 let mut state = state.lock().unwrap();
565 state.pos = pos;
566 state.need_redraw = true;
567 let pos = state.pos;
568 let len = state.len;
569 std::mem::drop(state);
571 manager.mark_redraw();
572 self.check_if_force_draw(manager, pos, len);
573 }
574 }
575
576 pub fn set_len(&self, len: u64) {
578 if let Some((manager, state)) = self.get_manager_and_state() {
579 let mut state = state.lock().unwrap();
580 state.len = len;
581 state.need_redraw = true;
582 let pos = state.pos;
583 let len = state.len;
584 std::mem::drop(state);
586 manager.mark_redraw();
587 self.check_if_force_draw(manager, pos, len);
588 }
589 }
590
591 pub fn get_pos(&self) -> u64 {
595 self.get_manager_and_state()
596 .map_or(0, |(_, state)| state.lock().unwrap().pos)
597 }
598
599 pub fn get_len(&self) -> u64 {
603 self.get_manager_and_state()
604 .map_or(0, |(_, state)| state.lock().unwrap().len)
605 }
606
607 pub fn finish(&self) {
609 if let Some((manager, state)) = self.get_manager_and_state() {
610 let state = state.lock().unwrap();
611 let pos = state.pos;
612 let len = state.len;
613 if pos != len {
614 self.set_pos(len);
615 }
616 std::mem::drop(state);
617 manager.draw(true);
618 }
619 }
620
621 pub fn finish_and_drop(self) {
623 self.finish();
624 }
626
627 pub fn set_visible(&self, visible: bool) {
629 if let Some((manager, state)) = self.get_manager_and_state() {
630 let mut state = state.lock().unwrap();
631 if state.visible != visible {
632 state.visible = visible;
633 state.need_redraw = true;
634 std::mem::drop(state);
636 manager.mark_redraw();
637 manager.draw(true);
638 }
639 }
640 }
641
642 pub fn is_visible(&self) -> bool {
646 self.get_manager_and_state()
647 .map_or(false, |(_, state)| state.lock().unwrap().visible)
648 }
649
650 pub fn set_message(&self, message: &str) {
652 if let Some((manager, state)) = self.get_manager_and_state() {
653 let mut state = state.lock().unwrap();
654 state.message = message.to_string();
655 state.need_redraw = true;
656 std::mem::drop(state);
658 manager.mark_redraw();
659 manager.draw(false);
660 }
661 }
662
663 pub fn set_template(&self, template: &str) {
665 if let Some((manager, state)) = self.get_manager_and_state() {
666 let mut state = state.lock().unwrap();
667 state.template = Template::new(template);
668 state.need_redraw = true;
669 std::mem::drop(state);
671 manager.mark_redraw();
672 manager.draw(false);
673 }
674 }
675
676 pub fn alive(&self) -> bool {
680 self.get_manager_and_state().is_some()
681 }
682}
683
684impl Drop for Bar {
685 fn drop(&mut self) {
687 if let Some((manager, _)) = self.get_manager_and_state() {
688 manager.states.lock().unwrap().remove(&self.id);
689 manager.mark_redraw();
690 manager.draw(true);
691 }
692 }
693}
694
695#[cfg(test)]
696mod tests {
697 use std::io::{Read, Seek};
698
699 use super::*;
700
701 #[test]
702 fn basic_test() {
703 let manager = Manager::new(std::time::Duration::from_secs(1));
704 let bar_1 = manager.create_bar(
705 100,
706 "Downloading",
707 "{msg}\n[{elapsed}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
708 true,
709 );
710 let bar_2 = manager.create_bar(
711 100,
712 "Uploading",
713 "{msg}\n[{elapsed}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
714 true,
715 );
716
717 bar_1.set_pos(50);
718 bar_2.set_pos(25);
719
720 std::mem::drop(bar_1);
721 std::mem::drop(bar_2);
722 }
723
724 #[test]
725 fn dont_crash_when_zero() {
726 let manager = Manager::new(std::time::Duration::from_secs(1));
727 let bar = manager.create_bar(
728 0,
729 "Downloading",
730 "{msg}\n[{elapsed}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
731 true,
732 );
733
734 bar.set_pos(0);
735 manager.draw(true);
736 }
737
738 #[test]
739 fn inc() {
740 let manager = Manager::new(std::time::Duration::from_secs(1));
741 let bar = manager.create_bar(
742 100,
743 "Downloading",
744 "{msg}\n[{elapsed}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
745 true,
746 );
747
748 bar.inc(10);
749 bar.inc(10);
750 bar.inc(10);
751 bar.inc(10);
752 bar.inc(10);
753
754 assert_eq!(bar.get_pos(), 50);
755
756 std::mem::drop(bar);
757 }
758
759 #[test]
760 fn visible() {
761 let manager = Manager::new(std::time::Duration::from_secs(1));
762 let bar = manager.create_bar(
763 100,
764 "Downloading",
765 "{msg}\n[{elapsed}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
766 true,
767 );
768
769 assert!(bar.is_visible());
770
771 bar.set_visible(false);
772 assert!(!bar.is_visible());
773
774 std::mem::drop(bar);
775 }
776
777 #[test]
778 fn ticker() {
779 let manager = Manager::new(std::time::Duration::from_secs(1));
780 manager.set_ticker(true);
781 let bar = manager.create_bar(
782 100,
783 "Downloading",
784 "{msg}\n[{elapsed}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
785 true,
786 );
787
788 std::thread::sleep(std::time::Duration::from_secs(2));
789 std::mem::drop(bar);
790 }
791
792 #[test]
793 fn alive() {
794 let manager = Manager::new(std::time::Duration::from_secs(1));
795 let bar = manager.create_bar(
796 100,
797 "Downloading",
798 "{msg}\n[{elapsed}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
799 true,
800 );
801
802 assert!(bar.alive());
803
804 std::mem::drop(manager);
805 assert!(!bar.alive());
806 }
807
808 #[cfg(target_os = "linux")]
809 #[test]
810 fn test_pb_to_file() {
811 const TEMPLATE_SIMPLE: &str = "{msg}\n{bytes}/{total_bytes}";
812 let memfd_name = std::ffi::CString::new("test_pb_to_file").unwrap();
813 let memfd_fd =
814 nix::sys::memfd::memfd_create(&memfd_name, nix::sys::memfd::MemFdCreateFlag::empty())
815 .unwrap();
816 let memfd_writer: std::fs::File = memfd_fd.into();
817 let mut memfd_writer_clone = memfd_writer.try_clone().unwrap();
818 let progressbar_manager =
819 Manager::new(std::time::Duration::from_secs(1)).with_file(memfd_writer);
820 let pb1 = progressbar_manager.create_bar(
821 10,
822 "Downloading http://d1.example.com/",
823 TEMPLATE_SIMPLE,
824 true,
825 );
826 let pb2 = progressbar_manager.create_bar(
827 10,
828 "Downloading http://d2.example.com/",
829 TEMPLATE_SIMPLE,
830 true,
831 );
832
833 pb1.set_pos(2);
834 pb2.set_pos(3);
835 progressbar_manager.draw(true);
836 pb1.set_pos(5);
837 pb2.set_pos(7);
838
839 std::mem::drop(progressbar_manager);
840 memfd_writer_clone
841 .seek(std::io::SeekFrom::Start(0))
842 .unwrap();
843 let mut output = String::new();
844 memfd_writer_clone.read_to_string(&mut output).unwrap();
845 assert_eq!(
846 output,
847 r#"Downloading http://d1.example.com/
8480 B/10 B
849Downloading http://d2.example.com/
8500 B/10 B
851Downloading http://d1.example.com/
8522 B/10 B
853Downloading http://d2.example.com/
8543 B/10 B
855Downloading http://d1.example.com/
8565 B/10 B
857Downloading http://d2.example.com/
8587 B/10 B
859"#
860 );
861 }
862}