use console::{Emoji, style, truncate_str};
use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle};
use std::process::Output;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, BufReader, Lines};
use tokio::process::ChildStderr;
use tokio::time::Instant;
const NAME_WIDTH: usize = 30;
static COG: Emoji<'_, '_> = Emoji("\u{2699}\u{fe0f} ", ""); static MAG: Emoji<'_, '_> = Emoji("\u{1f50d} ", ""); static PACKAGE: Emoji<'_, '_> = Emoji("\u{1f4e6} ", ""); static BROOM: Emoji<'_, '_> = Emoji("\u{1f9f9} ", ""); static SPARKLE: Emoji<'_, '_> = Emoji("\u{2728} ", ":-) ");
pub fn step_header(step: u32, total: u32, description: &str) {
let emoji = match step {
1 => COG,
2 => MAG,
3 => PACKAGE,
4 => BROOM,
_ => COG,
};
println!(
"{} {}{}",
style(format!("[{}/{}]", step, total)).bold().dim(),
emoji,
description
);
}
pub fn done(instant: Instant) {
println!(
"\n{}Done in {}",
SPARKLE,
style(HumanDuration(instant.elapsed())).cyan()
);
}
#[derive(Clone)]
pub struct CommandStatus {
pub identifier: String,
pub success: bool,
pub error_message: String,
}
impl CommandStatus {
pub fn success(identifier: &str) -> Self {
CommandStatus {
identifier: identifier.to_string(),
success: true,
error_message: String::new(),
}
}
pub fn error(identifier: &str, error_message: &str) -> Self {
CommandStatus {
identifier: identifier.to_string(),
success: false,
error_message: error_message.to_string(),
}
}
pub fn from_result<T>(identifier: &str, result: &anyhow::Result<T>) -> Self {
match result {
Ok(_) => Self::success(identifier),
Err(e) => Self::error(identifier, &e.to_string()),
}
}
}
pub fn summary(count: usize, status: &[CommandStatus]) {
let failed: Vec<_> = status.iter().filter(|s| !s.success).collect();
if !failed.is_empty() {
println!();
for s in &failed {
println!(
" {} {} {}",
style("\u{2717}").red().bold(),
style(&s.identifier).cyan(),
style(&s.error_message).red()
);
}
println!(
"\n {} of {} resource(s) failed",
style(failed.len()).red().bold(),
style(count).cyan()
);
}
}
#[derive(Clone)]
pub struct Progress {
name: String,
bar: ProgressBar,
}
impl Progress {
pub fn join(multi_progress: &MultiProgress, name: &str) -> Progress {
let progress = Progress {
name: name.to_string(),
bar: Self::spinner(),
};
progress.bar.enable_steady_tick(Duration::from_millis(100));
multi_progress.add(progress.bar.clone());
progress.bar.set_message(style(name).cyan().to_string());
progress
}
pub fn hidden(name: &str) -> Progress {
Progress {
name: name.to_string(),
bar: ProgressBar::hidden(),
}
}
pub fn new(name: &str) -> Progress {
let progress = Progress {
name: name.to_string(),
bar: Self::spinner(),
};
progress.bar.enable_steady_tick(Duration::from_millis(100));
progress.bar.set_message(style(name).cyan().to_string());
progress
}
fn spinner() -> ProgressBar {
ProgressBar::new_spinner().with_style(
ProgressStyle::default_spinner()
.tick_strings(&[
"\u{280b}", "\u{2819}", "\u{2839}", "\u{2838}", "\u{283c}", "\u{2834}",
"\u{2826}", "\u{2827}", "\u{2807}", "\u{280f}", " ",
])
.template(" {spinner:.dim.bold} {wide_msg}")
.expect("Invalid spinner template"),
)
}
fn finished_style() -> ProgressStyle {
ProgressStyle::default_spinner()
.template(" {wide_msg}")
.expect("Invalid template")
}
pub async fn trace_progress<R>(&self, mut reader: Lines<BufReader<R>>)
where
R: tokio::io::AsyncRead + Unpin,
{
while let Some(line) = reader
.next_line()
.await
.expect("Unable to read output from command.")
{
self.show_progress(line.as_str());
}
}
pub fn finish_output(
&self,
output: std::io::Result<Output>,
status: Option<&str>,
) -> CommandStatus {
match output {
Ok(output) => {
if output.status.success() {
self.finish_success(status);
CommandStatus::success(&self.name)
} else {
let msg = String::from_utf8_lossy(&output.stderr).replace('\n', " ");
self.finish_error(&msg);
CommandStatus::error(&self.name, &msg)
}
}
Err(e) => {
let msg = e.to_string();
self.finish_error(&msg);
CommandStatus::error(&self.name, &msg)
}
}
}
pub fn show_progress(&self, text: &str) {
let padded = format!("{:<width$}", self.name, width = NAME_WIDTH);
self.bar.set_message(format!(
"{} {}",
style(padded).cyan(),
style(truncate_str(text, 80, "...")).dim()
));
}
pub fn finish_success(&self, status: Option<&str>) {
self.bar.set_style(Self::finished_style());
let padded = format!("{:<width$}", self.name, width = NAME_WIDTH);
let msg = match status {
Some(s) => format!(
"{} {} {}",
style("\u{2713}").green().bold(),
style(padded).cyan(),
style(s).dim()
),
None => format!(
"{} {}",
style("\u{2713}").green().bold(),
style(padded).cyan()
),
};
self.bar.finish_with_message(msg);
}
pub fn finish_error(&self, err: &str) {
self.bar.set_style(Self::finished_style());
let padded = format!("{:<width$}", self.name, width = NAME_WIDTH);
self.bar.abandon_with_message(format!(
"{} {} {}",
style("\u{2717}").red().bold(),
style(padded).cyan(),
style(err).red()
));
}
}
pub fn stderr_reader(child: &mut tokio::process::Child) -> Lines<BufReader<ChildStderr>> {
let stderr = child
.stderr
.take()
.expect("Command did not have a handle to stderr.");
BufReader::new(stderr).lines()
}