[refactor] replace indicatif
with cliclack
for progress and logging, updating affected modules and dependencies
This commit is contained in:
54
Cargo.lock
generated
54
Cargo.lock
generated
@@ -298,6 +298,20 @@ dependencies = [
|
||||
"roff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cliclack"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c420bdc04c123a2df04d9c5a07289195f00007af6e45ab18f55e56dc7e04b8"
|
||||
dependencies = [
|
||||
"console",
|
||||
"indicatif",
|
||||
"once_cell",
|
||||
"strsim",
|
||||
"textwrap",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.54"
|
||||
@@ -1103,7 +1117,6 @@ dependencies = [
|
||||
"clap_complete",
|
||||
"clap_mangen",
|
||||
"directories",
|
||||
"indicatif",
|
||||
"polyscribe-core",
|
||||
"polyscribe-host",
|
||||
"polyscribe-protocol",
|
||||
@@ -1121,9 +1134,9 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"cliclack",
|
||||
"directories",
|
||||
"hex",
|
||||
"indicatif",
|
||||
"libc",
|
||||
"reqwest",
|
||||
"serde",
|
||||
@@ -1598,6 +1611,12 @@ version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.10"
|
||||
@@ -1680,6 +1699,17 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
"unicode-linebreak",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@@ -1972,6 +2002,12 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.1"
|
||||
@@ -2579,6 +2615,20 @@ name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
|
@@ -20,7 +20,6 @@ sha2 = "0.10.9"
|
||||
which = "6.0.3"
|
||||
tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros"] }
|
||||
clap = { version = "4.5.44", features = ["derive"] }
|
||||
indicatif = "0.17.11"
|
||||
directories = "5.0.1"
|
||||
whisper-rs = "0.14.3"
|
||||
cliclack = "0.3.6"
|
||||
|
@@ -9,7 +9,6 @@ clap = { version = "4.5.44", features = ["derive"] }
|
||||
clap_complete = "4.5.57"
|
||||
clap_mangen = "0.2.29"
|
||||
directories = "5.0.1"
|
||||
indicatif = "0.17.11"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.142"
|
||||
tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros", "process", "fs"] }
|
||||
|
@@ -7,7 +7,6 @@ use polyscribe_core::{config::ConfigService, ui::progress::ProgressReporter};
|
||||
use polyscribe_core::models; // Added: call into core models
|
||||
use polyscribe_host::PluginManager;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tracing::{error, info};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
fn init_tracing(quiet: bool, verbose: u8) {
|
||||
@@ -54,7 +53,7 @@ async fn main() -> Result<()> {
|
||||
gpu_layers,
|
||||
inputs,
|
||||
} => {
|
||||
info!("starting transcription workflow");
|
||||
polyscribe_core::ui::info("starting transcription workflow");
|
||||
let mut progress = ProgressReporter::new(args.no_interaction);
|
||||
|
||||
progress.step("Validating inputs");
|
||||
@@ -80,19 +79,19 @@ async fn main() -> Result<()> {
|
||||
Commands::Models { cmd } => {
|
||||
match cmd {
|
||||
ModelsCmd::Update => {
|
||||
info!("verifying/updating local models");
|
||||
polyscribe_core::ui::info("verifying/updating local models");
|
||||
tokio::task::spawn_blocking(|| models::update_local_models())
|
||||
.await
|
||||
.map_err(|e| anyhow!("blocking task join error: {e}"))?
|
||||
.context("updating models")?;
|
||||
}
|
||||
ModelsCmd::Download => {
|
||||
info!("interactive model selection and download");
|
||||
polyscribe_core::ui::info("interactive model selection and download");
|
||||
tokio::task::spawn_blocking(|| models::run_interactive_model_downloader())
|
||||
.await
|
||||
.map_err(|e| anyhow!("blocking task join error: {e}"))?
|
||||
.context("running downloader")?;
|
||||
println!("Model download complete.");
|
||||
polyscribe_core::ui::success("Model download complete.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -106,13 +105,14 @@ async fn main() -> Result<()> {
|
||||
PluginsCmd::List => {
|
||||
let list = pm.list().context("discovering plugins")?;
|
||||
for item in list {
|
||||
println!("{}", item.name);
|
||||
polyscribe_core::ui::info(item.name);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
PluginsCmd::Info { name } => {
|
||||
let info = pm.info(&name).with_context(|| format!("getting info for {}", name))?;
|
||||
println!("{}", serde_json::to_string_pretty(&info)?);
|
||||
let s = serde_json::to_string_pretty(&info)?;
|
||||
polyscribe_core::ui::info(s);
|
||||
Ok(())
|
||||
}
|
||||
PluginsCmd::Run { name, command, json } => {
|
||||
@@ -130,7 +130,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
let status = pm.forward_stdio(&mut child).await?;
|
||||
if !status.success() {
|
||||
error!("plugin returned non-zero exit code: {}", status);
|
||||
polyscribe_core::ui::error(format!("plugin returned non-zero exit code: {}", status));
|
||||
return Err(anyhow!("plugin failed"));
|
||||
}
|
||||
Ok(())
|
||||
|
@@ -13,7 +13,8 @@ directories = "5.0.1"
|
||||
chrono = "0.4.41"
|
||||
libc = "0.2.175"
|
||||
whisper-rs = "0.14.3"
|
||||
indicatif = "0.17.11"
|
||||
# UI and progress
|
||||
cliclack = { workspace = true }
|
||||
# New: HTTP downloads + hashing
|
||||
reqwest = { version = "0.12.7", default-features = false, features = ["blocking", "rustls-tls", "gzip", "json"] }
|
||||
sha2 = "0.10.8"
|
||||
|
@@ -7,7 +7,6 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use hex::ToHex;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::header::{
|
||||
ACCEPT_RANGES, CONTENT_LENGTH, CONTENT_RANGE, ETAG, IF_RANGE, LAST_MODIFIED, RANGE,
|
||||
@@ -80,45 +79,6 @@ fn mirror_label(url: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: build a single progress bar with desired format
|
||||
fn new_progress_bar(total: Option<u64>) -> ProgressBar {
|
||||
let zero_or_none = matches!(total, None | Some(0));
|
||||
|
||||
let pb = if zero_or_none {
|
||||
ProgressBar::new_spinner()
|
||||
} else {
|
||||
ProgressBar::new(total.unwrap())
|
||||
};
|
||||
pb.enable_steady_tick(Duration::from_millis(120));
|
||||
|
||||
// Use built-in byte and time placeholders
|
||||
let style = if zero_or_none {
|
||||
// No total known: show spinner, bytes so far, speed, and elapsed
|
||||
ProgressStyle::with_template(
|
||||
// Panel-ish spinner
|
||||
"{spinner} {bytes:>9} @ {bytes_per_sec} | {elapsed} | {msg}"
|
||||
).unwrap_or_else(|_| ProgressStyle::default_spinner())
|
||||
} else {
|
||||
// Total known: show bar, percent, bytes progress, speed, and ETA
|
||||
ProgressStyle::with_template(
|
||||
// Railcar + numeric focus
|
||||
"{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})"
|
||||
)
|
||||
.unwrap_or_else(|_| ProgressStyle::default_bar()).tick_strings(&[
|
||||
"▹▹▹▹▹",
|
||||
"▸▹▹▹▹",
|
||||
"▹▸▹▹▹",
|
||||
"▹▹▸▹▹",
|
||||
"▹▹▹▸▹",
|
||||
"▹▹▹▹▸",
|
||||
"▪▪▪▪▪",
|
||||
])
|
||||
.progress_chars("=>-")
|
||||
};
|
||||
|
||||
pb.set_style(style);
|
||||
pb
|
||||
}
|
||||
|
||||
// Perform a HEAD to get size/etag/last-modified and fill what we can
|
||||
fn head_entry(client: &Client, url: &str) -> Result<(Option<u64>, Option<String>, Option<String>, bool)> {
|
||||
@@ -603,7 +563,7 @@ pub fn ensure_model_available_noninteractive(name: &str) -> Result<PathBuf> {
|
||||
|
||||
// If already matches, early return
|
||||
if file_matches(&dest, entry.size, entry.sha256.as_deref())? {
|
||||
println!("Already up to date: {}", dest.display());
|
||||
crate::ui::info(format!("Already up to date: {}", dest.display()));
|
||||
return Ok(dest);
|
||||
}
|
||||
|
||||
@@ -611,12 +571,12 @@ pub fn ensure_model_available_noninteractive(name: &str) -> Result<PathBuf> {
|
||||
let base = &entry.base;
|
||||
let variant = &entry.variant;
|
||||
let size_str = format_size_mb(entry.size);
|
||||
println!("Base: {base} • Type: {variant}");
|
||||
println!(
|
||||
crate::ui::println_above_bars(format!("Base: {base} • Type: {variant}"));
|
||||
crate::ui::println_above_bars(format!(
|
||||
"Source: {} • Size: {}",
|
||||
mirror_label(&entry.url),
|
||||
size_str
|
||||
);
|
||||
));
|
||||
|
||||
download_with_progress(&dest, &entry)?;
|
||||
Ok(dest)
|
||||
@@ -693,7 +653,7 @@ fn download_with_progress(dest_path: &Path, entry: &ModelEntry) -> Result<()> {
|
||||
.user_agent("polyscribe-model-downloader/1")
|
||||
.build()?;
|
||||
|
||||
println!("Resolving source: {} ({})", mirror_label(url), url);
|
||||
crate::ui::info(format!("Resolving source: {} ({})", mirror_label(url), url));
|
||||
|
||||
// HEAD for size/etag/ranges
|
||||
let (mut total_len, remote_etag, _remote_last_mod, ranges_ok) = head_entry(&client, url)
|
||||
@@ -717,7 +677,7 @@ fn download_with_progress(dest_path: &Path, entry: &ModelEntry) -> Result<()> {
|
||||
|
||||
if dest_path.exists() {
|
||||
if file_matches(dest_path, total_len, entry.sha256.as_deref())? {
|
||||
println!("Already up to date: {}", dest_path.display());
|
||||
crate::ui::info(format!("Already up to date: {}", dest_path.display()));
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -749,16 +709,19 @@ fn download_with_progress(dest_path: &Path, entry: &ModelEntry) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
println!("Download: {}", part_path.display());
|
||||
crate::ui::info(format!("Download: {}", part_path.display()));
|
||||
|
||||
let pb_total = total_len.unwrap_or(0);
|
||||
let pb = if pb_total > 0 {
|
||||
let pb = new_progress_bar(Some(pb_total));
|
||||
pb.set_position(resume_from);
|
||||
pb
|
||||
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 {
|
||||
new_progress_bar(None)
|
||||
};
|
||||
sp = Some(crate::ui::Spinner::start("Downloading"));
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
let mut resp = req.send()?.error_for_status()?;
|
||||
@@ -781,7 +744,12 @@ 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()?;
|
||||
pb.set_position(0);
|
||||
if let Some(b) = bar.as_mut() {
|
||||
b.stop("restarting");
|
||||
}
|
||||
let mut b2 = cliclack::progress_bar(pb_total);
|
||||
b2.start("Downloading");
|
||||
bar = Some(b2);
|
||||
|
||||
// Reopen the part file since we dropped it
|
||||
part_file = OpenOptions::new()
|
||||
@@ -803,20 +771,25 @@ fn download_with_progress(dest_path: &Path, entry: &ModelEntry) -> Result<()> {
|
||||
}
|
||||
part_file.write_all(&buf[..read])?;
|
||||
if pb_total > 0 {
|
||||
let pos = part_file.metadata()?.len();
|
||||
pb.set_position(pos);
|
||||
if let Some(b) = bar.as_mut() {
|
||||
b.inc(read as u64);
|
||||
}
|
||||
} else {
|
||||
pb.inc(read as u64);
|
||||
// spinner: nothing to update per chunk beyond the animation
|
||||
}
|
||||
}
|
||||
part_file.flush()?;
|
||||
part_file.sync_all()?;
|
||||
}
|
||||
|
||||
pb.finish_and_clear();
|
||||
if pb_total > 0 {
|
||||
if let Some(b) = bar.as_mut() { b.stop("done"); }
|
||||
} else {
|
||||
if let Some(s) = sp.take() { s.success("done"); }
|
||||
}
|
||||
|
||||
if let Some(expected_hex) = entry.sha256.as_deref() {
|
||||
println!("Verify: SHA-256");
|
||||
crate::ui::info("Verify: SHA-256");
|
||||
let mut f = File::open(&part_path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buf = vec![0u8; 1024 * 1024];
|
||||
@@ -836,7 +809,7 @@ fn download_with_progress(dest_path: &Path, entry: &ModelEntry) -> Result<()> {
|
||||
));
|
||||
}
|
||||
} else {
|
||||
println!("Verify: checksum not provided by source (skipped)");
|
||||
crate::ui::info("Verify: checksum not provided by source (skipped)");
|
||||
}
|
||||
|
||||
if let Some(parent) = dest_path.parent() {
|
||||
@@ -853,26 +826,26 @@ fn download_with_progress(dest_path: &Path, entry: &ModelEntry) -> Result<()> {
|
||||
if elapsed > 0.0 {
|
||||
let mib = sz as f64 / 1024.0 / 1024.0;
|
||||
let rate = mib / elapsed;
|
||||
println!(
|
||||
"✔ Saved: {} ({}) in {:.1}s, {:.1} MiB/s",
|
||||
crate::ui::success(format!(
|
||||
"Saved: {} ({}) in {:.1}s, {:.1} MiB/s",
|
||||
dest_path.display(),
|
||||
format_size_mb(Some(sz)),
|
||||
elapsed,
|
||||
rate
|
||||
);
|
||||
));
|
||||
} else {
|
||||
println!(
|
||||
"✔ Saved: {} ({})",
|
||||
crate::ui::success(format!(
|
||||
"Saved: {} ({})",
|
||||
dest_path.display(),
|
||||
format_size_mb(Some(sz))
|
||||
);
|
||||
));
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"✔ Saved: {} ({})",
|
||||
crate::ui::success(format!(
|
||||
"Saved: {} ({})",
|
||||
dest_path.display(),
|
||||
format_size_mb(None)
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -912,8 +885,10 @@ pub fn run_interactive_model_downloader() -> Result<()> {
|
||||
}
|
||||
|
||||
ui::intro("PolyScribe model downloader");
|
||||
ui::println_above_bars("Select a model base:");
|
||||
for (i, base) in ordered_bases.iter().enumerate() {
|
||||
|
||||
// Build Select items for bases with counts and size ranges
|
||||
let mut base_labels: Vec<String> = Vec::new();
|
||||
for base in &ordered_bases {
|
||||
let variants = &by_base[base];
|
||||
let (min_sz, max_sz) = variants.iter().fold((None, None), |acc, m| {
|
||||
let (mut lo, mut hi) = acc;
|
||||
@@ -930,51 +905,24 @@ pub fn run_interactive_model_downloader() -> Result<()> {
|
||||
hi as f64 / 1_000_000.0
|
||||
),
|
||||
(Some(sz), _) => format!(" ~{:.2} MB", sz as f64 / 1_000_000.0),
|
||||
_ => "".to_string(),
|
||||
_ => String::new(),
|
||||
};
|
||||
ui::println_above_bars(format!(
|
||||
" {}. {} ({:>2} types){}",
|
||||
i + 1,
|
||||
base,
|
||||
variants.len(),
|
||||
size_info
|
||||
));
|
||||
base_labels.push(format!("{} ({} types){}", base, variants.len(), size_info));
|
||||
}
|
||||
let base_refs: Vec<&str> = base_labels.iter().map(|s| s.as_str()).collect();
|
||||
let base_idx = ui::prompt_select("Choose a model base", &base_refs)?;
|
||||
let chosen_base = ordered_bases[base_idx].clone();
|
||||
|
||||
let base_ans = ui::prompt_input("Base [1]", Some("1"))?;
|
||||
// Robust: accept either index (1-based) or base name
|
||||
let base_idx = base_ans.trim().parse::<usize>().ok();
|
||||
let chosen_base = if let Some(idx) = base_idx {
|
||||
if idx == 0 || idx > ordered_bases.len() {
|
||||
return Err(anyhow!("invalid base selection"));
|
||||
}
|
||||
ordered_bases[idx - 1].clone()
|
||||
} else {
|
||||
// Match by name, case-insensitive
|
||||
let ans = base_ans.trim().to_ascii_lowercase();
|
||||
let pos = ordered_bases
|
||||
.iter()
|
||||
.position(|b| b.eq_ignore_ascii_case(&ans))
|
||||
.ok_or_else(|| anyhow!("invalid base selection"))?;
|
||||
ordered_bases[pos].clone()
|
||||
};
|
||||
|
||||
// Prepare variant list for chosen base
|
||||
let mut variants = by_base.remove(&chosen_base).unwrap_or_default();
|
||||
|
||||
// Sort variants by a friendly order: default, en, then others alphabetically
|
||||
variants.sort_by(|a, b| {
|
||||
let rank = |v: &str| match v {
|
||||
"default" => 0,
|
||||
"en" => 1,
|
||||
_ => 2,
|
||||
};
|
||||
rank(&a.variant)
|
||||
.cmp(&rank(&b.variant))
|
||||
.then_with(|| a.variant.cmp(&b.variant))
|
||||
let rank = |v: &str| match v { "default" => 0, "en" => 1, _ => 2 };
|
||||
rank(&a.variant).cmp(&rank(&b.variant)).then_with(|| a.variant.cmp(&b.variant))
|
||||
});
|
||||
|
||||
ui::println_above_bars(format!("Select a type for '{}':", chosen_base));
|
||||
for (i, m) in variants.iter().enumerate() {
|
||||
// Build Multi-Select items for variants
|
||||
let mut variant_labels: Vec<String> = Vec::new();
|
||||
for m in &variants {
|
||||
let size = format_size_mb(m.size.as_ref().copied());
|
||||
let updated = m
|
||||
.last_modified
|
||||
@@ -982,62 +930,46 @@ pub fn run_interactive_model_downloader() -> Result<()> {
|
||||
.map(short_date)
|
||||
.map(|d| format!(" • updated {}", d))
|
||||
.unwrap_or_default();
|
||||
let variant_label = if m.variant == "default" {
|
||||
"default"
|
||||
} else {
|
||||
&m.variant
|
||||
};
|
||||
ui::println_above_bars(format!(
|
||||
" {}. {} ({}{})",
|
||||
i + 1,
|
||||
variant_label,
|
||||
size,
|
||||
updated
|
||||
));
|
||||
let variant_label = if m.variant == "default" { "default" } else { &m.variant };
|
||||
variant_labels.push(format!("{} ({}{})", variant_label, size, updated));
|
||||
}
|
||||
let variant_refs: Vec<&str> = variant_labels.iter().map(|s| s.as_str()).collect();
|
||||
let mut defaults = vec![false; variant_refs.len()];
|
||||
if !defaults.is_empty() { defaults[0] = true; }
|
||||
let picks = ui::prompt_multi_select(
|
||||
&format!("Select types for '{}'", chosen_base),
|
||||
&variant_refs,
|
||||
Some(&defaults),
|
||||
)?;
|
||||
|
||||
if picks.is_empty() {
|
||||
ui::warn("No types selected; aborting.");
|
||||
ui::outro("No changes made.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let type_ans = ui::prompt_input("Type [1]", Some("1"))?;
|
||||
let type_idx = type_ans
|
||||
.trim()
|
||||
.parse::<usize>()
|
||||
.ok()
|
||||
.filter(|n| *n >= 1 && *n <= variants.len())
|
||||
.or_else(|| {
|
||||
// Optional: allow typing the variant name
|
||||
let ans = type_ans.trim().to_ascii_lowercase();
|
||||
variants
|
||||
.iter()
|
||||
.position(|m| {
|
||||
let v = if m.variant == "default" {
|
||||
"default"
|
||||
} else {
|
||||
&m.variant
|
||||
};
|
||||
v.eq_ignore_ascii_case(&ans)
|
||||
})
|
||||
.map(|i| i + 1)
|
||||
ui::println_above_bars("Downloading selected models...");
|
||||
|
||||
// Setup multi-progress when multiple items are selected
|
||||
let labels: Vec<String> = picks
|
||||
.iter()
|
||||
.map(|&i| {
|
||||
let m = &variants[i];
|
||||
format!("{} ({})", m.name, format_size_mb(m.size))
|
||||
})
|
||||
.ok_or_else(|| anyhow!("invalid type selection"))?;
|
||||
|
||||
let picked = variants[type_idx - 1].clone();
|
||||
|
||||
fn entry_label(entry: &ModelEntry) -> String {
|
||||
format!("{} ({})", entry.name, format_size_mb(entry.size))
|
||||
}
|
||||
|
||||
let labels = vec![entry_label(&picked)];
|
||||
.collect();
|
||||
let mut pm = ui::progress::ProgressManager::default_for_files(labels.len());
|
||||
pm.init_files(&labels);
|
||||
if let Some(pb) = pm.per_bar(0) {
|
||||
pb.set_message("downloading");
|
||||
|
||||
for (bar_idx, idx) in picks.into_iter().enumerate() {
|
||||
let picked = variants[idx].clone();
|
||||
pm.set_per_message(bar_idx, "downloading");
|
||||
let _path = ensure_model_available_noninteractive(&picked.name)?;
|
||||
pm.mark_file_done(bar_idx);
|
||||
ui::success(format!("Ready: {}", picked.name));
|
||||
}
|
||||
|
||||
let path = ensure_model_available_noninteractive(&picked.name)?;
|
||||
ui::println_above_bars(format!("Ready: {}", path.display()));
|
||||
pm.mark_file_done(0);
|
||||
if let Some(total) = pm.total_bar() {
|
||||
total.finish_with_message("all done");
|
||||
}
|
||||
pm.finish_total("all done");
|
||||
ui::outro("Model selection complete.");
|
||||
Ok(())
|
||||
}
|
||||
|
@@ -1,64 +1,124 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2025 <COPYRIGHT HOLDER>. All rights reserved.
|
||||
|
||||
//! Minimal UI helpers used across the core crate.
|
||||
//! This keeps interactive bits centralized and easy to stub in tests.
|
||||
//! UI helpers powered by cliclack for interactive console experiences.
|
||||
//! Centralizes prompts, logging, and progress primitives.
|
||||
|
||||
/// Progress indicators and reporting tools for displaying task completion.
|
||||
pub mod progress;
|
||||
|
||||
use std::io::{self, Write};
|
||||
use std::io;
|
||||
|
||||
/// Print an informational line to stderr (suppressed when quiet mode is enabled by callers).
|
||||
/// Log an informational message.
|
||||
pub fn info(msg: impl AsRef<str>) {
|
||||
eprintln!("{}", msg.as_ref());
|
||||
let m = msg.as_ref();
|
||||
let _ = cliclack::log::info(m);
|
||||
}
|
||||
|
||||
/// Print a warning line to stderr.
|
||||
/// Log a warning message.
|
||||
pub fn warn(msg: impl AsRef<str>) {
|
||||
eprintln!("WARNING: {}", msg.as_ref());
|
||||
let m = msg.as_ref();
|
||||
let _ = cliclack::log::warning(m);
|
||||
}
|
||||
|
||||
/// Print an error line to stderr.
|
||||
/// Log an error message.
|
||||
pub fn error(msg: impl AsRef<str>) {
|
||||
eprintln!("ERROR: {}", msg.as_ref());
|
||||
let m = msg.as_ref();
|
||||
let _ = cliclack::log::error(m);
|
||||
}
|
||||
|
||||
/// Print a short intro header (non-fancy).
|
||||
/// Log a success message.
|
||||
pub fn success(msg: impl AsRef<str>) {
|
||||
let m = msg.as_ref();
|
||||
let _ = cliclack::log::success(m);
|
||||
}
|
||||
|
||||
/// Log a note message with a prompt and a message.
|
||||
pub fn note(prompt: impl AsRef<str>, message: impl AsRef<str>) {
|
||||
let _ = cliclack::note(prompt.as_ref(), message.as_ref());
|
||||
}
|
||||
|
||||
/// Print a short intro header.
|
||||
pub fn intro(title: impl AsRef<str>) {
|
||||
eprintln!("== {} ==", title.as_ref());
|
||||
let _ = cliclack::intro(title.as_ref());
|
||||
}
|
||||
|
||||
/// Print a short outro footer (non-fancy).
|
||||
/// Print a short outro footer.
|
||||
pub fn outro(msg: impl AsRef<str>) {
|
||||
eprintln!("{}", msg.as_ref());
|
||||
let _ = cliclack::outro(msg.as_ref());
|
||||
}
|
||||
|
||||
/// Print a line that should appear above any progress indicators (plain for now).
|
||||
/// Print a line that should appear above any progress indicators.
|
||||
pub fn println_above_bars(line: impl AsRef<str>) {
|
||||
eprintln!("{}", line.as_ref());
|
||||
let _ = cliclack::log::info(line.as_ref());
|
||||
}
|
||||
|
||||
/// Prompt for input on stdin. Returns default if provided and user enters empty string.
|
||||
/// Prompt for input on stdin using cliclack's input component.
|
||||
/// 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> {
|
||||
let mut stdout = io::stdout();
|
||||
match default {
|
||||
Some(def) => {
|
||||
write!(stdout, "{} [{}]: ", prompt, def)?;
|
||||
}
|
||||
None => {
|
||||
write!(stdout, "{}: ", prompt)?;
|
||||
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()))
|
||||
}
|
||||
|
||||
/// Present a single-choice selector and return the selected index.
|
||||
pub fn prompt_select<'a>(prompt: &str, items: &[&'a str]) -> io::Result<usize> {
|
||||
let mut sel = cliclack::select::<usize>(prompt);
|
||||
for (idx, label) in items.iter().enumerate() {
|
||||
sel = sel.item(idx, *label, "");
|
||||
}
|
||||
sel.interact()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
/// 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>> {
|
||||
let mut ms = cliclack::multiselect::<usize>(prompt);
|
||||
for (idx, label) in items.iter().enumerate() {
|
||||
ms = ms.item(idx, *label, "");
|
||||
}
|
||||
if let Some(def) = defaults {
|
||||
let selected: Vec<usize> = def
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, &on)| if on { Some(i) } else { None })
|
||||
.collect();
|
||||
if !selected.is_empty() {
|
||||
ms = ms.initial_values(selected);
|
||||
}
|
||||
}
|
||||
stdout.flush()?;
|
||||
ms.interact()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf)?;
|
||||
let trimmed = buf.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.unwrap_or_default().to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
/// A simple spinner wrapper built on top of `cliclack::spinner()`.
|
||||
///
|
||||
/// This wrapper provides a minimal API with start/stop/success/error methods
|
||||
/// to standardize spinner usage across the project.
|
||||
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)
|
||||
}
|
||||
/// 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());
|
||||
}
|
||||
/// 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());
|
||||
}
|
||||
/// 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());
|
||||
}
|
||||
}
|
||||
|
@@ -1,22 +1,21 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2025 <COPYRIGHT HOLDER>. All rights reserved.
|
||||
|
||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||
use std::io::IsTerminal as _;
|
||||
|
||||
/// Manages a set of per-file progress bars plus a top aggregate bar.
|
||||
/// Manages a set of per-file progress bars plus a top aggregate bar using cliclack.
|
||||
pub struct ProgressManager {
|
||||
enabled: bool,
|
||||
mp: Option<MultiProgress>,
|
||||
per: Vec<ProgressBar>,
|
||||
total: Option<ProgressBar>,
|
||||
per: Vec<cliclack::ProgressBar>,
|
||||
total: Option<cliclack::ProgressBar>,
|
||||
completed: usize,
|
||||
total_len: usize,
|
||||
}
|
||||
|
||||
impl ProgressManager {
|
||||
/// Create a new manager with the given enabled flag.
|
||||
pub fn new(enabled: bool) -> Self {
|
||||
Self { enabled, mp: None, per: Vec::new(), total: None, completed: 0 }
|
||||
Self { enabled, per: Vec::new(), total: None, completed: 0, total_len: 0 }
|
||||
}
|
||||
|
||||
/// Create a manager that enables bars when `n > 1`, stderr is a TTY, and not quiet.
|
||||
@@ -27,61 +26,69 @@ impl ProgressManager {
|
||||
|
||||
/// Initialize bars for the given file labels. If disabled or single file, no-op.
|
||||
pub fn init_files(&mut self, labels: &[String]) {
|
||||
self.total_len = labels.len();
|
||||
if !self.enabled || labels.len() <= 1 {
|
||||
// No bars in single-file mode or when disabled
|
||||
self.enabled = false;
|
||||
return;
|
||||
}
|
||||
let mp = MultiProgress::new();
|
||||
// Aggregate bar at the top
|
||||
let total = mp.add(ProgressBar::new(labels.len() as u64));
|
||||
total.set_style(ProgressStyle::with_template("{prefix} [{bar:40.cyan/blue}] {pos}/{len}")
|
||||
.unwrap()
|
||||
.progress_chars("=>-"));
|
||||
total.set_prefix("Total");
|
||||
let mut total = cliclack::progress_bar(labels.len() as u64);
|
||||
total.start("Total");
|
||||
self.total = Some(total);
|
||||
// Per-file bars
|
||||
// Per-file bars (100% scale for each)
|
||||
for label in labels {
|
||||
let pb = mp.add(ProgressBar::new(100));
|
||||
pb.set_style(ProgressStyle::with_template("{prefix} [{bar:40.green/black}] {pos}% {msg}")
|
||||
.unwrap()
|
||||
.progress_chars("=>-"));
|
||||
pb.set_position(0);
|
||||
pb.set_prefix(label.clone());
|
||||
let mut pb = cliclack::progress_bar(100);
|
||||
pb.start(label);
|
||||
self.per.push(pb);
|
||||
}
|
||||
self.mp = Some(mp);
|
||||
}
|
||||
|
||||
/// Returns true when bars are enabled (multi-file TTY mode).
|
||||
pub fn is_enabled(&self) -> bool { self.enabled }
|
||||
|
||||
/// Get a clone of the per-file progress bar at index, if enabled.
|
||||
pub fn per_bar(&self, idx: usize) -> Option<ProgressBar> {
|
||||
if !self.enabled { return None; }
|
||||
self.per.get(idx).cloned()
|
||||
/// Update a per-file bar message.
|
||||
pub fn set_per_message(&mut self, idx: usize, message: &str) {
|
||||
if !self.enabled { return; }
|
||||
if let Some(pb) = self.per.get_mut(idx) {
|
||||
pb.set_message(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a clone of the aggregate (total) progress bar, if enabled.
|
||||
pub fn total_bar(&self) -> Option<ProgressBar> {
|
||||
if !self.enabled { return None; }
|
||||
self.total.as_ref().cloned()
|
||||
/// Update a per-file bar percent (0..=100).
|
||||
pub fn set_per_percent(&mut self, idx: usize, percent: u64) {
|
||||
if !self.enabled { return; }
|
||||
if let Some(pb) = self.per.get_mut(idx) {
|
||||
let p = percent.min(100);
|
||||
pb.set_message(&format!("{p}%"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark a file as finished (set to 100% and update total counter).
|
||||
pub fn mark_file_done(&mut self, idx: usize) {
|
||||
if !self.enabled { return; }
|
||||
if let Some(pb) = self.per.get(idx) {
|
||||
pb.set_position(100);
|
||||
pb.finish_with_message("done");
|
||||
if let Some(pb) = self.per.get_mut(idx) {
|
||||
pb.stop("done");
|
||||
}
|
||||
self.completed += 1;
|
||||
if let Some(total) = &self.total { total.set_position(self.completed as u64); }
|
||||
if let Some(total) = &mut self.total {
|
||||
total.inc(1);
|
||||
if self.completed >= self.total_len {
|
||||
total.stop("all done");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish the aggregate bar with a custom message.
|
||||
pub fn finish_total(&mut self, message: &str) {
|
||||
if !self.enabled { return; }
|
||||
if let Some(total) = &mut self.total {
|
||||
total.stop(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple reporter for displaying progress messages in the terminal.
|
||||
/// Provides different output formatting based on whether the environment is interactive or not.
|
||||
/// A simple reporter for displaying progress messages using cliclack logging.
|
||||
#[derive(Debug)]
|
||||
pub struct ProgressReporter {
|
||||
non_interactive: bool,
|
||||
@@ -89,37 +96,23 @@ pub struct ProgressReporter {
|
||||
|
||||
impl ProgressReporter {
|
||||
/// Creates a new progress reporter.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `non_interactive` - Whether the output should be formatted for non-interactive environments.
|
||||
pub fn new(non_interactive: bool) -> Self {
|
||||
Self { non_interactive }
|
||||
}
|
||||
pub fn new(non_interactive: bool) -> Self { Self { non_interactive } }
|
||||
|
||||
/// Displays a progress step message.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `message` - The message to display for this progress step.
|
||||
pub fn step(&mut self, message: &str) {
|
||||
if self.non_interactive {
|
||||
eprintln!("[..] {message}");
|
||||
let _ = cliclack::log::info(format!("[..] {message}"));
|
||||
} else {
|
||||
eprintln!("• {message}");
|
||||
let _ = cliclack::log::info(format!("• {message}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays a completion message.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `message` - The message to display when a task is completed.
|
||||
pub fn finish_with_message(&mut self, message: &str) {
|
||||
if self.non_interactive {
|
||||
eprintln!("[ok] {message}");
|
||||
let _ = cliclack::log::info(format!("[ok] {message}"));
|
||||
} else {
|
||||
eprintln!("✓ {message}");
|
||||
let _ = cliclack::log::info(format!("✓ {message}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user