Prompter gains interactive(), select() and text() (with the Validator type), and confirm() now propagates cancellation as Err so flows abort instead of silently taking a default when the user hits Ctrl+C. The terminal prompter implements the full port; the port also re-exports the path display helper, which is pure presentation formatting used by events and messages rather than terminal code.
110 lines
3.7 KiB
Rust
110 lines
3.7 KiB
Rust
//! Terminal UI helpers: progress bars, interactive prompts and live build
|
|
//! views.
|
|
|
|
/// Live build view for `pkh deb` (status bar + rolling log pane)
|
|
pub mod deb;
|
|
/// Interactive raw-mode prompts: free-text input, option selection and
|
|
/// yes/no confirmation
|
|
pub mod prompt;
|
|
|
|
#[cfg(test)]
|
|
use indicatif::ProgressDrawTarget;
|
|
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
|
use std::time::Duration;
|
|
|
|
/// Style of an unsized operation: spinner and prefix on one line
|
|
pub(crate) fn spinner_style() -> ProgressStyle {
|
|
ProgressStyle::default_bar()
|
|
.template("> {spinner:.blue} {prefix}")
|
|
.unwrap()
|
|
}
|
|
|
|
/// Progress draw target for a [`MultiProgress`]: hidden in test runs
|
|
///
|
|
/// Steady-tick spinners redraw from a background thread straight to the real
|
|
/// stderr, bypassing both the test harness capture and the per-test log
|
|
/// files; in tests there is no terminal to animate anyway.
|
|
#[cfg(test)]
|
|
pub(crate) fn progress_draw_target() -> ProgressDrawTarget {
|
|
ProgressDrawTarget::hidden()
|
|
}
|
|
|
|
/// Style of a sized transfer: prefix on the first line, the bar on its own
|
|
/// indented line below so long prefixes cannot push it out of the terminal
|
|
pub(crate) fn transfer_style() -> ProgressStyle {
|
|
ProgressStyle::default_bar()
|
|
.template(
|
|
"> {spinner:.blue} {prefix}\n {msg} [{bar:40.cyan/blue}] {pos}/{len} ({eta})",
|
|
)
|
|
.unwrap()
|
|
.progress_chars("=> ")
|
|
}
|
|
|
|
/// Create a spinner-style progress bar attached to `multi`, returning the bar
|
|
/// and a callback compatible with [`crate::ProgressCallback`]
|
|
pub fn create_progress_bar(
|
|
multi: &MultiProgress,
|
|
) -> (ProgressBar, impl Fn(&str, &str, usize, usize) + '_) {
|
|
let pb = multi.add(ProgressBar::new(0));
|
|
pb.enable_steady_tick(Duration::from_millis(50));
|
|
pb.set_style(spinner_style());
|
|
|
|
let pb_clone = pb.clone();
|
|
let callback = move |prefix: &str, msg: &str, progress: usize, total: usize| {
|
|
let pb = &pb_clone;
|
|
if progress != 0 && total != 0 {
|
|
pb.set_style(transfer_style());
|
|
} else {
|
|
pb.set_style(spinner_style());
|
|
}
|
|
|
|
if !prefix.is_empty() {
|
|
pb.set_prefix(prefix.to_string());
|
|
}
|
|
|
|
pb.set_message(msg.to_string());
|
|
pb.set_length(total as u64);
|
|
pb.set_position(progress as u64);
|
|
};
|
|
|
|
(pb, callback)
|
|
}
|
|
|
|
/// Label of the interactive series selector, rendered by [`prompt::select`]
|
|
/// as `> <label><choice>`
|
|
const SELECT_SERIES_LABEL: &str = "Select series: ";
|
|
|
|
/// Interactive one-line series selector with arrow key navigation and direct typing.
|
|
///
|
|
/// The user can:
|
|
/// - Navigate with ↑/↓ arrow keys to cycle through options
|
|
/// - Type directly to enter a custom release name
|
|
/// - Press Tab to cycle through options matching the current input prefix
|
|
/// - Press Enter to confirm, Escape to use the default
|
|
///
|
|
/// Returns the selected series name, or an error if cancelled (Ctrl+C).
|
|
/// Without a terminal (piped input) or with no options, falls back to the
|
|
/// default.
|
|
pub fn select_series(
|
|
options: &[String],
|
|
default: &str,
|
|
) -> Result<String, Box<dyn std::error::Error>> {
|
|
// Nothing to choose, or no terminal to ask: announce the default and take it
|
|
let fallback = || {
|
|
println!("> {SELECT_SERIES_LABEL}{default} (default)");
|
|
Ok(default.to_string())
|
|
};
|
|
|
|
if options.is_empty() {
|
|
return fallback();
|
|
}
|
|
|
|
match prompt::select(SELECT_SERIES_LABEL, options, default) {
|
|
Ok(series) => Ok(series),
|
|
// prompt::select refuses to answer without a TTY; keep the
|
|
// historical fallback to the default
|
|
Err(err) if err.is::<prompt::PromptError>() => fallback(),
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|