cli_ui/summary.rs
1//! Summary block renderer.
2//!
3//! Used via the [`summary!`](macro@crate::summary) macro at the end of a command run.
4//!
5//! # Example
6//! ```rust,no_run
7//! use cli_ui::summary;
8//! use cli_ui::styles::{paint, CYAN, YELLOW, DIM, OK};
9//!
10//! summary! {
11//! done: "All assets localized",
12//! "input" => paint(CYAN, "index.html"),
13//! "output" => paint(CYAN, "dist/index.html"),
14//! section,
15//! "assets" => format!("{} remote · {} local", paint(OK, "19"), paint(OK, "0")),
16//! "size" => paint(YELLOW, "1.64 MB"),
17//! "time" => paint(DIM, "13034ms"),
18//! }
19//! ```
20//!
21//! Output:
22//! ```text
23//! ────────────────────────────────────────────────
24//!
25//! DONE All assets localized
26//!
27//! input index.html
28//! output dist/index.html
29//!
30//! assets 19 remote · 0 local
31//! size 1.64 MB
32//! time 13034ms
33//!
34//! ────────────────────────────────────────────────
35//! ```
36
37use crate::styles::*;
38use crate::term;
39
40/// Builder for the end-of-run summary block.
41///
42/// Construct via the [`summary!`](crate::summary!) macro rather than directly.
43pub struct Summary {
44 sections: Vec<SummarySection>,
45}
46
47struct SummarySection {
48 entries: Vec<SummaryEntry>,
49}
50
51enum SummaryEntry {
52 /// Green ` DONE ` badge with message.
53 Done(String),
54 /// Yellow ` WARN ` badge with message.
55 Warn(String),
56 /// Key/value stat — keys are right-aligned per section.
57 Stat { key: String, val: String },
58 /// Blank line separator.
59 Blank,
60}
61
62impl Summary {
63 /// Create an empty summary builder.
64 pub fn new() -> Self {
65 Self {
66 sections: vec![SummarySection {
67 entries: Vec::new(),
68 }],
69 }
70 }
71
72 /// Add a green `DONE` status line.
73 pub fn done(mut self, msg: &str) -> Self {
74 self.cur().entries.push(SummaryEntry::Done(msg.to_string()));
75 self
76 }
77
78 /// Add a yellow `WARN` status line (used when there are errors).
79 pub fn warn(mut self, msg: &str) -> Self {
80 self.cur().entries.push(SummaryEntry::Warn(msg.to_string()));
81 self
82 }
83
84 /// Add a key/value stat line.
85 ///
86 /// Keys are right-aligned to the longest key in the same section.
87 /// Values are printed as-is (may contain ANSI codes from [`paint`]).
88 pub fn stat(mut self, key: &str, val: &str) -> Self {
89 self.cur().entries.push(SummaryEntry::Stat {
90 key: key.to_string(),
91 val: val.to_string(),
92 });
93 self
94 }
95
96 /// Add a blank line within the current section.
97 pub fn blank(mut self) -> Self {
98 self.cur().entries.push(SummaryEntry::Blank);
99 self
100 }
101
102 /// Start a new alignment section.
103 ///
104 /// Keys in different sections are aligned independently, so a long key
105 /// in the stats block doesn't push the files block out of alignment.
106 pub fn section(mut self) -> Self {
107 self.sections.push(SummarySection {
108 entries: Vec::new(),
109 });
110 self
111 }
112
113 /// Render the summary to stderr.
114 pub fn print(self) {
115 let w = term::width().min(80);
116 let rule = paint(DIM, &"─".repeat(w));
117
118 eprintln!();
119 eprintln!(" {rule}");
120 eprintln!();
121
122 for section in &self.sections {
123 // right-align keys: find max width per section
124 let max_key = section
125 .entries
126 .iter()
127 .filter_map(|e| match e {
128 SummaryEntry::Stat { key, .. } => Some(key.len()),
129 _ => None,
130 })
131 .max()
132 .unwrap_or(0);
133
134 for entry in §ion.entries {
135 match entry {
136 SummaryEntry::Done(msg) => {
137 let badge = paint(
138 anstyle::Style::new()
139 .bg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Green)))
140 .fg_color(Some(anstyle::Color::Ansi(
141 anstyle::AnsiColor::BrightWhite,
142 )))
143 .bold(),
144 " DONE ",
145 );
146 eprintln!(" {} {}", badge, paint(OK, msg));
147 }
148 SummaryEntry::Warn(msg) => {
149 let badge = paint(
150 anstyle::Style::new()
151 .bg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Yellow)))
152 .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Black)))
153 .bold(),
154 " WARN ",
155 );
156 eprintln!(" {} {}", badge, paint(YELLOW, msg));
157 }
158 SummaryEntry::Stat { key, val } => {
159 let pad = " ".repeat(max_key - key.len());
160 eprintln!(" {}{} {}", pad, paint(DIM, key), val);
161 }
162 SummaryEntry::Blank => eprintln!(),
163 }
164 }
165 }
166
167 eprintln!();
168 eprintln!(" {rule}");
169 eprintln!();
170 }
171
172 fn cur(&mut self) -> &mut SummarySection {
173 self.sections.last_mut().unwrap()
174 }
175}
176
177impl Default for Summary {
178 fn default() -> Self {
179 Self::new()
180 }
181}