[refactor] propagate no-progress
and no-interaction
flags, enhance prompt handling, and update progress bar logic with cliclack
This commit is contained in:
82
Cargo.lock
generated
82
Cargo.lock
generated
@@ -97,6 +97,22 @@ version = "1.0.99"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
||||
|
||||
[[package]]
|
||||
name = "assert_cmd"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"bstr",
|
||||
"doc-comment",
|
||||
"libc",
|
||||
"predicates",
|
||||
"predicates-core",
|
||||
"predicates-tree",
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.27"
|
||||
@@ -172,6 +188,17 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.9",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
@@ -374,6 +401,12 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "difflib"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -416,6 +449,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "doc-comment"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
@@ -1113,6 +1152,7 @@ name = "polyscribe-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"clap_mangen",
|
||||
@@ -1191,6 +1231,33 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "predicates"
|
||||
version = "3.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"difflib",
|
||||
"predicates-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "predicates-core"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
|
||||
|
||||
[[package]]
|
||||
name = "predicates-tree"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
|
||||
dependencies = [
|
||||
"predicates-core",
|
||||
"termtree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.36"
|
||||
@@ -1699,6 +1766,12 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termtree"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.2"
|
||||
@@ -2055,6 +2128,15 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
|
@@ -3,6 +3,10 @@ name = "polyscribe-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "polyscribe"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.99"
|
||||
clap = { version = "4.5.44", features = ["derive"] }
|
||||
@@ -23,3 +27,6 @@ polyscribe-protocol = { path = "../polyscribe-protocol" }
|
||||
[features]
|
||||
# Optional GPU-specific flags can be forwarded down to core/host if needed
|
||||
default = []
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.16"
|
||||
|
@@ -25,6 +25,10 @@ pub struct Cli {
|
||||
#[arg(long, default_value_t = false)]
|
||||
pub no_interaction: bool,
|
||||
|
||||
/// Disable progress bars/spinners
|
||||
#[arg(long, default_value_t = false)]
|
||||
pub no_progress: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
@@ -35,10 +35,11 @@ async fn main() -> Result<()> {
|
||||
|
||||
init_tracing(args.quiet, args.verbose);
|
||||
|
||||
// Optionally propagate quiet/no-interaction/verbosity to core if your lib exposes setters.
|
||||
// polyscribe_core::set_quiet(args.quiet);
|
||||
// polyscribe_core::set_no_interaction(args.no_interaction);
|
||||
// polyscribe_core::set_verbose(args.verbose);
|
||||
// Propagate UI flags to core so ui facade can apply policy
|
||||
polyscribe_core::set_quiet(args.quiet);
|
||||
polyscribe_core::set_no_interaction(args.no_interaction);
|
||||
polyscribe_core::set_verbose(args.verbose);
|
||||
polyscribe_core::set_no_progress(args.no_progress);
|
||||
|
||||
let _cfg = ConfigService::load_or_default().context("loading configuration")?;
|
||||
|
||||
|
@@ -2,11 +2,9 @@
|
||||
// Copyright (c) 2025 <COPYRIGHT HOLDER>. All rights reserved.
|
||||
|
||||
use std::process::Command;
|
||||
use assert_cmd::cargo::cargo_bin;
|
||||
|
||||
fn bin() -> String {
|
||||
std::env::var("CARGO_BIN_EXE_polyscribe")
|
||||
.unwrap_or_else(|_| "polyscribe".to_string())
|
||||
}
|
||||
fn bin() -> std::path::PathBuf { cargo_bin("polyscribe") }
|
||||
|
||||
#[test]
|
||||
fn aux_completions_bash_outputs_script() {
|
||||
|
@@ -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