[feat] add --no-progress CLI option to disable progress bars and spinners

This commit is contained in:
2025-08-12 08:28:48 +02:00
parent 041e504cb2
commit 9b4bd545dd
3 changed files with 61 additions and 7 deletions

View File

@@ -19,6 +19,7 @@ use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
static QUIET: AtomicBool = AtomicBool::new(false);
static NO_INTERACTION: AtomicBool = AtomicBool::new(false);
static VERBOSE: AtomicU8 = AtomicU8::new(0);
static NO_PROGRESS: AtomicBool = AtomicBool::new(false);
/// Set quiet mode: when true, non-interactive logs should be suppressed.
pub fn set_quiet(q: bool) {
@@ -47,6 +48,15 @@ pub fn verbose_level() -> u8 {
VERBOSE.load(Ordering::Relaxed)
}
/// Disable interactive progress indicators (bars/spinners)
pub fn set_no_progress(b: bool) {
NO_PROGRESS.store(b, Ordering::Relaxed);
}
/// Return current no-progress state
pub fn is_no_progress() -> bool {
NO_PROGRESS.load(Ordering::Relaxed)
}
/// Check whether stdin is connected to a TTY. Used to avoid blocking prompts when not interactive.
pub fn stdin_is_tty() -> bool {
#[cfg(unix)]
@@ -246,7 +256,7 @@ pub mod ui {
/// Create a manager that enables bars when `n > 1`, stderr is a TTY, and not quiet.
pub fn default_for_files(n: usize) -> Self {
let enabled = n > 1 && atty::is(Stream::Stderr) && !crate::is_quiet();
let enabled = n > 1 && atty::is(Stream::Stderr) && !crate::is_quiet() && !crate::is_no_progress();
Self::new(enabled)
}

View File

@@ -55,6 +55,10 @@ struct Args {
#[arg(long = "no-interaction", global = true)]
no_interaction: bool,
/// Disable interactive progress indicators (bars/spinners)
#[arg(long = "no-progress", global = true)]
no_progress: bool,
/// Optional auxiliary subcommands (completions, man)
#[command(subcommand)]
aux: Option<AuxCommands>,
@@ -218,6 +222,7 @@ fn run() -> Result<()> {
polyscribe::set_verbose(args.verbose);
polyscribe::set_quiet(args.quiet);
polyscribe::set_no_interaction(args.no_interaction);
polyscribe::set_no_progress(args.no_progress);
// Startup banner via UI (TTY-aware through cliclack), suppressed when quiet
polyscribe::ui::intro(format!("PolyScribe v{}", env!("CARGO_PKG_VERSION")));

View File

@@ -14,6 +14,8 @@ use reqwest::blocking::Client;
use reqwest::redirect::Policy;
use serde::Deserialize;
use sha2::{Digest, Sha256};
use indicatif::{ProgressBar, ProgressStyle};
use atty::Stream;
// --- Model downloader: list & download ggml models from Hugging Face ---
@@ -693,21 +695,47 @@ fn download_one_model(client: &Client, models_dir: &Path, entry: &ModelEntry) ->
.with_context(|| format!("Failed to create {}", tmp_path.display()))?,
);
// Set up progress bar if interactive and we know size
let show_progress = !crate::is_quiet() && !crate::is_no_progress() && atty::is(Stream::Stderr) && entry.size > 0;
let pb_opt = if show_progress {
let pb = ProgressBar::new(entry.size);
let style = ProgressStyle::with_template("Downloading {prefix} ({total_bytes}) [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({percent}%)")
.unwrap()
.progress_chars("=>-");
pb.set_style(style);
pb.set_prefix(format!("{}", entry.name));
Some(pb)
} else { None };
let mut hasher = Sha256::new();
let mut downloaded: u64 = 0;
let mut buf = [0u8; 1024 * 128];
let mut read_err: Option<anyhow::Error> = None;
loop {
let n = resp.read(&mut buf).context("Network read error")?;
if n == 0 {
break;
}
let nres = resp.read(&mut buf);
match nres {
Ok(n) => {
if n == 0 { break; }
hasher.update(&buf[..n]);
file.write_all(&buf[..n]).context("Write error")?;
if let Err(e) = file.write_all(&buf[..n]) { read_err = Some(anyhow!(e)); break; }
downloaded += n as u64;
if let Some(pb) = &pb_opt { pb.set_position(downloaded.min(entry.size)); }
}
Err(e) => { read_err = Some(anyhow!("Network read error: {}", e)); break; }
}
}
file.flush().ok();
if let Some(err) = read_err {
if let Some(pb) = &pb_opt { pb.abandon_with_message("download failed"); }
let _ = std::fs::remove_file(&tmp_path);
return Err(err);
}
let got = to_hex_lower(&hasher.finalize());
if let Some(expected) = &entry.sha256 {
if got != expected.to_lowercase() {
if let Some(pb) = &pb_opt { pb.abandon_with_message("hash mismatch"); }
let _ = std::fs::remove_file(&tmp_path);
return Err(anyhow!(
"SHA-256 mismatch for {}: expected {}, got {}",
@@ -728,6 +756,7 @@ fn download_one_model(client: &Client, models_dir: &Path, entry: &ModelEntry) ->
}
std::fs::rename(&tmp_path, &final_path)
.with_context(|| format!("Failed to move into place: {}", final_path.display()))?;
if let Some(pb) = &pb_opt { pb.finish_with_message("saved"); }
qlog!("Saved: {}", final_path.display());
Ok(())
}
@@ -805,7 +834,17 @@ pub fn update_local_models() -> Result<()> {
if let Some(remote) = map.get(&model_name) {
// If SHA256 available, verify and update if mismatch
if let Some(expected) = &remote.sha256 {
match compute_file_sha256_hex(&path) {
// Show a small spinner while verifying hash (TTY, not quiet, not no-progress)
let show_spin = !crate::is_quiet() && !crate::is_no_progress() && atty::is(Stream::Stderr);
let spinner = if show_spin {
let pb = ProgressBar::new_spinner();
pb.enable_steady_tick(std::time::Duration::from_millis(100));
pb.set_message(format!("Verifying {}", fname));
Some(pb)
} else { None };
let verify_res = compute_file_sha256_hex(&path);
if let Some(pb) = &spinner { pb.finish_and_clear(); }
match verify_res {
Ok(local_hash) => {
if local_hash.eq_ignore_ascii_case(expected) {
qlog!("{} is up-to-date.", fname);