[refactor] propagate no-progress and no-interaction flags, enhance prompt handling, and update progress bar logic with cliclack

This commit is contained in:
2025-08-14 10:34:52 +02:00
parent 9841550dcc
commit 0573369b81
7 changed files with 207 additions and 43 deletions

View File

@@ -17,7 +17,6 @@ use std::collections::BTreeSet;
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::thread;
use std::time::{Duration, Instant};
fn format_size_mb(size: Option<u64>) -> String {
@@ -712,16 +711,7 @@ fn download_with_progress(dest_path: &Path, entry: &ModelEntry) -> Result<()> {
crate::ui::info(format!("Download: {}", part_path.display()));
let pb_total = total_len.unwrap_or(0);
let mut bar = None;
let mut sp: Option<crate::ui::Spinner> = None;
if pb_total > 0 {
let mut b = cliclack::progress_bar(pb_total);
b.start("Downloading");
if resume_from > 0 { b.inc(resume_from); }
bar = Some(b);
} else {
sp = Some(crate::ui::Spinner::start("Downloading"));
}
let mut bar = crate::ui::BytesProgress::start(pb_total, "Downloading", resume_from);
let start = Instant::now();
let mut resp = req.send()?.error_for_status()?;
@@ -744,12 +734,8 @@ fn download_with_progress(dest_path: &Path, entry: &ModelEntry) -> Result<()> {
// Plain GET without conditional headers
let mut req2 = client.get(url);
resp = req2.send()?.error_for_status()?;
if let Some(b) = bar.as_mut() {
b.stop("restarting");
}
let mut b2 = cliclack::progress_bar(pb_total);
b2.start("Downloading");
bar = Some(b2);
bar.stop("restarting");
bar = crate::ui::BytesProgress::start(pb_total, "Downloading", 0);
// Reopen the part file since we dropped it
part_file = OpenOptions::new()
@@ -770,23 +756,13 @@ fn download_with_progress(dest_path: &Path, entry: &ModelEntry) -> Result<()> {
break;
}
part_file.write_all(&buf[..read])?;
if pb_total > 0 {
if let Some(b) = bar.as_mut() {
b.inc(read as u64);
}
} else {
// spinner: nothing to update per chunk beyond the animation
}
bar.inc(read as u64);
}
part_file.flush()?;
part_file.sync_all()?;
}
if pb_total > 0 {
if let Some(b) = bar.as_mut() { b.stop("done"); }
} else {
if let Some(s) = sp.take() { s.success("done"); }
}
bar.stop("done");
if let Some(expected_hex) = entry.sha256.as_deref() {
crate::ui::info("Verify: SHA-256");

View File

@@ -8,6 +8,7 @@
pub mod progress;
use std::io;
use std::io::IsTerminal;
/// Log an informational message.
pub fn info(msg: impl AsRef<str>) {
@@ -57,6 +58,9 @@ pub fn println_above_bars(line: impl AsRef<str>) {
/// Returns default if provided and user enters empty string.
/// In non-interactive workflows, callers should skip prompt based on their flags.
pub fn prompt_input(prompt: &str, default: Option<&str>) -> io::Result<String> {
if crate::is_no_interaction() || !crate::stdin_is_tty() {
return Ok(default.unwrap_or("").to_string());
}
let mut q = cliclack::input(prompt);
if let Some(def) = default { q = q.default_input(def); }
q.interact().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
@@ -64,6 +68,9 @@ pub fn prompt_input(prompt: &str, default: Option<&str>) -> io::Result<String> {
/// Present a single-choice selector and return the selected index.
pub fn prompt_select<'a>(prompt: &str, items: &[&'a str]) -> io::Result<usize> {
if crate::is_no_interaction() || !crate::stdin_is_tty() {
return Err(io::Error::new(io::ErrorKind::Other, "interactive prompt disabled"));
}
let mut sel = cliclack::select::<usize>(prompt);
for (idx, label) in items.iter().enumerate() {
sel = sel.item(idx, *label, "");
@@ -74,6 +81,9 @@ pub fn prompt_select<'a>(prompt: &str, items: &[&'a str]) -> io::Result<usize> {
/// Present a multi-choice selector and return indices of selected items.
pub fn prompt_multi_select<'a>(prompt: &str, items: &[&'a str], defaults: Option<&[bool]>) -> io::Result<Vec<usize>> {
if crate::is_no_interaction() || !crate::stdin_is_tty() {
return Err(io::Error::new(io::ErrorKind::Other, "interactive prompt disabled"));
}
let mut ms = cliclack::multiselect::<usize>(prompt);
for (idx, label) in items.iter().enumerate() {
ms = ms.item(idx, *label, "");
@@ -92,6 +102,41 @@ pub fn prompt_multi_select<'a>(prompt: &str, items: &[&'a str], defaults: Option
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
/// Confirm prompt with default, respecting non-interactive mode.
pub fn prompt_confirm(prompt: &str, default: bool) -> io::Result<bool> {
if crate::is_no_interaction() || !crate::stdin_is_tty() {
return Ok(default);
}
let mut q = cliclack::confirm(prompt);
// If `cliclack::confirm` lacks default, we simply ask; caller can handle ESC/cancel if needed.
q.interact().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
/// Read a secret/password without echoing, respecting non-interactive mode.
pub fn prompt_password(prompt: &str) -> io::Result<String> {
if crate::is_no_interaction() || !crate::stdin_is_tty() {
return Err(io::Error::new(io::ErrorKind::Other, "password prompt disabled in non-interactive mode"));
}
let mut q = cliclack::password(prompt);
q.interact().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
/// Input with validation closure; on non-interactive returns default or error when no default.
pub fn prompt_input_validated<F>(prompt: &str, default: Option<&str>, validate: F) -> io::Result<String>
where
F: Fn(&str) -> Result<(), String> + 'static,
{
if crate::is_no_interaction() || !crate::stdin_is_tty() {
if let Some(def) = default { return Ok(def.to_string()); }
return Err(io::Error::new(io::ErrorKind::Other, "interactive prompt disabled"));
}
let mut q = cliclack::input(prompt);
if let Some(def) = default { q = q.default_input(def); }
q.validate(move |s: &String| validate(s))
.interact()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
/// A simple spinner wrapper built on top of `cliclack::spinner()`.
///
/// This wrapper provides a minimal API with start/stop/success/error methods
@@ -101,24 +146,75 @@ pub struct Spinner(cliclack::ProgressBar);
impl Spinner {
/// Creates and starts a new spinner with the provided status text.
pub fn start(text: impl AsRef<str>) -> Self {
let s = cliclack::spinner();
s.start(text.as_ref());
Self(s)
if crate::is_no_progress() || crate::is_no_interaction() || !std::io::stderr().is_terminal() {
// Fallback: no spinner, but log start
let _ = cliclack::log::info(text.as_ref());
let s = cliclack::spinner();
Self(s)
} else {
let s = cliclack::spinner();
s.start(text.as_ref());
Self(s)
}
}
/// Stops the spinner with a submitted/completed style and message.
pub fn stop(self, text: impl AsRef<str>) {
let s = self.0;
s.stop(text.as_ref());
if crate::is_no_progress() {
let _ = cliclack::log::info(text.as_ref());
} else {
s.stop(text.as_ref());
}
}
/// Marks the spinner as successfully finished (alias for `stop`).
pub fn success(self, text: impl AsRef<str>) {
let s = self.0;
// cliclack progress bar uses `stop` for successful completion styling
s.stop(text.as_ref());
if crate::is_no_progress() {
let _ = cliclack::log::success(text.as_ref());
} else {
s.stop(text.as_ref());
}
}
/// Marks the spinner as failed with an error style and message.
pub fn error(self, text: impl AsRef<str>) {
let s = self.0;
s.error(text.as_ref());
if crate::is_no_progress() {
let _ = cliclack::log::error(text.as_ref());
} else {
s.error(text.as_ref());
}
}
}
/// Byte-count progress bar that respects `--no-progress` and TTY state.
pub struct BytesProgress(Option<cliclack::ProgressBar>);
impl BytesProgress {
/// Start a new progress bar with a total and initial position.
pub fn start(total: u64, text: &str, initial: u64) -> Self {
if crate::is_no_progress() || crate::is_no_interaction() || !std::io::stderr().is_terminal() || total == 0 {
let _ = cliclack::log::info(text);
return Self(None);
}
let mut b = cliclack::progress_bar(total);
b.start(text);
if initial > 0 { b.inc(initial); }
Self(Some(b))
}
/// Increment by delta bytes.
pub fn inc(&mut self, delta: u64) {
if let Some(b) = self.0.as_mut() { b.inc(delta); }
}
/// Stop with a message.
pub fn stop(mut self, text: &str) {
if let Some(b) = self.0.take() { b.stop(text); } else { let _ = cliclack::log::info(text); }
}
/// Mark as error with a message.
pub fn error(mut self, text: &str) {
if let Some(b) = self.0.take() { b.error(text); } else { let _ = cliclack::log::error(text); }
}
}