Skip to content

Commit

Permalink
feat(interactive): Add better progress bars (#1152)
Browse files Browse the repository at this point in the history
  • Loading branch information
aawsome committed May 13, 2024
1 parent cfd15ea commit fc8e3c5
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 28 deletions.
4 changes: 3 additions & 1 deletion src/commands/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand All @@ -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 };
Expand Down
88 changes: 61 additions & 27 deletions src/commands/tui/progress.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -48,7 +48,7 @@ impl ProgressBars for TuiProgressBars {
struct CounterData {
prefix: String,
begin: SystemTime,
length: u64,
length: Option<u64>,
count: u64,
}

Expand All @@ -57,7 +57,7 @@ impl CounterData {
Self {
prefix,
begin: SystemTime::now(),
length: 0,
length: None,
count: 0,
}
}
Expand All @@ -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();
}
}

Expand All @@ -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) {
Expand Down
14 changes: 14 additions & 0 deletions src/commands/tui/widgets.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod popup;
mod prompt;
mod select_table;
mod sized_gauge;
mod sized_paragraph;
mod sized_table;
mod text_input;
Expand All @@ -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::*;
Expand Down Expand Up @@ -75,3 +77,15 @@ pub type PopUpPrompt = Prompt<PopUpText>;
pub fn popup_prompt(title: &'static str, text: Text<'static>) -> PopUpPrompt {
Prompt(popup_text(title, text))
}

pub type PopUpGauge = PopUp<WithBlock<SizedGauge>>;
pub fn popup_gauge(
title: impl Into<Title<'static>>,
text: Span<'static>,
ratio: f64,
) -> PopUpGauge {
PopUp(WithBlock::new(
SizedGauge::new(text, ratio),
Block::bordered().title(title),
))
}
33 changes: 33 additions & 0 deletions src/commands/tui/widgets/sized_gauge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use super::*;

pub struct SizedGauge {
p: Gauge<'static>,
width: Option<u16>,
}

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<u16> {
self.width.map(|w| w + 10)
}
fn height(&self) -> Option<u16> {
Some(1)
}
}

impl Draw for SizedGauge {
fn draw(&mut self, area: Rect, f: &mut Frame<'_>) {
f.render_widget(&self.p, area);
}
}

0 comments on commit fc8e3c5

Please sign in to comment.