use std::io::IsTerminal;
use std::marker::PhantomData;
use std::sync::Arc;
use std::time::Duration;
use indicatif::{ProgressBar as IndProgressBar, ProgressStyle};
use super::Role;
use super::renderer::{Renderer, Writer};
use super::status_builder::StatusBuilder;
pub(crate) fn stderr_is_terminal() -> bool {
std::io::stderr().is_terminal()
}
pub struct Spinner<'p> {
pub(crate) renderer: Arc<Renderer>,
pub(crate) sink: Arc<dyn Writer>,
pub(crate) depth: usize,
pub(crate) bar: IndProgressBar,
pub(crate) message: String,
pub(crate) finished: bool,
pub(crate) _phantom: PhantomData<&'p ()>,
}
impl<'p> Spinner<'p> {
pub fn set_message(&self, text: impl Into<String>) {
self.bar.set_message(text.into());
}
pub fn finish_ok(self, final_text: impl Into<String>) -> StatusBuilder<'p> {
self.finish_with(Role::Ok, final_text)
}
pub fn finish_warn(self, final_text: impl Into<String>) -> StatusBuilder<'p> {
self.finish_with(Role::Warn, final_text)
}
pub fn finish_fail(self, final_text: impl Into<String>) -> StatusBuilder<'p> {
self.finish_with(Role::Fail, final_text)
}
pub fn finish_skipped(self, final_text: impl Into<String>) -> StatusBuilder<'p> {
self.finish_with(Role::Skipped, final_text)
}
fn finish_with(mut self, role: Role, subject: impl Into<String>) -> StatusBuilder<'p> {
self.bar.finish_and_clear();
self.finished = true;
StatusBuilder::new(
self.renderer.clone(),
self.sink.clone(),
self.depth,
role,
subject,
)
}
}
impl Drop for Spinner<'_> {
fn drop(&mut self) {
if self.finished {
return;
}
self.bar.finish_and_clear();
let msg = std::mem::take(&mut self.message);
let sb = StatusBuilder::new(
self.renderer.clone(),
self.sink.clone(),
self.depth,
Role::Info,
msg,
);
drop(sb);
}
}
pub struct ProgressBar<'p> {
pub(crate) bar: IndProgressBar,
pub(crate) _phantom: PhantomData<&'p ()>,
}
impl<'p> ProgressBar<'p> {
pub fn inc(&self, delta: u64) {
self.bar.inc(delta);
}
pub fn set_position(&self, pos: u64) {
self.bar.set_position(pos);
}
pub fn set_message(&self, m: impl Into<String>) {
self.bar.set_message(m.into());
}
pub fn finish(self) {
self.bar.finish_and_clear();
}
}
pub(crate) fn make_spinner_bar(
multi: &indicatif::MultiProgress,
renderer: &Renderer,
verbosity: super::Verbosity,
message: &str,
) -> IndProgressBar {
if verbosity == super::Verbosity::Quiet || !stderr_is_terminal() {
IndProgressBar::hidden()
} else {
build_spinner(multi, renderer, message)
}
}
pub(crate) fn make_progress_bar(
multi: &indicatif::MultiProgress,
total: u64,
verbosity: super::Verbosity,
message: &str,
) -> IndProgressBar {
if verbosity == super::Verbosity::Quiet || !stderr_is_terminal() {
IndProgressBar::hidden()
} else {
build_progress_bar(multi, total, message)
}
}
pub(crate) fn build_spinner(
multi: &indicatif::MultiProgress,
renderer: &Renderer,
message: &str,
) -> IndProgressBar {
let pb = multi.add(IndProgressBar::new_spinner());
let frames_raw = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
let styled: Vec<String> = frames_raw
.iter()
.map(|f| renderer.theme.info.apply_to(f).to_string())
.collect();
let mut tick_refs: Vec<&str> = styled.iter().map(|s| s.as_str()).collect();
tick_refs.push(" ");
pb.set_style(
ProgressStyle::with_template("{spinner} {msg}")
.unwrap_or_else(|_| ProgressStyle::default_spinner())
.tick_strings(&tick_refs),
);
pb.set_message(message.to_string());
pb.enable_steady_tick(Duration::from_millis(80));
pb
}
pub(crate) fn build_progress_bar(
multi: &indicatif::MultiProgress,
total: u64,
message: &str,
) -> IndProgressBar {
let pb = multi.add(IndProgressBar::new(total));
pb.set_style(
ProgressStyle::with_template("{spinner:.cyan} [{bar:30.cyan/dim}] {pos}/{len} {msg}")
.unwrap_or_else(|_| ProgressStyle::default_bar())
.progress_chars("━╸─"),
);
pb.set_message(message.to_string());
pb
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use super::super::renderer::{Renderer, StringSink};
use super::super::{Theme, Verbosity};
use super::*;
use crate::output::strip_ansi;
fn renderer() -> Arc<Renderer> {
Arc::new(Renderer::new(Theme::default(), Verbosity::Normal))
}
fn sink_for(buf: &Arc<Mutex<String>>) -> Arc<dyn Writer> {
Arc::new(StringSink(buf.clone()))
}
#[test]
fn finish_ok_emits_status_at_section_depth() {
let r = renderer();
let buf = Arc::new(Mutex::new(String::new()));
let sink = sink_for(&buf);
let sp = Spinner {
renderer: r.clone(),
sink: sink.clone(),
depth: 1,
bar: indicatif::ProgressBar::hidden(),
message: "doing work".into(),
finished: false,
_phantom: std::marker::PhantomData,
};
let _ = sp.finish_ok("done");
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains(" ✓ done"), "got: {out:?}");
}
#[test]
fn drop_without_finish_emits_info_record() {
let r = renderer();
let buf = Arc::new(Mutex::new(String::new()));
let sink = sink_for(&buf);
{
let _sp = Spinner {
renderer: r.clone(),
sink: sink.clone(),
depth: 0,
bar: indicatif::ProgressBar::hidden(),
message: "abandoned".into(),
finished: false,
_phantom: std::marker::PhantomData,
};
}
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("abandoned"), "got: {out:?}");
}
#[test]
fn quiet_printer_returns_hidden_spinner() {
use super::super::printer::Printer;
let p = Printer::with_format(
super::super::Verbosity::Quiet,
None,
super::super::OutputFormat::Table,
);
let sp = p.spinner("x");
assert!(sp.bar.is_hidden(), "Quiet should yield a hidden bar");
}
}