rustic_rs/commands/tui/
progress.rs1use std::io::Stdout;
2use std::sync::{Arc, RwLock};
3use std::time::{Duration, SystemTime};
4
5use bytesize::ByteSize;
6use ratatui::{Terminal, backend::CrosstermBackend};
7use rustic_core::{Progress, ProgressBars, ProgressType, RusticProgress};
8
9use super::widgets::{Draw, popup_gauge, popup_text};
10
11#[derive(Debug, Clone)]
12pub struct TuiProgressBars {
13 pub terminal: Arc<RwLock<Terminal<CrosstermBackend<Stdout>>>>,
14}
15
16impl TuiProgressBars {
17 fn as_progress(&self, progress_type: ProgressType, prefix: String) -> Progress {
18 let progress = TuiProgress {
19 terminal: self.terminal.clone(),
20 data: Arc::new(RwLock::new(CounterData::new(prefix))),
21 progress_type,
22 };
23 progress.popup();
24 Progress::new(progress)
25 }
26}
27
28impl ProgressBars for TuiProgressBars {
29 fn progress(&self, progress_type: ProgressType, prefix: &str) -> Progress {
30 self.as_progress(progress_type, prefix.to_string())
31 }
32}
33
34#[derive(Debug)]
35struct CounterData {
36 prefix: String,
37 begin: SystemTime,
38 length: Option<u64>,
39 count: u64,
40}
41
42impl CounterData {
43 fn new(prefix: String) -> Self {
44 Self {
45 prefix,
46 begin: SystemTime::now(),
47 length: None,
48 count: 0,
49 }
50 }
51}
52
53#[derive(Clone, Debug)]
54pub struct TuiProgress {
55 terminal: Arc<RwLock<Terminal<CrosstermBackend<Stdout>>>>,
56 data: Arc<RwLock<CounterData>>,
57 progress_type: ProgressType,
58}
59
60fn fmt_duration(d: Duration) -> String {
61 let seconds = d.as_secs();
62 let (minutes, seconds) = (seconds / 60, seconds % 60);
63 let (hours, minutes) = (minutes / 60, minutes % 60);
64 format!("[{hours:02}:{minutes:02}:{seconds:02}]")
65}
66
67impl TuiProgress {
68 fn popup(&self) {
69 let data = self.data.read().unwrap();
70 let elapsed = data.begin.elapsed().unwrap();
71 let length = data.length;
72 let count = data.count;
73 let ratio = match length {
74 None | Some(0) => 0.0,
75 Some(l) => count as f64 / l as f64,
76 };
77 let eta = match ratio {
78 r if r < 0.01 => " ETA: -".to_string(),
79 r if r > 0.999_999 => String::new(),
80 r => {
81 format!(
82 " ETA: {}",
83 fmt_duration(Duration::from_secs(1) + elapsed.div_f64(r / (1.0 - r)))
84 )
85 }
86 };
87 let prefix = &data.prefix;
88 let message = match self.progress_type {
89 ProgressType::Spinner => {
90 format!("{} {prefix}", fmt_duration(elapsed))
91 }
92 ProgressType::Counter => {
93 format!(
94 "{} {prefix} {}{}{eta}",
95 fmt_duration(elapsed),
96 count,
97 length.map_or(String::new(), |l| format!("/{l}"))
98 )
99 }
100 ProgressType::Bytes => {
101 format!(
102 "{} {prefix} {}{}{eta}",
103 fmt_duration(elapsed),
104 ByteSize(count).display(),
105 length.map_or(String::new(), |l| format!("/{}", ByteSize(l).display()))
106 )
107 }
108 };
109 drop(data);
110
111 let mut terminal = self.terminal.write().unwrap();
112 _ = terminal
113 .draw(|f| {
114 let area = f.area();
115 match self.progress_type {
116 ProgressType::Spinner => {
117 let mut popup = popup_text("progress", message.into());
118 popup.draw(area, f);
119 }
120 ProgressType::Counter | ProgressType::Bytes => {
121 let mut popup = popup_gauge("progress", message.into(), ratio);
122 popup.draw(area, f);
123 }
124 }
125 })
126 .unwrap();
127 }
128}
129
130impl RusticProgress for TuiProgress {
131 fn is_hidden(&self) -> bool {
132 false
133 }
134 fn set_length(&self, len: u64) {
135 self.data.write().unwrap().length = Some(len);
136 self.popup();
137 }
138 fn set_title(&self, title: &str) {
139 self.data.write().unwrap().prefix = title.to_string();
140 self.popup();
141 }
142
143 fn inc(&self, inc: u64) {
144 self.data.write().unwrap().count += inc;
145 self.popup();
146 }
147 fn finish(&self) {}
148}