aim/crates/aim-cli/src/ui/progress.rs

155 lines
5.1 KiB
Rust

use std::io::IsTerminal;
use std::time::Duration;
use aim_core::app::progress::{OperationEvent, OperationKind, OperationStage, ProgressReporter};
use indicatif::{ProgressBar, ProgressStyle};
pub fn new_progress_bar(total: Option<u64>) -> ProgressBar {
match total {
Some(total) => ProgressBar::new(total),
None => ProgressBar::new_spinner(),
}
}
pub fn spinner_style() -> ProgressStyle {
ProgressStyle::with_template("{spinner} {msg}").expect("spinner template is valid")
}
pub fn byte_style() -> ProgressStyle {
ProgressStyle::with_template("{bar:32.cyan/blue} {bytes}/{total_bytes} {msg}")
.expect("byte progress template is valid")
}
pub fn operation_label(kind: OperationKind) -> &'static str {
match kind {
OperationKind::Add => "Installing",
OperationKind::Search => "Searching",
OperationKind::UpdateBatch => "Updating",
OperationKind::UpdateItem => "Updating",
OperationKind::Remove => "Removing",
}
}
pub fn stage_label(stage: OperationStage) -> &'static str {
match stage {
OperationStage::ResolveQuery => "Resolving source",
OperationStage::DiscoverRelease => "Discovering release",
OperationStage::SelectArtifact => "Selecting artifact",
OperationStage::DownloadArtifact => "Downloading artifact",
OperationStage::StagePayload => "Staging payload",
OperationStage::WriteDesktopEntry => "Writing desktop entry",
OperationStage::ExtractIcon => "Extracting icon",
OperationStage::RefreshIntegration => "Refreshing desktop integration",
OperationStage::SaveRegistry => "Saving registry",
OperationStage::Finalize => "Finalizing",
}
}
pub fn event_message(event: &OperationEvent) -> Option<String> {
match event {
OperationEvent::Started { kind, label } => {
Some(format!("{} {label}", operation_label(*kind)))
}
OperationEvent::StageChanged { stage, message } => {
let title = stage_label(*stage);
if title.eq_ignore_ascii_case(message) {
Some(title.to_owned())
} else {
Some(format!("{title}: {message}"))
}
}
OperationEvent::Progress { .. } => None,
OperationEvent::Warning { message } => Some(format!("Warning: {message}")),
OperationEvent::Finished { summary } => Some(summary.clone()),
OperationEvent::Failed { stage, reason } => {
Some(format!("{} failed: {reason}", stage_label(*stage)))
}
}
}
pub struct TerminalProgressReporter {
interactive: bool,
progress_bar: Option<ProgressBar>,
byte_total: Option<u64>,
}
impl TerminalProgressReporter {
pub fn stderr() -> Self {
Self {
interactive: std::io::stderr().is_terminal(),
progress_bar: None,
byte_total: None,
}
}
fn clear_progress(&mut self) {
if let Some(progress_bar) = self.progress_bar.take() {
progress_bar.finish_and_clear();
}
self.byte_total = None;
}
fn show_spinner(&mut self, message: String) {
if !self.interactive {
eprintln!("{message}");
return;
}
let progress_bar = self.progress_bar.get_or_insert_with(|| {
let progress_bar = new_progress_bar(None);
progress_bar.set_style(spinner_style());
progress_bar.enable_steady_tick(Duration::from_millis(100));
progress_bar
});
progress_bar.set_message(message);
self.byte_total = None;
}
fn show_progress(&mut self, current: u64, total: Option<u64>) {
if !self.interactive {
return;
}
let total = total.unwrap_or_else(|| current.max(1));
let replace_progress = self.byte_total != Some(total);
if replace_progress {
self.clear_progress();
let progress_bar = new_progress_bar(Some(total));
progress_bar.set_style(byte_style());
self.progress_bar = Some(progress_bar);
self.byte_total = Some(total);
}
if let Some(progress_bar) = &self.progress_bar {
progress_bar.set_length(total);
progress_bar.set_position(current.min(total));
}
}
}
impl Default for TerminalProgressReporter {
fn default() -> Self {
Self::stderr()
}
}
impl ProgressReporter for TerminalProgressReporter {
fn report(&mut self, event: &OperationEvent) {
match event {
OperationEvent::Started { .. } | OperationEvent::StageChanged { .. } => {
if let Some(message) = event_message(event) {
self.show_spinner(message);
}
}
OperationEvent::Progress { current, total } => self.show_progress(*current, *total),
OperationEvent::Warning { .. } | OperationEvent::Failed { .. } => {
self.clear_progress();
if let Some(message) = event_message(event) {
eprintln!("{message}");
}
}
OperationEvent::Finished { .. } => self.clear_progress(),
}
}
}