Skip to main content

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, 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}