diff --git a/src/commands/tui.rs b/src/commands/tui.rs index 7fed8c6e5..47dbf4ba5 100644 --- a/src/commands/tui.rs +++ b/src/commands/tui.rs @@ -23,7 +23,7 @@ use crossterm::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::prelude::*; -use rustic_core::{IndexedFull, ProgressBars, SnapshotGroupCriterion}; +use rustic_core::{IndexedFull, Progress, ProgressBars, SnapshotGroupCriterion}; struct App<'a, P, S> { snapshots: Snapshots<'a, P, S>, @@ -46,7 +46,9 @@ pub fn run(group_by: SnapshotGroupCriterion) -> Result<()> { let progress = TuiProgressBars { terminal: terminal.clone(), }; + let p = progress.progress_spinner("starting rustic in interactive mode..."); let repo = open_repository_indexed_with_progress(&config.repository, progress)?; + p.finish(); // create app and run it let snapshots = Snapshots::new(&repo, config.snapshot_filter.clone(), group_by)?; let app = App { snapshots }; diff --git a/src/commands/tui/progress.rs b/src/commands/tui/progress.rs index 6852d7b25..164201630 100644 --- a/src/commands/tui/progress.rs +++ b/src/commands/tui/progress.rs @@ -1,12 +1,12 @@ use std::io::Stdout; use std::sync::{Arc, RwLock}; -use std::time::SystemTime; +use std::time::{Duration, SystemTime}; use bytesize::ByteSize; use ratatui::{backend::CrosstermBackend, Terminal}; use rustic_core::{Progress, ProgressBars}; -use super::widgets::{popup_text, Draw}; +use super::widgets::{popup_gauge, popup_text, Draw}; #[derive(Clone)] pub struct TuiProgressBars { @@ -48,7 +48,7 @@ impl ProgressBars for TuiProgressBars { struct CounterData { prefix: String, begin: SystemTime, - length: u64, + length: Option, count: u64, } @@ -57,7 +57,7 @@ impl CounterData { Self { prefix, begin: SystemTime::now(), - length: 0, + length: None, count: 0, } } @@ -78,44 +78,78 @@ pub struct TuiProgress { progress_type: TuiProgressType, } +fn fmt_duration(d: Duration) -> String { + let seconds = d.as_secs(); + let (minutes, seconds) = (seconds / 60, seconds % 60); + let (hours, minutes) = (minutes / 60, minutes % 60); + format!("[{hours:02}:{minutes:02}:{seconds:02}]") +} + impl TuiProgress { fn popup(&self) { let data = self.data.read().unwrap(); - let seconds = data.begin.elapsed().unwrap().as_secs(); - let (minutes, seconds) = (seconds / 60, seconds % 60); - let (hours, minutes) = (minutes / 60, minutes % 60); + let elapsed = data.begin.elapsed().unwrap(); + let length = data.length; + let count = data.count; + let ratio = match length { + None | Some(0) => 0.0, + Some(l) => count as f64 / l as f64, + }; + let eta = match ratio { + r if r < 0.01 => " ETA: -".to_string(), + r if r > 0.999999 => String::new(), + r => { + format!( + " ETA: {}", + fmt_duration(Duration::from_secs(1) + elapsed.div_f64(r / (1.0 - r))) + ) + } + }; + let prefix = &data.prefix; let message = match self.progress_type { TuiProgressType::Spinner => { - format!("[{hours:02}:{minutes:02}:{seconds:02}]") + format!("{} {prefix}", fmt_duration(elapsed)) } TuiProgressType::Counter => { format!( - "[{hours:02}:{minutes:02}:{seconds:02}] {}/{}", - data.count, data.length + "{} {prefix} {}{}{eta}", + fmt_duration(elapsed), + count, + length.map_or(String::new(), |l| format!("/{l}")) ) } TuiProgressType::Bytes => { format!( - "[{hours:02}:{minutes:02}:{seconds:02}] {}/{}", - ByteSize(data.count).to_string_as(true), - ByteSize(data.length).to_string_as(true) + "{} {prefix} {}{}{eta}", + fmt_duration(elapsed), + ByteSize(count).to_string_as(true), + length.map_or(String::new(), |l| format!( + "/{}", + ByteSize(l).to_string_as(true) + )) ) } TuiProgressType::Hidden => String::new(), - } - .into(); + }; + drop(data); - if !matches!(self.progress_type, TuiProgressType::Hidden) { - let mut popup = popup_text(data.prefix.clone(), message); - drop(data); - let mut terminal = self.terminal.write().unwrap(); - _ = terminal - .draw(|f| { - let area = f.size(); - popup.draw(area, f); - }) - .unwrap(); - } + let mut terminal = self.terminal.write().unwrap(); + _ = terminal + .draw(|f| { + let area = f.size(); + match self.progress_type { + TuiProgressType::Hidden => {} + TuiProgressType::Spinner => { + let mut popup = popup_text("progress", message.into()); + popup.draw(area, f); + } + TuiProgressType::Counter | TuiProgressType::Bytes => { + let mut popup = popup_gauge("progress", message.into(), ratio); + popup.draw(area, f); + } + } + }) + .unwrap(); } } @@ -124,7 +158,7 @@ impl Progress for TuiProgress { matches!(self.progress_type, TuiProgressType::Hidden) } fn set_length(&self, len: u64) { - self.data.write().unwrap().length = len; + self.data.write().unwrap().length = Some(len); self.popup(); } fn set_title(&self, title: &'static str) { diff --git a/src/commands/tui/widgets.rs b/src/commands/tui/widgets.rs index a6081f904..f15e43b47 100644 --- a/src/commands/tui/widgets.rs +++ b/src/commands/tui/widgets.rs @@ -1,6 +1,7 @@ mod popup; mod prompt; mod select_table; +mod sized_gauge; mod sized_paragraph; mod sized_table; mod text_input; @@ -10,6 +11,7 @@ pub use popup::*; pub use prompt::*; use ratatui::widgets::block::Title; pub use select_table::*; +pub use sized_gauge::*; pub use sized_paragraph::*; pub use sized_table::*; pub use text_input::*; @@ -75,3 +77,15 @@ pub type PopUpPrompt = Prompt; pub fn popup_prompt(title: &'static str, text: Text<'static>) -> PopUpPrompt { Prompt(popup_text(title, text)) } + +pub type PopUpGauge = PopUp>; +pub fn popup_gauge( + title: impl Into>, + text: Span<'static>, + ratio: f64, +) -> PopUpGauge { + PopUp(WithBlock::new( + SizedGauge::new(text, ratio), + Block::bordered().title(title), + )) +} diff --git a/src/commands/tui/widgets/sized_gauge.rs b/src/commands/tui/widgets/sized_gauge.rs new file mode 100644 index 000000000..e84738690 --- /dev/null +++ b/src/commands/tui/widgets/sized_gauge.rs @@ -0,0 +1,33 @@ +use super::*; + +pub struct SizedGauge { + p: Gauge<'static>, + width: Option, +} + +impl SizedGauge { + pub fn new(text: Span<'static>, ratio: f64) -> Self { + let width = text.width().try_into().ok(); + let p = Gauge::default() + .gauge_style(Style::default().fg(Color::Blue)) + .use_unicode(true) + .label(text) + .ratio(ratio); + Self { p, width } + } +} + +impl SizedWidget for SizedGauge { + fn width(&self) -> Option { + self.width.map(|w| w + 10) + } + fn height(&self) -> Option { + Some(1) + } +} + +impl Draw for SizedGauge { + fn draw(&mut self, area: Rect, f: &mut Frame<'_>) { + f.render_widget(&self.p, area); + } +}