//! 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; use crossterm::terminal; #[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("=> ") } /// [`transfer_style`] with human-formatted byte counters, for the events /// reported through [`crate::report::BuildView::byte_progress`] pub(crate) fn byte_style() -> ProgressStyle { ProgressStyle::default_bar() .template( "> {spinner:.blue} {prefix}\n {msg} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})", ) .unwrap() .progress_chars("=> ") } /// A standalone spinner for one-line waits outside a build view (the /// prune size pass): braille frames over the message behind the `> ` /// prompt prefix, drawn only on terminals. Callers finish it with /// [`ProgressBar::finish_and_clear`]. pub fn wait_spinner(msg: &str) -> ProgressBar { let pb = ProgressBar::new_spinner(); pb.set_style( ProgressStyle::with_template("> {spinner:.blue} {msg}").expect("spinner template"), ); pb.set_message(msg.to_string()); pb.enable_steady_tick(Duration::from_millis(120)); pb } /// Width of the terminal in columns, or `None` when it cannot be determined pub(crate) fn terminal_width() -> Option { terminal::size().ok().map(|(cols, _)| cols as usize) } /// Removal progress bar of `pkh prune`, in the deb live view's layout: /// the running count behind the spinner on the first line, the byte bar /// with its counters on the second when the plan carries sizes (`--force` /// skips the sizing and gets the count line only). The removed path is /// deliberately not rendered — it fights the bar on a narrow terminal, /// and the failed-removal errors name the paths that matter. Drive it /// with [`removal_progress`]; finish it with /// [`ProgressBar::finish_and_clear`]. pub fn removal_bar(multi: &MultiProgress, total_bytes: u64) -> ProgressBar { let pb = multi.add(ProgressBar::new(total_bytes)); let template = if total_bytes > 0 { "> {spinner:.blue} {prefix}\n [{bar:24.cyan/blue}] {bytes}/{total_bytes}" } else { "> {spinner:.blue} {prefix}" }; pb.set_style( ProgressStyle::with_template(template) .expect("removal template") .progress_chars("=> "), ); pb.enable_steady_tick(Duration::from_millis(120)); pb } /// Advance the removal bar to the `idx`-th of `count` artifact; /// `done` bytes already freed pub fn removal_progress(bar: &ProgressBar, idx: usize, count: usize, done: u64) { bar.set_prefix(format!("Removing {}/{}", idx + 1, count)); if let Some(total) = bar.length() && total > 0 { bar.set_position(done.min(total)); } } /// Terminal build view for the pull flow: one bar line attached to the /// shared [`MultiProgress`], spinner-style while the work is unsized, /// transfer-style once a byte or item total is known. The bar is created /// eagerly (an empty spinner, so the line exists from the first moment) /// and released on drop. pub struct PullView { multi: MultiProgress, pb: ProgressBar, } impl PullView { /// Attach a pull view to `multi` pub fn new(multi: &MultiProgress) -> Self { let pb = multi.add(ProgressBar::new(0)); pb.enable_steady_tick(Duration::from_millis(50)); pb.set_style(spinner_style()); Self { multi: multi.clone(), pb, } } } impl Drop for PullView { fn drop(&mut self) { self.pb.finish_and_clear(); self.multi.remove(&self.pb); } } impl crate::report::BuildView for PullView { fn message(&self, text: &str) { self.pb.set_style(spinner_style()); self.pb.set_message(text.to_string()); } fn progress(&self, label: &str, pos: usize, total: usize) { if pos != 0 && total != 0 { self.pb.set_style(transfer_style()); } else { self.pb.set_style(spinner_style()); } self.pb.set_message(label.to_string()); self.pb.set_length(total as u64); self.pb.set_position(pos as u64); } fn byte_progress(&self, label: &str, pos: u64, total: u64) { if pos != 0 && total != 0 { self.pb.set_style(byte_style()); } else { self.pb.set_style(spinner_style()); } self.pb.set_message(label.to_string()); self.pb.set_length(total); self.pb.set_position(pos); } fn is_enabled(&self) -> bool { true } } /// Label of the interactive series selector, rendered by [`prompt::select`] /// as `>