use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
use std::sync::OnceLock;
use std::time::Duration;
static MUTED: OnceLock<bool> = OnceLock::new();
pub fn is_muted() -> bool {
*MUTED.get_or_init(compute_muted)
}
fn compute_muted() -> bool {
if std::env::var("JARVY_NO_PROGRESS").as_deref() == Ok("1") {
return true;
}
if std::env::var("JARVY_QUIET").as_deref() == Ok("1") {
return true;
}
if !is_stdout_tty() {
return true;
}
let mut prev_was_format_flag = false;
for a in std::env::args() {
if a == "--quiet" || a == "-q" {
return true;
}
if a == "--json" || a.starts_with("--format=json") || a.starts_with("--log-format=json") {
return true;
}
if prev_was_format_flag && a == "json" {
return true;
}
prev_was_format_flag = a == "--format" || a == "--log-format";
}
crate::sandbox::is_seamless_auto()
}
fn is_stdout_tty() -> bool {
use std::io::IsTerminal;
std::io::stdout().is_terminal()
}
pub struct Progress {
multi: MultiProgress,
pub plain: bool,
}
impl Progress {
pub fn start() -> Self {
let muted = is_muted();
let plain = muted && crate::sandbox::is_seamless_auto();
let multi = if muted {
MultiProgress::with_draw_target(ProgressDrawTarget::hidden())
} else {
MultiProgress::new()
};
Progress { multi, plain }
}
pub fn add(&self, prefix: impl Into<String>, message: impl Into<String>) -> Spinner {
let prefix = prefix.into();
let message = message.into();
if self.plain {
println!("{} {}", prefix, message);
return Spinner {
bar: None,
plain_prefix: prefix,
};
}
let bar = self.multi.add(ProgressBar::new_spinner());
bar.set_style(default_spinner_style());
bar.set_prefix(prefix.clone());
bar.set_message(message);
bar.enable_steady_tick(Duration::from_millis(120));
Spinner {
bar: Some(bar),
plain_prefix: prefix,
}
}
pub fn println(&self, msg: impl AsRef<str>) {
if self.plain || is_muted() {
println!("{}", msg.as_ref());
} else {
let _ = self.multi.println(msg.as_ref());
}
}
}
pub struct Spinner {
bar: Option<ProgressBar>,
plain_prefix: String,
}
impl Spinner {
pub fn set_message(&self, message: impl Into<String>) {
if let Some(bar) = &self.bar {
bar.set_message(message.into());
}
}
pub fn finish_ok(self, message: impl Into<String>) {
let message = message.into();
match self.bar {
Some(bar) => {
bar.set_style(done_style("✓"));
bar.finish_with_message(message);
}
None => {
println!("{} ✓ {}", self.plain_prefix, message);
}
}
}
pub fn finish_skipped(self, message: impl Into<String>) {
let message = message.into();
match self.bar {
Some(bar) => {
bar.set_style(done_style("·"));
bar.finish_with_message(message);
}
None => {
println!("{} · {}", self.plain_prefix, message);
}
}
}
pub fn finish_failed(self, message: impl Into<String>) {
let message = message.into();
match self.bar {
Some(bar) => {
bar.set_style(done_style("✗"));
bar.finish_with_message(message);
}
None => {
println!("{} ✗ {}", self.plain_prefix, message);
}
}
}
}
fn default_spinner_style() -> ProgressStyle {
ProgressStyle::with_template("{prefix:.bold.dim} {spinner:.cyan} {msg}")
.expect("static template")
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"])
}
fn done_style(mark: &'static str) -> ProgressStyle {
let template = format!("{{prefix:.bold.dim}} {mark} {{msg}}");
ProgressStyle::with_template(&template).expect("done template")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn progress_construction_does_not_panic_when_muted() {
#[allow(unsafe_code)]
unsafe {
std::env::set_var("JARVY_NO_PROGRESS", "1");
}
let p = Progress::start();
let s = p.add("[1/1]", "test op");
s.finish_ok("done");
}
}