cli_ui/macros.rs
1//! User-facing macros for runtime output.
2//!
3//! All macros write to **stderr** via [`anstream`], which automatically
4//! strips ANSI codes when output is piped or redirected.
5//!
6//! # Quick reference
7//!
8//! | Macro | Output |
9//! |-------|--------|
10//! | [`header!`](crate::header) | Themed badge + version + tagline |
11//! | [`phase!`](crate::phase) | `▶ init reading file.html` |
12//! | [`step!`](crate::step) | `▸ downloading remote assets` |
13//! | [`substep!`](crate::substep) | `└─ https://… → ./fonts/…` |
14//! | [`ok!`](crate::ok) | `✓ dist/index.html` |
15//! | [`bail!`](crate::bail) | Error + exit(1) |
16//! | [`summary!`](macro@crate::summary) | Aligned summary block |
17
18/// Print the app header with a themed badge.
19///
20/// Reads `badge` from the app theme set in `#[cli(theme = "...")]`.
21/// Use the 4-argument form for the default cyan badge.
22///
23/// # Example
24/// ```rust,no_run
25/// cli_ui::header!("sitefetch", "0.1.0", "offline asset localizer", "all assets offline");
26/// ```
27///
28/// Output:
29/// ```text
30/// sitefetch v0.1.0
31/// offline asset localizer — all assets offline
32/// ```
33#[macro_export]
34macro_rules! header {
35 // with explicit badge style (generated code passes this form)
36 ($name:expr, $version:expr, $about:expr, $tagline:expr, badge=$badge:expr) => {{
37 use $crate::styles::*;
38 eprintln!();
39 eprintln!(
40 " {} {}",
41 paint($badge, &format!(" {} ", $name)),
42 paint(DIM, &format!("v{}", $version)),
43 );
44 eprintln!(" {}", paint(DIM, &format!("{} — {}", $about, $tagline)));
45 eprintln!();
46 }};
47 // default cyan badge
48 ($name:expr, $version:expr, $about:expr, $tagline:expr) => {{
49 use $crate::styles::*;
50 eprintln!();
51 eprintln!(
52 " {} {}",
53 paint(BADGE_CYAN, &format!(" {} ", $name)),
54 paint(DIM, &format!("v{}", $version)),
55 );
56 eprintln!(" {}", paint(DIM, &format!("{} — {}", $about, $tagline)));
57 eprintln!();
58 }};
59}
60
61/// Print a phase line: `▶ tag message`
62///
63/// # Example
64/// ```rust,no_run
65/// cli_ui::phase!("init", "reading {}", "index.html");
66/// // ▶ init reading index.html
67/// ```
68#[macro_export]
69macro_rules! phase {
70 ($tag:expr, $($arg:tt)*) => {{
71 use $crate::styles::*;
72 let msg = format!($($arg)*);
73 eprintln!(
74 " {} {:<6} {}",
75 paint(ACCENT, PHASE),
76 paint(ACCENT, $tag),
77 paint(DIM, &msg),
78 );
79 }};
80}
81
82/// Print a step header: `▸ message`
83///
84/// # Example
85/// ```rust,no_run
86/// cli_ui::step!("downloading remote assets");
87/// // ▸ downloading remote assets
88/// ```
89#[macro_export]
90macro_rules! step {
91 ($($arg:tt)*) => {{
92 use $crate::styles::*;
93 let msg = format!($($arg)*);
94 eprintln!();
95 eprintln!(" {} {}", paint(WHITE, BULLET), paint(WHITE, &msg));
96 }};
97}
98
99/// Print a CSS/asset sub-step: `└─ url → local`
100///
101/// # Example
102/// ```rust,no_run
103/// cli_ui::substep!("https://fonts.gstatic.com/inter.ttf", "./fonts/inter.ttf");
104/// ```
105#[macro_export]
106macro_rules! substep {
107 ($url:expr, $local:expr) => {
108 $crate::progress::substep($url, $local);
109 };
110}
111
112/// Print a success line outside of a [`Progress`](crate::Progress) block.
113///
114/// # Example
115/// ```rust,no_run
116/// cli_ui::ok!("dist/index.html");
117/// // ✓ dist/index.html
118/// ```
119#[macro_export]
120macro_rules! ok {
121 ($path:expr) => {{
122 use $crate::styles::*;
123 eprintln!(
124 " {} {}",
125 paint(OK, CHECK),
126 paint(DIM, &$path.to_string())
127 );
128 }};
129}
130
131/// Print an error message and exit with code 1.
132///
133/// # Example
134/// ```rust,no_run
135/// # let path = std::path::PathBuf::from("/etc/hosts");
136/// cli_ui::bail!("cannot read file: {}", path.display());
137/// ```
138#[macro_export]
139macro_rules! bail {
140 ($($arg:tt)*) => {{
141 $crate::print_error(&format!($($arg)*));
142 ::std::process::exit(1);
143 }};
144}
145
146/// Build and print a styled summary block at the end of a run.
147///
148/// # Syntax
149/// ```rust,no_run
150/// use cli_ui::{summary, styles::{paint, CYAN, YELLOW, DIM, OK}};
151///
152/// summary! {
153/// done: "All assets localized", // or warn: "..." for yellow badge
154/// "input" => paint(CYAN, "in.html"),
155/// "output" => paint(CYAN, "dist/"),
156/// section, // starts new alignment group
157/// "assets" => format!("{} remote", paint(OK, "19")),
158/// "size" => paint(YELLOW, "1.64 MB"),
159/// "time" => paint(DIM, "13034ms"),
160/// }
161/// ```
162///
163/// The `section,` separator resets column alignment — keys in each section
164/// are right-aligned independently.
165#[macro_export]
166macro_rules! summary {
167 (done: $msg:expr, $($rest:tt)*) => {{
168 let s = $crate::summary::Summary::new().done($msg);
169 let s = $crate::__summary_inner!(s, $($rest)*);
170 s.print();
171 }};
172 (warn: $msg:expr, $($rest:tt)*) => {{
173 let s = $crate::summary::Summary::new().warn($msg);
174 let s = $crate::__summary_inner!(s, $($rest)*);
175 s.print();
176 }};
177 ($($rest:tt)*) => {{
178 let s = $crate::summary::Summary::new();
179 let s = $crate::__summary_inner!(s, $($rest)*);
180 s.print();
181 }};
182}
183
184#[doc(hidden)]
185#[macro_export]
186macro_rules! __summary_inner {
187 ($s:expr,) => { $s };
188 ($s:expr) => { $s };
189
190 ($s:expr, done: $msg:expr, $($rest:tt)*) => {{
191 let s = $s.done($msg);
192 $crate::__summary_inner!(s, $($rest)*)
193 }};
194 ($s:expr, done: $msg:expr) => { $s.done($msg) };
195
196 ($s:expr, warn: $msg:expr, $($rest:tt)*) => {{
197 let s = $s.warn($msg);
198 $crate::__summary_inner!(s, $($rest)*)
199 }};
200 ($s:expr, warn: $msg:expr) => { $s.warn($msg) };
201
202 ($s:expr, section, $($rest:tt)*) => {{
203 let s = $s.blank().section();
204 $crate::__summary_inner!(s, $($rest)*)
205 }};
206
207 ($s:expr, $key:literal => $val:expr, $($rest:tt)*) => {{
208 let s = $s.stat($key, &format!("{}", $val));
209 $crate::__summary_inner!(s, $($rest)*)
210 }};
211 ($s:expr, $key:literal => $val:expr) => {
212 $s.stat($key, &format!("{}", $val))
213 };
214}