[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

82
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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,
}

View File

@@ -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")?;

View File

@@ -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() {

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); }
}
}