diff --git a/update-engine/src/display/group_display.rs b/update-engine/src/display/group_display.rs index 0e04361ce4b..9e75b64757a 100644 --- a/update-engine/src/display/group_display.rs +++ b/update-engine/src/display/group_display.rs @@ -12,8 +12,9 @@ use swrite::{swrite, SWrite}; use unicode_width::UnicodeWidthStr; use crate::{ - errors::UnknownReportKey, events::EventReport, EventBuffer, - ExecutionStatus, ExecutionTerminalInfo, StepSpec, TerminalKind, + display::ProgressRatioDisplay, errors::UnknownReportKey, + events::EventReport, EventBuffer, ExecutionStatus, ExecutionTerminalInfo, + StepSpec, TerminalKind, }; use super::{ @@ -309,11 +310,13 @@ impl GroupDisplayStats { }; swrite!(line, "{:>HEADER_WIDTH$} ", header.style(header_style)); - let terminal_count = self.terminal_count(); swrite!( line, - "{terminal_count}/{}: {} running, {} {}", - self.total, + "{}: {} running, {} {}", + ProgressRatioDisplay::current_and_total( + self.terminal_count(), + self.total + ), self.running.style(formatter.styles().meta_style), self.completed.style(formatter.styles().meta_style), "completed".style(formatter.styles().progress_style), diff --git a/update-engine/src/display/line_display_shared.rs b/update-engine/src/display/line_display_shared.rs index 99b03b13f76..e31d36dcd7c 100644 --- a/update-engine/src/display/line_display_shared.rs +++ b/update-engine/src/display/line_display_shared.rs @@ -16,6 +16,7 @@ use owo_colors::OwoColorize; use swrite::{swrite, SWrite as _}; use crate::{ + display::ProgressRatioDisplay, events::{ ProgressCounter, ProgressEvent, ProgressEventKind, StepEvent, StepEventKind, StepInfo, StepOutcome, @@ -633,10 +634,12 @@ fn format_progress_counter(counter: &ProgressCounter) -> String { let percent = (counter.current as f64 / total as f64) * 100.0; // <12.34> is 5 characters wide. let percent_width = 5; - let counter_width = total.to_string().len(); format!( - "{:>percent_width$.2}% ({:>counter_width$}/{} {})", - percent, counter.current, total, counter.units, + "{:>percent_width$.2}% ({} {})", + percent, + ProgressRatioDisplay::current_and_total(counter.current, total) + .padded(true), + counter.units, ) } None => format!("{} {}", counter.current, counter.units), @@ -716,17 +719,16 @@ impl LineDisplayFormatter { ) { ld_step_info.nest_data.add_prefix(line); - // Print out "/)". Leave space such that we - // print out e.g. "1/8)" and " 3/14)". - // Add 1 to the index to make it 1-based. - let step_index = ld_step_info.step_info.index + 1; - let step_index_width = ld_step_info.total_steps.to_string().len(); + // Print out "(/)" in a padded way, so that successive + // steps are vertically aligned. swrite!( line, - "{:width$}/{:width$}) ", - step_index, - ld_step_info.total_steps, - width = step_index_width + "({}) ", + ProgressRatioDisplay::index_and_total( + ld_step_info.step_info.index, + ld_step_info.total_steps + ) + .padded(true), ); swrite!( diff --git a/update-engine/src/display/mod.rs b/update-engine/src/display/mod.rs index c58a4535a08..f6775dd37be 100644 --- a/update-engine/src/display/mod.rs +++ b/update-engine/src/display/mod.rs @@ -11,11 +11,14 @@ //! * [`LineDisplay`]: a line-oriented display suitable for the command line. //! * [`GroupDisplay`]: manages state and shows the results of several //! [`LineDisplay`]s at once. +//! * Some utility displayers which can be used to build custom displayers. mod group_display; mod line_display; mod line_display_shared; +mod utils; pub use group_display::GroupDisplay; pub use line_display::{LineDisplay, LineDisplayStyles}; use line_display_shared::*; +pub use utils::*; diff --git a/update-engine/src/display/utils.rs b/update-engine/src/display/utils.rs new file mode 100644 index 00000000000..08790f352b3 --- /dev/null +++ b/update-engine/src/display/utils.rs @@ -0,0 +1,108 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Utility displayers. + +use std::fmt; + +/// Given current and total, displays `{current}/{total}`. +/// +/// * If the `index_and_total` constructor is called, then `current` is `index +/// + 1`. +/// * If `padded` is `true`, `current` is right-aligned and padded with spaces +/// to the width of `total`. +/// +/// # Examples +/// +/// ``` +/// use update_engine::display::ProgressRatioDisplay; +/// +/// // 0-based index and total. +/// let display = ProgressRatioDisplay::index_and_total(0 as u64, 8 as u64); +/// assert_eq!(display.to_string(), "1/8"); +/// +/// // 1-based current and total. +/// let display = ProgressRatioDisplay::current_and_total(82 as u64, 230 as u64); +/// assert_eq!(display.to_string(), "82/230"); +/// +/// // With padding. +/// let display = display.padded(true); +/// assert_eq!(display.to_string(), " 82/230"); +/// ``` +#[derive(Debug)] +pub struct ProgressRatioDisplay { + current: u64, + total: u64, + padded: bool, +} + +impl ProgressRatioDisplay { + /// Create a new `ProgressRatioDisplay` with current and total values. + /// + /// `current` is considered to be 1-based. For example, "20/80 jobs done". + pub fn current_and_total(current: T, total: T) -> Self { + Self { current: current.to_u64(), total: total.to_u64(), padded: false } + } + + /// Create a new `ProgressRatioDisplay` with index and total values. + /// + /// The index is 0-based (i.e. 1 is added to it). For example, step index 0 + /// out of 8 total steps is shown as "1/8". + pub fn index_and_total(index: T, total: T) -> Self { + Self { + current: index + .to_u64() + .checked_add(1) + .expect("index can't be u64::MAX"), + total: total.to_u64(), + padded: false, + } + } + + /// If set to true, the current value is padded to the same width as the + /// total. + pub fn padded(self, padded: bool) -> Self { + Self { padded, ..self } + } +} + +impl fmt::Display for ProgressRatioDisplay { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.padded { + let width = self.total.to_string().len(); + write!(f, "{:>width$}/{}", self.current, self.total) + } else { + write!(f, "{}/{}", self.current, self.total) + } + } +} + +/// Trait that abstracts over `usize` and `u64`. +/// +/// There are no `From` implementations between `usize` and `u64`, but we +/// assert below that all the architectures we support are 64-bit. +pub trait ToU64 { + fn to_u64(self) -> u64; +} + +const _: () = { + assert!( + std::mem::size_of::() == std::mem::size_of::(), + "usize and u64 are the same size" + ); +}; + +impl ToU64 for usize { + #[inline] + fn to_u64(self) -> u64 { + self as u64 + } +} + +impl ToU64 for u64 { + #[inline] + fn to_u64(self) -> u64 { + self + } +} diff --git a/wicket/src/ui/panes/update.rs b/wicket/src/ui/panes/update.rs index 3a61e25a3aa..de34391fccb 100644 --- a/wicket/src/ui/panes/update.rs +++ b/wicket/src/ui/panes/update.rs @@ -29,6 +29,7 @@ use ratatui::widgets::{ use ratatui::Frame; use slog::{info, o, Logger}; use tui_tree_widget::{Tree, TreeItem, TreeState}; +use update_engine::display::ProgressRatioDisplay; use update_engine::{ AbortReason, CompletionReason, ExecutionStatus, FailureReason, StepKey, TerminalKind, WillNotBeRunReason, @@ -1984,9 +1985,11 @@ impl ComponentUpdateListState { )); status_text.push(Span::styled( format!( - " (step {}/{})", - step_key.index + 1, - summary.total_steps, + " (step {})", + ProgressRatioDisplay::index_and_total( + step_key.index, + summary.total_steps, + ) ), style::plain_text(), )); @@ -2015,9 +2018,11 @@ impl ComponentUpdateListState { )); status_text.push(Span::styled( format!( - " at step {}/{}", - info.step_key.index + 1, - summary.total_steps, + " at step {}", + ProgressRatioDisplay::index_and_total( + info.step_key.index, + summary.total_steps, + ) ), style::plain_text(), )); @@ -2033,9 +2038,11 @@ impl ComponentUpdateListState { )); status_text.push(Span::styled( format!( - " at step {}/{}", - info.step_key.index + 1, - summary.total_steps, + " at step {}", + ProgressRatioDisplay::index_and_total( + info.step_key.index, + summary.total_steps, + ) ), style::plain_text(), ));