Files
pkh/src/ui.rs
T
vhaudiquet d64e472845 report: grow the Prompter port and move display_path out of the ui module
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.
2026-09-18 20:34:33 +02:00

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