[refactor] propagate no-progress
and no-interaction
flags, enhance prompt handling, and update progress bar logic with cliclack
This commit is contained in:
@@ -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");
|
||||
|
@@ -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); }
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user