[feat] add --no-progress
CLI option to disable progress bars and spinners
This commit is contained in:
12
src/lib.rs
12
src/lib.rs
@@ -19,6 +19,7 @@ use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
|||||||
static QUIET: AtomicBool = AtomicBool::new(false);
|
static QUIET: AtomicBool = AtomicBool::new(false);
|
||||||
static NO_INTERACTION: AtomicBool = AtomicBool::new(false);
|
static NO_INTERACTION: AtomicBool = AtomicBool::new(false);
|
||||||
static VERBOSE: AtomicU8 = AtomicU8::new(0);
|
static VERBOSE: AtomicU8 = AtomicU8::new(0);
|
||||||
|
static NO_PROGRESS: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
/// Set quiet mode: when true, non-interactive logs should be suppressed.
|
/// Set quiet mode: when true, non-interactive logs should be suppressed.
|
||||||
pub fn set_quiet(q: bool) {
|
pub fn set_quiet(q: bool) {
|
||||||
@@ -47,6 +48,15 @@ pub fn verbose_level() -> u8 {
|
|||||||
VERBOSE.load(Ordering::Relaxed)
|
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.
|
/// Check whether stdin is connected to a TTY. Used to avoid blocking prompts when not interactive.
|
||||||
pub fn stdin_is_tty() -> bool {
|
pub fn stdin_is_tty() -> bool {
|
||||||
#[cfg(unix)]
|
#[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.
|
/// Create a manager that enables bars when `n > 1`, stderr is a TTY, and not quiet.
|
||||||
pub fn default_for_files(n: usize) -> Self {
|
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)
|
Self::new(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -55,6 +55,10 @@ struct Args {
|
|||||||
#[arg(long = "no-interaction", global = true)]
|
#[arg(long = "no-interaction", global = true)]
|
||||||
no_interaction: bool,
|
no_interaction: bool,
|
||||||
|
|
||||||
|
/// Disable interactive progress indicators (bars/spinners)
|
||||||
|
#[arg(long = "no-progress", global = true)]
|
||||||
|
no_progress: bool,
|
||||||
|
|
||||||
/// Optional auxiliary subcommands (completions, man)
|
/// Optional auxiliary subcommands (completions, man)
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
aux: Option<AuxCommands>,
|
aux: Option<AuxCommands>,
|
||||||
@@ -218,6 +222,7 @@ fn run() -> Result<()> {
|
|||||||
polyscribe::set_verbose(args.verbose);
|
polyscribe::set_verbose(args.verbose);
|
||||||
polyscribe::set_quiet(args.quiet);
|
polyscribe::set_quiet(args.quiet);
|
||||||
polyscribe::set_no_interaction(args.no_interaction);
|
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
|
// Startup banner via UI (TTY-aware through cliclack), suppressed when quiet
|
||||||
polyscribe::ui::intro(format!("PolyScribe v{}", env!("CARGO_PKG_VERSION")));
|
polyscribe::ui::intro(format!("PolyScribe v{}", env!("CARGO_PKG_VERSION")));
|
||||||
|
@@ -14,6 +14,8 @@ use reqwest::blocking::Client;
|
|||||||
use reqwest::redirect::Policy;
|
use reqwest::redirect::Policy;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
use atty::Stream;
|
||||||
|
|
||||||
// --- Model downloader: list & download ggml models from Hugging Face ---
|
// --- 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()))?,
|
.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 hasher = Sha256::new();
|
||||||
|
let mut downloaded: u64 = 0;
|
||||||
let mut buf = [0u8; 1024 * 128];
|
let mut buf = [0u8; 1024 * 128];
|
||||||
|
let mut read_err: Option<anyhow::Error> = None;
|
||||||
loop {
|
loop {
|
||||||
let n = resp.read(&mut buf).context("Network read error")?;
|
let nres = resp.read(&mut buf);
|
||||||
if n == 0 {
|
match nres {
|
||||||
break;
|
Ok(n) => {
|
||||||
|
if n == 0 { break; }
|
||||||
|
hasher.update(&buf[..n]);
|
||||||
|
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; }
|
||||||
}
|
}
|
||||||
hasher.update(&buf[..n]);
|
|
||||||
file.write_all(&buf[..n]).context("Write error")?;
|
|
||||||
}
|
}
|
||||||
file.flush().ok();
|
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());
|
let got = to_hex_lower(&hasher.finalize());
|
||||||
if let Some(expected) = &entry.sha256 {
|
if let Some(expected) = &entry.sha256 {
|
||||||
if got != expected.to_lowercase() {
|
if got != expected.to_lowercase() {
|
||||||
|
if let Some(pb) = &pb_opt { pb.abandon_with_message("hash mismatch"); }
|
||||||
let _ = std::fs::remove_file(&tmp_path);
|
let _ = std::fs::remove_file(&tmp_path);
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"SHA-256 mismatch for {}: expected {}, got {}",
|
"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)
|
std::fs::rename(&tmp_path, &final_path)
|
||||||
.with_context(|| format!("Failed to move into place: {}", final_path.display()))?;
|
.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());
|
qlog!("Saved: {}", final_path.display());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -805,7 +834,17 @@ pub fn update_local_models() -> Result<()> {
|
|||||||
if let Some(remote) = map.get(&model_name) {
|
if let Some(remote) = map.get(&model_name) {
|
||||||
// If SHA256 available, verify and update if mismatch
|
// If SHA256 available, verify and update if mismatch
|
||||||
if let Some(expected) = &remote.sha256 {
|
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) => {
|
Ok(local_hash) => {
|
||||||
if local_hash.eq_ignore_ascii_case(expected) {
|
if local_hash.eq_ignore_ascii_case(expected) {
|
||||||
qlog!("{} is up-to-date.", fname);
|
qlog!("{} is up-to-date.", fname);
|
||||||
|
Reference in New Issue
Block a user