[feat] add progress management and centralized TTY-aware UI helpers with cliclack and indicatif
This commit is contained in:
43
Cargo.lock
generated
43
Cargo.lock
generated
@@ -103,17 +103,6 @@ version = "1.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "atty"
|
|
||||||
version = "0.2.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
|
||||||
dependencies = [
|
|
||||||
"hermit-abi",
|
|
||||||
"libc",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -599,15 +588,6 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hermit-abi"
|
|
||||||
version = "0.1.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -1146,7 +1126,6 @@ name = "polyscribe"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"atty",
|
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
@@ -1967,28 +1946,6 @@ dependencies = [
|
|||||||
"fs_extra",
|
"fs_extra",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi"
|
|
||||||
version = "0.3.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
|
||||||
dependencies = [
|
|
||||||
"winapi-i686-pc-windows-gnu",
|
|
||||||
"winapi-x86_64-pc-windows-gnu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-i686-pc-windows-gnu"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
|
@@ -31,7 +31,6 @@ whisper-rs = { git = "https://github.com/tazz4843/whisper-rs" }
|
|||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
cliclack = "0.3"
|
cliclack = "0.3"
|
||||||
indicatif = "0.17"
|
indicatif = "0.17"
|
||||||
atty = "0.2"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
147
src/lib.rs
147
src/lib.rs
@@ -59,17 +59,8 @@ pub fn is_no_progress() -> bool {
|
|||||||
|
|
||||||
/// Check whether stdin is connected to a TTY. Used to avoid blocking prompts when not interactive.
|
/// Check whether stdin is connected to a TTY. Used to avoid blocking prompts when not interactive.
|
||||||
pub fn stdin_is_tty() -> bool {
|
pub fn stdin_is_tty() -> bool {
|
||||||
#[cfg(unix)]
|
use std::io::IsTerminal as _;
|
||||||
{
|
std::io::stdin().is_terminal()
|
||||||
use std::os::unix::io::AsRawFd;
|
|
||||||
unsafe { libc::isatty(std::io::stdin().as_raw_fd()) == 1 }
|
|
||||||
}
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
{
|
|
||||||
// Best-effort on non-Unix: assume TTY when not redirected by common CI vars
|
|
||||||
// This avoids introducing a new dependency for atty.
|
|
||||||
!(std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A guard that temporarily redirects stderr to /dev/null on Unix when quiet mode is active.
|
/// A guard that temporarily redirects stderr to /dev/null on Unix when quiet mode is active.
|
||||||
@@ -184,139 +175,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Centralized UI helpers (TTY-aware, quiet/verbose-aware)
|
/// Centralized UI helpers (TTY-aware, quiet/verbose-aware)
|
||||||
pub mod ui {
|
pub mod ui;
|
||||||
use std::io;
|
|
||||||
// Prefer cliclack for all user-visible messages to ensure consistent, TTY-aware output.
|
|
||||||
// Falls back to stderr printing if needed.
|
|
||||||
/// Startup intro/banner (suppressed when quiet).
|
|
||||||
pub fn intro(msg: impl AsRef<str>) {
|
|
||||||
if crate::is_quiet() { return; }
|
|
||||||
// Use cliclack intro to render a nice banner when TTY
|
|
||||||
let _ = cliclack::intro(msg.as_ref());
|
|
||||||
}
|
|
||||||
/// Print an informational line (suppressed when quiet).
|
|
||||||
pub fn info(msg: impl AsRef<str>) {
|
|
||||||
if crate::is_quiet() { return; }
|
|
||||||
let _ = cliclack::log::info(msg.as_ref());
|
|
||||||
}
|
|
||||||
/// Print a warning (always printed).
|
|
||||||
pub fn warn(msg: impl AsRef<str>) {
|
|
||||||
// cliclack provides a warning-level log utility
|
|
||||||
let _ = cliclack::log::warning(msg.as_ref());
|
|
||||||
}
|
|
||||||
/// Print an error (always printed).
|
|
||||||
pub fn error(msg: impl AsRef<str>) {
|
|
||||||
let _ = cliclack::log::error(msg.as_ref());
|
|
||||||
}
|
|
||||||
/// Print a line above any progress bars (maps to cliclack log; synchronized).
|
|
||||||
pub fn println_above_bars(msg: impl AsRef<str>) {
|
|
||||||
if crate::is_quiet() { return; }
|
|
||||||
// cliclack logs are synchronized with its spinners/bars
|
|
||||||
let _ = cliclack::log::info(msg.as_ref());
|
|
||||||
}
|
|
||||||
/// Final outro/summary printed below any progress indicators (suppressed when quiet).
|
|
||||||
pub fn outro(msg: impl AsRef<str>) {
|
|
||||||
if crate::is_quiet() { return; }
|
|
||||||
let _ = cliclack::outro(msg.as_ref());
|
|
||||||
}
|
|
||||||
/// Prompt the user (TTY-aware via cliclack) and read a line from stdin. Returns the raw line with trailing newline removed.
|
|
||||||
pub fn prompt_line(prompt: &str) -> io::Result<String> {
|
|
||||||
// Route prompt through cliclack to keep consistent styling and avoid direct eprint!/println!
|
|
||||||
let _ = cliclack::log::info(prompt);
|
|
||||||
let mut s = String::new();
|
|
||||||
io::stdin().read_line(&mut s)?;
|
|
||||||
Ok(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Progress manager built on indicatif MultiProgress for per-file and aggregate bars
|
|
||||||
/// TTY-aware progress UI built on `indicatif` for per-file and aggregate progress bars.
|
|
||||||
///
|
|
||||||
/// This small helper encapsulates a `MultiProgress` with one aggregate (total) bar and
|
|
||||||
/// one per-file bar. It is intentionally minimal to keep integration lightweight.
|
|
||||||
pub mod progress {
|
|
||||||
use atty::Stream;
|
|
||||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
|
||||||
|
|
||||||
/// Manages a set of per-file progress bars plus a top aggregate bar.
|
|
||||||
pub struct ProgressManager {
|
|
||||||
enabled: bool,
|
|
||||||
mp: Option<MultiProgress>,
|
|
||||||
per: Vec<ProgressBar>,
|
|
||||||
total: Option<ProgressBar>,
|
|
||||||
total_n: usize,
|
|
||||||
completed: usize,
|
|
||||||
done: Vec<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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, total_n: 0, completed: 0, done: Vec::new() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a manager that enables bars when `n > 1`, stderr is a TTY, and not quiet.
|
|
||||||
pub fn default_for_files(n: usize) -> Self {
|
|
||||||
let enabled = n > 1 && atty::is(Stream::Stderr) && !crate::is_quiet() && !crate::is_no_progress();
|
|
||||||
Self::new(enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize bars for the given file labels. If disabled or single file, no-op.
|
|
||||||
pub fn init_files(&mut self, labels: &[String]) {
|
|
||||||
self.total_n = labels.len();
|
|
||||||
if !self.enabled || self.total_n <= 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");
|
|
||||||
self.total = Some(total);
|
|
||||||
// Per-file bars
|
|
||||||
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());
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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");
|
|
||||||
}
|
|
||||||
self.completed += 1;
|
|
||||||
if let Some(total) = &self.total { total.set_position(self.completed as u64); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Logging macros and helpers
|
/// Logging macros and helpers
|
||||||
/// Log an error using the UI helper (always printed). Recommended for user-visible errors.
|
/// Log an error using the UI helper (always printed). Recommended for user-visible errors.
|
||||||
|
954
src/main.rs
954
src/main.rs
File diff suppressed because it is too large
Load Diff
1336
src/models.rs
1336
src/models.rs
File diff suppressed because it is too large
Load Diff
84
src/ui.rs
Normal file
84
src/ui.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2025 <COPYRIGHT HOLDER>. All rights reserved.
|
||||||
|
|
||||||
|
//! Centralized UI helpers (TTY-aware, quiet/verbose-aware)
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
/// Startup intro/banner (suppressed when quiet).
|
||||||
|
pub fn intro(msg: impl AsRef<str>) {
|
||||||
|
let _ = cliclack::intro(msg.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Final outro/summary printed below any progress indicators (suppressed when quiet).
|
||||||
|
pub fn outro(msg: impl AsRef<str>) {
|
||||||
|
let _ = cliclack::outro(msg.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Info message (TTY-aware; suppressed by --quiet is handled by outer callers if needed)
|
||||||
|
pub fn info(msg: impl AsRef<str>) {
|
||||||
|
let _ = cliclack::log::info(msg.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print a warning (always printed).
|
||||||
|
pub fn warn(msg: impl AsRef<str>) {
|
||||||
|
// cliclack provides a warning-level log utility
|
||||||
|
let _ = cliclack::log::warning(msg.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print an error (always printed).
|
||||||
|
pub fn error(msg: impl AsRef<str>) {
|
||||||
|
let _ = cliclack::log::error(msg.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print a line above any progress bars (maps to cliclack log; synchronized).
|
||||||
|
pub fn println_above_bars(msg: impl AsRef<str>) {
|
||||||
|
if crate::is_quiet() { return; }
|
||||||
|
// cliclack logs are synchronized with its spinners/bars
|
||||||
|
let _ = cliclack::log::info(msg.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Input prompt with a question: returns Ok(None) if non-interactive or canceled
|
||||||
|
pub fn prompt_input(question: impl AsRef<str>, default: Option<&str>) -> anyhow::Result<Option<String>> {
|
||||||
|
if crate::is_no_interaction() || !crate::stdin_is_tty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let mut p = cliclack::input(question.as_ref());
|
||||||
|
if let Some(d) = default {
|
||||||
|
// Use default_input when available in 0.3.x
|
||||||
|
p = p.default_input(d);
|
||||||
|
}
|
||||||
|
match p.interact() {
|
||||||
|
Ok(s) => Ok(Some(s)),
|
||||||
|
Err(_) => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirmation prompt; returns Ok(None) if non-interactive or canceled
|
||||||
|
pub fn prompt_confirm(question: impl AsRef<str>, default_yes: bool) -> anyhow::Result<Option<bool>> {
|
||||||
|
if crate::is_no_interaction() || !crate::stdin_is_tty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let res = cliclack::confirm(question.as_ref())
|
||||||
|
.initial_value(default_yes)
|
||||||
|
.interact();
|
||||||
|
match res {
|
||||||
|
Ok(v) => Ok(Some(v)),
|
||||||
|
Err(_) => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prompt the user (TTY-aware via cliclack) and read a line from stdin. Returns the raw line with trailing newline removed.
|
||||||
|
pub fn prompt_line(prompt: &str) -> io::Result<String> {
|
||||||
|
// Route prompt through cliclack to keep consistent styling and avoid direct eprint!/println!
|
||||||
|
let _ = cliclack::log::info(prompt);
|
||||||
|
let mut s = String::new();
|
||||||
|
io::stdin().read_line(&mut s)?;
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TTY-aware progress UI built on `indicatif` for per-file and aggregate progress bars.
|
||||||
|
///
|
||||||
|
/// This small helper encapsulates a `MultiProgress` with one aggregate (total) bar and
|
||||||
|
/// one per-file bar. It is intentionally minimal to keep integration lightweight.
|
||||||
|
pub mod progress;
|
81
src/ui/progress.rs
Normal file
81
src/ui/progress.rs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// 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.
|
||||||
|
pub struct ProgressManager {
|
||||||
|
enabled: bool,
|
||||||
|
mp: Option<MultiProgress>,
|
||||||
|
per: Vec<ProgressBar>,
|
||||||
|
total: Option<ProgressBar>,
|
||||||
|
completed: 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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a manager that enables bars when `n > 1`, stderr is a TTY, and not quiet.
|
||||||
|
pub fn default_for_files(n: usize) -> Self {
|
||||||
|
let enabled = n > 1 && std::io::stderr().is_terminal() && !crate::is_quiet() && !crate::is_no_progress();
|
||||||
|
Self::new(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize bars for the given file labels. If disabled or single file, no-op.
|
||||||
|
pub fn init_files(&mut self, labels: &[String]) {
|
||||||
|
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");
|
||||||
|
self.total = Some(total);
|
||||||
|
// Per-file bars
|
||||||
|
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());
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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");
|
||||||
|
}
|
||||||
|
self.completed += 1;
|
||||||
|
if let Some(total) = &self.total { total.set_position(self.completed as u64); }
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user