use indicatif::{ProgressBar, ProgressStyle};
use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
pub trait Reporter {
fn step(&self, msg: &str);
fn done(&self, msg: &str);
fn suspend(&self, f: &mut dyn FnMut());
fn finish(&self);
}
thread_local! {
static ACTIVE: RefCell<Option<Rc<dyn Reporter>>> = const { RefCell::new(None) };
}
fn set_active(next: Option<Rc<dyn Reporter>>) -> Option<Rc<dyn Reporter>> {
ACTIVE.with(|slot| std::mem::replace(&mut *slot.borrow_mut(), next))
}
fn active() -> Option<Rc<dyn Reporter>> {
ACTIVE.with(|slot| slot.borrow().clone())
}
#[must_use = "dropping this immediately uninstalls the reporter; bind it, e.g. `let _progress = progress::install();`"]
pub struct Guard {
previous: Option<Rc<dyn Reporter>>,
}
impl Drop for Guard {
fn drop(&mut self) {
if let Some(outgoing) = set_active(self.previous.take()) {
outgoing.finish();
}
}
}
pub fn install_reporter(reporter: Rc<dyn Reporter>) -> Guard {
Guard { previous: set_active(Some(reporter)) }
}
pub fn install() -> Guard {
if console::Term::stderr().is_term() {
install_reporter(Rc::new(SpinnerReporter::new()))
} else {
install_reporter(Rc::new(PlainReporter))
}
}
pub fn step(msg: &str) {
if let Some(r) = active() {
r.step(msg);
}
}
pub fn done(msg: &str) {
if let Some(r) = active() {
r.done(msg);
}
}
pub fn clear() {
if let Some(r) = active() {
r.finish();
}
}
pub fn suspend<T>(f: impl FnOnce() -> T) -> T {
match active() {
None => f(),
Some(r) => {
let mut f = Some(f);
let mut out = None;
r.suspend(&mut || out = Some((f.take().expect("suspend ran twice"))()));
out.expect("reporter did not run the suspended closure")
}
}
}
struct SpinnerReporter {
pb: RefCell<ProgressBar>,
started: std::cell::Cell<bool>,
}
impl SpinnerReporter {
fn new_bar() -> ProgressBar {
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::with_template("{spinner:.cyan} {msg}")
.unwrap()
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"),
);
pb
}
fn new() -> Self {
Self { pb: RefCell::new(Self::new_bar()), started: std::cell::Cell::new(false) }
}
}
#[cfg(test)]
impl SpinnerReporter {
fn is_finished(&self) -> bool {
self.pb.borrow().is_finished()
}
}
impl Reporter for SpinnerReporter {
fn step(&self, msg: &str) {
if !self.started.get() {
self.pb.borrow().enable_steady_tick(Duration::from_millis(80));
self.started.set(true);
}
self.pb.borrow().set_message(msg.to_string());
}
fn done(&self, msg: &str) {
self.pb.borrow().println(format!("✓ {msg}"));
}
fn suspend(&self, f: &mut dyn FnMut()) {
self.pb.borrow().suspend(f);
}
fn finish(&self) {
self.pb.borrow().finish_and_clear();
*self.pb.borrow_mut() = Self::new_bar();
self.started.set(false);
}
}
struct PlainReporter;
impl Reporter for PlainReporter {
fn step(&self, _msg: &str) {}
fn done(&self, msg: &str) {
eprintln!("{msg}");
}
fn suspend(&self, f: &mut dyn FnMut()) {
f();
}
fn finish(&self) {}
}
#[cfg(test)]
#[derive(Default)]
pub struct RecordingReporter {
events: RefCell<Vec<String>>,
}
#[cfg(test)]
impl RecordingReporter {
pub fn events(&self) -> Vec<String> {
self.events.borrow().clone()
}
}
#[cfg(test)]
impl Reporter for RecordingReporter {
fn step(&self, msg: &str) {
self.events.borrow_mut().push(format!("step: {msg}"));
}
fn done(&self, msg: &str) {
self.events.borrow_mut().push(format!("done: {msg}"));
}
fn suspend(&self, f: &mut dyn FnMut()) {
f();
}
fn finish(&self) {}
}
#[cfg(test)]
pub fn record() -> (Rc<RecordingReporter>, Guard) {
let rec = Rc::new(RecordingReporter::default());
let guard = install_reporter(rec.clone());
(rec, guard)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn calls_are_noops_when_nothing_installed() {
step("working");
done("worked");
clear();
assert_eq!(suspend(|| 7), 7);
}
#[test]
fn recorder_captures_step_and_done_in_order() {
let (rec, _g) = record();
step("pulling pack a");
done("updated pack a");
assert_eq!(
rec.events().as_slice(),
&["step: pulling pack a", "done: updated pack a"]
);
}
#[test]
fn dropping_guard_restores_the_noop_slot() {
let rec = {
let (rec, _g) = record();
step("inside");
rec
};
step("outside");
assert_eq!(rec.events().as_slice(), &["step: inside"]);
}
#[test]
fn nested_install_replaces_then_restores_outer() {
let (outer, _g) = record();
{
let (inner, _g2) = record();
step("inner");
assert_eq!(inner.events().as_slice(), &["step: inner"]);
}
step("outer");
assert_eq!(outer.events().as_slice(), &["step: outer"]);
}
#[test]
fn spinner_does_not_tick_until_first_step() {
let r = SpinnerReporter::new();
assert!(!r.started.get());
r.step("go");
assert!(r.started.get());
}
#[test]
fn finish_resets_the_spinner_for_later_steps() {
let r = SpinnerReporter::new();
r.step("a");
r.finish();
r.step("b");
assert!(!r.is_finished());
}
#[test]
fn suspend_runs_closure_once_and_returns_its_value() {
let (_rec, _g) = record();
let mut calls = 0;
let out = suspend(|| {
calls += 1;
"yes"
});
assert_eq!(out, "yes");
assert_eq!(calls, 1);
}
}