use std::time::Duration;
use ratatui::widgets::{Block, Borders, Gauge};
use super::{Component, RenderContext};
#[derive(Clone, Debug, PartialEq)]
pub enum ProgressBarMessage {
SetProgress(f32),
Increment(f32),
Complete,
Reset,
SetEta(Option<Duration>),
SetRateText(Option<String>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProgressBarOutput {
Completed,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct ProgressBarState {
progress: f32,
label: Option<String>,
disabled: bool,
show_percentage: bool,
eta_millis: Option<u64>,
rate_text: Option<String>,
show_eta: bool,
show_rate: bool,
}
impl Default for ProgressBarState {
fn default() -> Self {
Self {
progress: 0.0,
label: None,
disabled: false,
show_percentage: true,
eta_millis: None,
rate_text: None,
show_eta: true,
show_rate: true,
}
}
}
impl ProgressBarState {
pub fn new() -> Self {
Self::default()
}
pub fn with_progress(progress: f32) -> Self {
Self {
progress: progress.clamp(0.0, 1.0),
..Self::default()
}
}
pub fn with_label(label: impl Into<String>) -> Self {
Self {
label: Some(label.into()),
..Self::default()
}
}
pub fn progress(&self) -> f32 {
self.progress
}
pub fn percentage(&self) -> u16 {
(self.progress * 100.0).round() as u16
}
pub fn set_progress(&mut self, progress: f32) {
self.progress = progress.clamp(0.0, 1.0);
}
pub fn is_complete(&self) -> bool {
self.progress >= 1.0
}
pub fn label(&self) -> Option<&str> {
self.label.as_deref()
}
pub fn set_label(&mut self, label: Option<String>) {
self.label = label;
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
pub fn set_disabled(&mut self, disabled: bool) {
self.disabled = disabled;
}
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn with_show_percentage(mut self, show: bool) -> Self {
self.show_percentage = show;
self
}
pub fn with_show_eta(mut self, show: bool) -> Self {
self.show_eta = show;
self
}
pub fn with_show_rate(mut self, show: bool) -> Self {
self.show_rate = show;
self
}
pub fn show_percentage(&self) -> bool {
self.show_percentage
}
pub fn show_eta(&self) -> bool {
self.show_eta
}
pub fn show_rate(&self) -> bool {
self.show_rate
}
pub fn set_show_percentage(&mut self, show: bool) {
self.show_percentage = show;
}
pub fn set_show_eta(&mut self, show: bool) {
self.show_eta = show;
}
pub fn set_show_rate(&mut self, show: bool) {
self.show_rate = show;
}
pub fn eta(&self) -> Option<Duration> {
self.eta_millis.map(Duration::from_millis)
}
pub fn eta_millis(&self) -> Option<u64> {
self.eta_millis
}
pub fn rate_text(&self) -> Option<&str> {
self.rate_text.as_deref()
}
pub fn set_eta(&mut self, eta: Option<Duration>) {
self.eta_millis = eta.map(|d| d.as_millis() as u64);
}
pub fn set_rate_text(&mut self, rate_text: Option<String>) {
self.rate_text = rate_text;
}
}
pub fn format_eta(duration: Duration) -> String {
let total_secs = duration.as_secs();
if total_secs < 60 {
format!("{}s", total_secs)
} else if total_secs < 3600 {
let mins = total_secs / 60;
let secs = total_secs % 60;
format!("{}m {:02}s", mins, secs)
} else {
let hours = total_secs / 3600;
let mins = (total_secs % 3600) / 60;
format!("{}h {:02}m", hours, mins)
}
}
pub struct ProgressBar;
impl Component for ProgressBar {
type State = ProgressBarState;
type Message = ProgressBarMessage;
type Output = ProgressBarOutput;
fn init() -> Self::State {
ProgressBarState::default()
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
let was_complete = state.is_complete();
match msg {
ProgressBarMessage::SetProgress(value) => {
state.set_progress(value);
}
ProgressBarMessage::Increment(amount) => {
state.set_progress(state.progress + amount);
}
ProgressBarMessage::Complete => {
state.progress = 1.0;
}
ProgressBarMessage::Reset => {
state.progress = 0.0;
state.eta_millis = None;
state.rate_text = None;
return None;
}
ProgressBarMessage::SetEta(eta) => {
state.eta_millis = eta.map(|d| d.as_millis() as u64);
return None;
}
ProgressBarMessage::SetRateText(text) => {
state.rate_text = text;
return None;
}
}
if state.is_complete() && (!was_complete || matches!(msg, ProgressBarMessage::Complete)) {
Some(ProgressBarOutput::Completed)
} else {
None
}
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
let label = build_label(state);
let gauge = Gauge::default()
.block(Block::default().borders(Borders::ALL))
.gauge_style(ctx.theme.progress_filled_style())
.percent(state.percentage())
.label(label.clone());
let annotation =
crate::annotation::Annotation::new(crate::annotation::WidgetType::Progress)
.with_id("progress_bar")
.with_label(label)
.with_value(format!("{}%", state.percentage()));
let annotated = crate::annotation::Annotate::new(gauge, annotation);
ctx.frame.render_widget(annotated, ctx.area);
}
}
fn build_label(state: &ProgressBarState) -> String {
let mut parts = Vec::new();
if let Some(l) = &state.label {
parts.push(l.clone());
}
if state.show_percentage {
parts.push(format!("{}%", state.percentage()));
}
if state.show_rate {
if let Some(rate) = &state.rate_text {
parts.push(format!("[{}]", rate));
}
}
if state.show_eta {
if let Some(millis) = state.eta_millis {
let duration = Duration::from_millis(millis);
parts.push(format!("ETA: {}", format_eta(duration)));
}
}
parts.join(" ")
}
#[cfg(test)]
mod tests;