rustic_rs/commands/tui/
progress.rs

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