pub fn paint(style: Style, s: &str) -> StringExpand description
Apply an anstyle::Style to s and return an owned String.
Respects NO_COLOR and pipe detection via anstream.
§Example
use cli_ui::styles::{paint, OK, CHECK};
let line = format!("{} done", paint(OK, CHECK));Examples found in repository?
examples/subcommands.rs (line 140)
125fn download(g: &Global, opt: DownloadOpt) {
126 header!(
127 "mytool",
128 env!("CARGO_PKG_VERSION"),
129 "batch file processor",
130 "fast and parallel"
131 );
132 phase!("download", "{}", opt.url);
133 if g.verbose {
134 phase!("debug", "output={} jobs={}", opt.output.display(), opt.jobs);
135 }
136 step!("fetching");
137 ok!(opt.output.join("file.csv").display());
138 summary! {
139 done: "Download complete",
140 "url" => paint(CYAN, &opt.url),
141 "output" => paint(CYAN, &opt.output.display().to_string()),
142 section,
143 "size" => paint(YELLOW, &format_bytes(43_100)),
144 "time" => paint(DIM, "320ms"),
145 }
146}
147
148fn upload(g: &Global, opt: UploadOpt) {
149 header!(
150 "mytool",
151 env!("CARGO_PKG_VERSION"),
152 "batch file processor",
153 "fast and parallel"
154 );
155 phase!("upload", "{} → {}", opt.file.display(), opt.remote);
156 if g.verbose {
157 phase!("debug", "compress={}", opt.compress);
158 }
159 step!("uploading");
160 ok!(opt.remote.as_str());
161 summary! {
162 done: "Upload complete",
163 "file" => paint(CYAN, &opt.file.display().to_string()),
164 "remote" => paint(CYAN, &opt.remote),
165 }
166}
167
168fn net_download(g: &Global, opt: NetDownloadOpt) {
169 header!(
170 "mytool",
171 env!("CARGO_PKG_VERSION"),
172 "batch file processor",
173 "fast and parallel"
174 );
175 phase!("net-download", "{} (timeout: {}s)", opt.url, opt.timeout);
176 if g.verbose {
177 phase!("debug", "using network stack");
178 }
179 step!("fetching");
180 ok!("./data/file.bin");
181 summary! {
182 done: "Download complete",
183 "url" => paint(CYAN, &opt.url),
184 "time" => paint(DIM, "120ms"),
185 }
186}
187
188fn net_ping(g: &Global) {
189 header!(
190 "mytool",
191 env!("CARGO_PKG_VERSION"),
192 "batch file processor",
193 "fast and parallel"
194 );
195 phase!("ping", "checking connectivity...");
196 if g.verbose {
197 phase!("debug", "sending ICMP");
198 }
199 summary! {
200 done: "Reachable",
201 "latency" => paint(OK, "12ms"),
202 }
203}
204
205fn status(g: &Global) {
206 header!(
207 "mytool",
208 env!("CARGO_PKG_VERSION"),
209 "batch file processor",
210 "fast and parallel"
211 );
212 phase!("status", "checking...");
213 if g.verbose {
214 phase!("debug", "reading config");
215 }
216 summary! {
217 done: "All systems operational",
218 "version" => paint(OK, env!("CARGO_PKG_VERSION")),
219 "config" => paint(DIM, &g.config.as_deref()
220 .map(|p| p.display().to_string())
221 .unwrap_or_else(|| "default".into())),
222 }
223}More examples
examples/mini_cargo.rs (line 716)
697fn cmd_build(opt: &BuildOpt, global: &Global) {
698 let mode = if opt.release { "release" } else { "dev" };
699 phase!("compile", "Compiling packages in {} mode", mode);
700 if let Some(ref e) = opt.example {
701 phase!("target", "example = {e}");
702 }
703 if let Some(ref b) = opt.bin {
704 phase!("target", "bin = {b}");
705 }
706 if let Some(ref t) = opt.target {
707 phase!("target", "target = {t}");
708 }
709 if let Some(ref f) = opt.features {
710 phase!("features", "{f}");
711 }
712 fake_compile(opt.jobs);
713 ok!("target/debug/mini_cargo");
714 summary! {
715 done: "Build finished",
716 "mode" => paint(OK, mode),
717 "jobs" => paint(DIM, &opt.jobs.to_string()),
718 "verbose" => paint(DIM, &global.verbose.to_string()),
719 }
720}
721
722fn cmd_run(opt: &RunOpt, global: &Global) {
723 let mode = if opt.release { "release" } else { "dev" };
724 phase!("compile", "Compiling in {} mode", mode);
725 if let Some(ref e) = opt.example {
726 phase!("run", "example = {e}");
727 }
728 if let Some(ref b) = opt.bin {
729 phase!("run", "bin = {b}");
730 }
731 if let Some(ref f) = opt.features {
732 phase!("features", "{f}");
733 }
734 fake_compile(4);
735 phase!("running", "Running `target/{}/...`", mode);
736 summary! {
737 done: "Run complete",
738 "mode" => paint(OK, mode),
739 "quiet" => paint(DIM, &global.quiet.to_string()),
740 }
741}
742
743fn cmd_add(opt: &AddOpt, _global: &Global) {
744 let kind = if opt.dev {
745 "dev-dependency"
746 } else if opt.build {
747 "build-dependency"
748 } else {
749 "dependency"
750 };
751 phase!("resolve", "Resolving {} `{}`", kind, opt.crate_name);
752 if let Some(ref f) = opt.features {
753 phase!("features", "enabling: {f}");
754 }
755 if opt.dry_run {
756 phase!("dry-run", "would write to Cargo.toml (dry run)");
757 } else {
758 phase!("update", "Updating Cargo.toml");
759 phase!("lock", "Updating Cargo.lock");
760 }
761 summary! {
762 done: "Dependency added",
763 "crate" => paint(CYAN, &opt.crate_name),
764 "kind" => paint(DIM, kind),
765 }
766}
767
768fn cmd_remove(opt: &RemoveOpt, _global: &Global) {
769 phase!("remove", "Removing `{}` from Cargo.toml", opt.crate_name);
770 summary! {
771 done: "Dependency removed",
772 "crate" => paint(CYAN, &opt.crate_name),
773 }
774}
775
776fn cmd_new(opt: &NewOpt, _global: &Global) {
777 let kind = if opt.lib { "library" } else { "binary" };
778 let name = opt
779 .name
780 .as_deref()
781 .or_else(|| opt.path.file_name()?.to_str())
782 .unwrap_or("my-package");
783 phase!("create", "Creating {} package `{}`", kind, name);
784 phase!("edition", "edition = {}", opt.edition);
785 if let Some(ref v) = opt.vcs {
786 phase!("vcs", "initializing {} repo", v);
787 }
788 ok!(opt.path.display());
789 summary! {
790 done: "Package created",
791 "name" => paint(CYAN, name),
792 "kind" => paint(DIM, kind),
793 "edition" => paint(DIM, &opt.edition),
794 }
795}
796
797fn cmd_init(opt: &InitOpt, _global: &Global) {
798 let dir = opt
799 .path
800 .as_deref()
801 .unwrap_or_else(|| std::path::Path::new("."));
802 let kind = if opt.lib { "library" } else { "binary" };
803 phase!(
804 "init",
805 "Initializing {} package in `{}`",
806 kind,
807 dir.display()
808 );
809 if let Some(ref v) = opt.vcs {
810 phase!("vcs", "initializing {} repo", v);
811 }
812 summary! {
813 done: "Package initialized",
814 "dir" => paint(CYAN, &dir.display().to_string()),
815 "edition" => paint(DIM, &opt.edition),
816 }
817}
818
819fn cmd_test(opt: &TestOpt, _global: &Global) {
820 let mode = if opt.release { "release" } else { "dev" };
821 phase!("compile", "Compiling test targets in {} mode", mode);
822 if let Some(ref f) = opt.filter {
823 phase!("filter", "running tests matching `{f}`");
824 }
825 if let Some(ref f) = opt.features {
826 phase!("features", "{f}");
827 }
828 fake_compile(4);
829 step!("running {} test thread(s)", opt.test_threads);
830 eprintln!();
831 eprintln!(
832 " {} test result: {}. {} passed; 0 failed",
833 paint(DIM, "│"),
834 paint(OK, "ok"),
835 paint(OK, "42"),
836 );
837 eprintln!();
838 summary! {
839 done: "Tests passed",
840 "mode" => paint(OK, mode),
841 "threads" => paint(DIM, &opt.test_threads.to_string()),
842 "passed" => paint(OK, "42"),
843 "failed" => paint(DIM, "0"),
844 }
845}
846
847fn cmd_check(opt: &CheckOpt, _global: &Global) {
848 phase!("check", "Checking package");
849 if opt.all_targets {
850 phase!("targets", "checking all targets");
851 }
852 if let Some(ref f) = opt.features {
853 phase!("features", "{f}");
854 }
855 if let Some(ref t) = opt.target {
856 phase!("target", "{t}");
857 }
858 fake_compile(4);
859 summary! { done: "Check passed" }
860}
861
862fn cmd_clean(opt: &CleanOpt, _global: &Global) {
863 let mode = if opt.release { "release" } else { "debug" };
864 phase!("clean", "Removing target/{}", mode);
865 if let Some(ref p) = opt.package {
866 phase!("package", "{p}");
867 }
868 summary! {
869 done: "Clean complete",
870 "mode" => paint(DIM, mode),
871 }
872}
873
874fn cmd_metadata(opt: &MetadataOpt, _global: &Global) {
875 phase!("metadata", "Resolving dependency graph");
876 if opt.no_deps {
877 phase!("filter", "excluding transitive dependencies");
878 }
879 eprintln!();
880 eprintln!(
881 " {} {{\"packages\":[...],\"workspace_root\":\"{}\"}}",
882 paint(DIM, "│"),
883 paint(DIM, "."),
884 );
885 eprintln!();
886 summary! {
887 done: "Metadata output",
888 "version" => paint(DIM, &opt.format_version.to_string()),
889 }
890}
891
892fn cmd_update(opt: &UpdateOpt, _global: &Global) {
893 if let Some(ref c) = opt.crate_name {
894 phase!("update", "Updating `{}`", c);
895 } else {
896 phase!("update", "Updating all dependencies");
897 }
898 if opt.dry_run {
899 phase!("dry-run", "would update Cargo.lock (dry run)");
900 }
901 summary! { done: "Lock file updated" }
902}
903
904fn cmd_info(opt: &InfoOpt, _global: &Global) {
905 phase!("fetch", "Fetching info for `{}`", opt.crate_name);
906 if let Some(ref v) = opt.version {
907 phase!("version", "showing v{v}");
908 }
909 eprintln!();
910 eprintln!(
911 " {} {} v1.0.0",
912 paint(DIM, "│"),
913 paint(CYAN, &opt.crate_name)
914 );
915 eprintln!(
916 " {} {}",
917 paint(DIM, "│"),
918 paint(DIM, "A well-known Rust crate (emulated)")
919 );
920 eprintln!(
921 " {} https://crates.io/crates/{}",
922 paint(DIM, "│"),
923 opt.crate_name
924 );
925 eprintln!();
926 summary! {
927 done: "Info retrieved",
928 "crate" => paint(CYAN, &opt.crate_name),
929 }
930}
931
932fn cmd_import(opt: &ImportOpt, _global: &Global) {
933 phase!("read", "Reading CSV manifest `{}`", opt.file.display());
934 if opt.dry_run {
935 phase!("dry-run", "would update Cargo.toml (dry run)");
936 } else {
937 phase!("update", "Updating Cargo.toml");
938 }
939 summary! {
940 done: "Import complete",
941 "file" => paint(CYAN, &opt.file.display().to_string()),
942 }
943}
944
945// ── Helpers ───────────────────────────────────────────────────────────────────
946
947fn fake_compile(jobs: usize) {
948 let pkgs = ["proc-macro2", "quote", "syn", "serde", "cli-ui"];
949 step!("compiling {} packages ({} jobs)", pkgs.len(), jobs);
950 eprintln!();
951 for pkg in &pkgs {
952 eprintln!(
953 " {} {} {}",
954 paint(DIM, "│"),
955 paint(DIM, "Compiling"),
956 paint(CYAN, pkg),
957 );
958 }
959 eprintln!();
960}examples/validation.rs (line 330)
268fn main() {
269 let opt = Opt::parse();
270 let input = opt.resolved_alt_input();
271 let output = opt.resolved_alt_output();
272 let start = std::time::Instant::now();
273
274 header!(
275 "processor",
276 env!("CARGO_PKG_VERSION"),
277 "process and transform data files",
278 "fast, validated, reliable"
279 );
280
281 let fmt = if opt.json {
282 "json"
283 } else if opt.csv {
284 "csv"
285 } else if opt.toml {
286 "toml"
287 } else {
288 "default"
289 };
290
291 phase!("init", "reading {}", input.display());
292 phase!("cfg", "format={} jobs={} port={}", fmt, opt.jobs, opt.port);
293
294 if opt.dry_run {
295 phase!("mode", "dry run — nothing will be written");
296 }
297 if opt.verbose {
298 phase!("debug", "verbose output enabled");
299 }
300 if opt.sign {
301 phase!("auth", "signing enabled, key={:?}", opt.key);
302 }
303 if opt.no_auth {
304 phase!("auth", "authentication disabled");
305 }
306 if let Some(ref p) = opt.pattern {
307 phase!("filter", "pattern: {p}");
308 }
309 if let Some(ref n) = opt.worker_name {
310 phase!("worker", "name: {n}");
311 }
312 if let Some(ref p) = opt.profile {
313 phase!("profile", "{p}");
314 }
315 if let Some(ref e) = opt.example {
316 phase!("example", "{e}");
317 }
318 if let Some(ref b) = opt.bin {
319 phase!("bin", "{b}");
320 }
321
322 if !opt.dry_run {
323 let out = output.join(format!("result.{fmt}"));
324 ok!(out.display());
325 }
326
327 let elapsed = start.elapsed().as_millis();
328 summary! {
329 done: "Processing complete",
330 "input" => paint(CYAN, &input.display().to_string()),
331 "output" => paint(CYAN, &output.display().to_string()),
332 section,
333 "format" => paint(OK, fmt),
334 "jobs" => paint(OK, &opt.jobs.to_string()),
335 "port" => paint(YELLOW, &opt.port.to_string()),
336 "time" => paint(DIM, &format!("{elapsed}ms")),
337 }
338}examples/mytool.rs (line 195)
82fn main() {
83 // parse() handles --help, --version, --completions,
84 // unknown flags, and missing positionals automatically
85 let opt = Opt::parse();
86
87 // resolved_alt_input() generated from conflicts_with = "input"
88 let input = opt.resolved_alt_input();
89 let output = opt.resolved_alt_output();
90 let start = std::time::Instant::now();
91
92 // bail! prints error and exits 1
93 if !input.exists() {
94 bail!("input path does not exist: {}", input.display());
95 }
96
97 header!(
98 "mytool",
99 env!("CARGO_PKG_VERSION"),
100 "batch file processor",
101 "fast and parallel"
102 );
103
104 phase!("init", "scanning {}", input.display());
105 phase!(
106 "scan",
107 "found {} remote · {} local jobs: {}",
108 8,
109 3,
110 opt.jobs
111 );
112
113 if opt.dry_run {
114 phase!("mode", "dry run — nothing will be written");
115 }
116 if !opt.include.is_empty() {
117 phase!("filter", "include: {}", opt.include.join(", "));
118 }
119 if !opt.exclude.is_empty() {
120 phase!("filter", "exclude: {}", opt.exclude.join(", "));
121 }
122
123 // ── remote files ──────────────────────────────────────────────────
124 let remote: &[(&str, &str, usize)] = &[
125 ("https://example.com/data.csv", "./data/data.csv", 43_100),
126 ("https://example.com/ref.json", "./data/ref.json", 8_500),
127 (
128 "https://example.com/schema.json",
129 "./data/schema.json",
130 2_300,
131 ),
132 ("https://cdn.example.com/lib.js", "./js/lib.js", 128_000),
133 (
134 "https://cdn.example.com/style.css",
135 "./css/style.css",
136 12_400,
137 ),
138 ];
139
140 step!("downloading remote files");
141 let pb = Progress::new(remote.len());
142 let mut errors = 0usize;
143 for (url, local, bytes) in remote {
144 // simulate one failure
145 if url.contains("lib.js") {
146 pb.fail("file", url, "connection timeout");
147 errors += 1;
148 } else {
149 pb.ok("file", "remote", url, local, *bytes);
150 }
151 }
152 pb.finish();
153
154 // ── local files ───────────────────────────────────────────────────
155 let local: &[(&str, &str, usize)] = &[
156 ("config.toml", "./cfg/config.toml", 1_200),
157 ("rules.yaml", "./cfg/rules.yaml", 3_400),
158 ("README.md", "./README.md", 8_800),
159 ];
160
161 step!("copying local files");
162 let pb2 = Progress::new(local.len());
163 for (src, dst, bytes) in local {
164 pb2.ok("file", "local", src, dst, *bytes);
165 }
166 pb2.finish();
167
168 // ── nested operations ─────────────────────────────────────────────
169 step!("resolving references in style.css");
170 substep!(
171 "https://fonts.example.com/Inter.woff2",
172 "./fonts/Inter.woff2"
173 );
174 substep!(
175 "https://fonts.example.com/JetBrains.woff2",
176 "./fonts/JetBrains.woff2"
177 );
178
179 // ── write ─────────────────────────────────────────────────────────
180 if !opt.dry_run {
181 step!("writing output");
182 ok!(output.join(format!("result.{}", opt.format)).display());
183 }
184
185 // ── summary ───────────────────────────────────────────────────────
186 let remote_ok = remote.len() - errors;
187 let total_files = remote_ok + local.len();
188 let total_bytes: usize = remote
189 .iter()
190 .filter(|(u, _, _)| !u.contains("lib.js"))
191 .map(|(_, _, b)| b)
192 .chain(local.iter().map(|(_, _, b)| b))
193 .sum();
194
195 let input_val = paint(CYAN, &input.display().to_string());
196 let output_val = paint(
197 BOLD,
198 &output
199 .join(format!("result.{}", opt.format))
200 .display()
201 .to_string(),
202 );
203 let files_val = format!(
204 "{} total · {} errors",
205 paint(OK, &total_files.to_string()),
206 paint(if errors > 0 { ERR } else { DIM }, &errors.to_string())
207 );
208 let size_val = paint(YELLOW, &format_bytes(total_bytes));
209 let time_val = paint(DIM, &format!("{}ms", start.elapsed().as_millis()));
210
211 if errors == 0 {
212 summary! {
213 done: "All files processed",
214 "input" => input_val,
215 "output" => output_val,
216 section,
217 "files" => files_val,
218 "size" => size_val,
219 "time" => time_val,
220 }
221 } else {
222 summary! {
223 warn: "Completed with errors",
224 "input" => input_val,
225 "output" => output_val,
226 section,
227 "files" => files_val,
228 "size" => size_val,
229 "time" => time_val,
230 }
231 }
232}examples/prompt.rs (line 178)
17fn main() -> cli_ui::prompt::Result<()> {
18 header!(
19 "prompt",
20 env!("CARGO_PKG_VERSION"),
21 "interactive prompt showcase",
22 "all prompt types"
23 );
24
25 intro("Set up a new project");
26
27 // ── 1. Single select with per-option hints ────────────────────────────────
28 let template = select("How would you like to start your new project?")
29 .option("basic", "A basic, helpful starter project")
30 .option("blog", "Use blog template")
31 .hint("Markdown-based blog with RSS feed support")
32 .option("docs", "Use docs (Starlight) template")
33 .hint("Documentation site with search and sidebar")
34 .option("minimal", "Use minimal (empty) template")
35 .hint("Just a Cargo.toml and src/lib.rs — nothing else")
36 .run()?;
37
38 // ── 2. Text input with validation ─────────────────────────────────────────
39 let dir = text("Where should we create your new project?")
40 .default("./my-app")
41 .placeholder("e.g. ./my-project")
42 .hint("Use a relative or absolute path")
43 .validate(|s| {
44 if s.starts_with('.') || s.starts_with('/') {
45 Ok(())
46 } else {
47 Err("path must start with . or /".into())
48 }
49 })
50 .run()?;
51
52 // ── 3. Multi select with per-option hints ─────────────────────────────────
53 let features = multiselect("Which features would you like to enable?")
54 .option("ts", "TypeScript")
55 .hint("JavaScript with syntax for types")
56 .option("tailwind", "Tailwind CSS")
57 .hint("A utility-first CSS framework")
58 .option("react", "React")
59 .hint("A JavaScript library for building user interfaces")
60 .option("vue", "Vue")
61 .hint("The Progressive JavaScript Framework")
62 .option("eslint", "ESLint")
63 .hint("Find and fix problems in your JavaScript code")
64 .run()?;
65
66 // ── 4. Grouped multiselect ────────────────────────────────────────────────
67 let tools = groupmultiselect("Select development tools:")
68 .group("Frontend")
69 .item("TypeScript")
70 .hint("JavaScript with syntax for types")
71 .item("ESLint")
72 .hint("Find and fix problems in your JS code")
73 .item("Prettier")
74 .hint("An opinionated code formatter")
75 .group("Backend")
76 .item("Node.js")
77 .hint("JavaScript runtime built on V8")
78 .item("Express")
79 .hint("Fast, unopinionated web framework")
80 .item("Prisma")
81 .hint("Next-generation ORM for Node.js")
82 .group("Testing")
83 .item("Jest")
84 .hint("Delightful JavaScript testing framework")
85 .item("Cypress")
86 .hint("End-to-end testing for the modern web")
87 .item("Vitest")
88 .hint("Vite-native unit testing framework")
89 .run()?;
90
91 // ── 5. Autocomplete ───────────────────────────────────────────────────────
92 let pkg = autocomplete("Search for a UI package:")
93 .option("astro", "Astro")
94 .hint("The web framework for content-based sites")
95 .option("react", "React")
96 .hint("A JavaScript library for building user interfaces")
97 .option("vue", "Vue")
98 .hint("The Progressive JavaScript Framework")
99 .option("svelte", "Svelte")
100 .hint("Cybernetically enhanced web apps")
101 .option("angular", "Angular")
102 .hint("Platform for building mobile & desktop web apps")
103 .option("solid", "SolidJS")
104 .hint("Simple and performant reactivity for building UIs")
105 .option("qwik", "Qwik")
106 .hint("HTML-first framework with instant interactivity")
107 .placeholder("Type to search...")
108 .max_items(5)
109 .run()?;
110
111 // ── 6. Confirm ────────────────────────────────────────────────────────────
112 let git = confirm("Initialize a new git repository?")
113 .default(true)
114 .run()?;
115
116 // ── 7. Secret ─────────────────────────────────────────────────────────────
117 let _token = secret("Enter your API token (optional)")
118 .allow_empty(true)
119 .hint("Leave empty to skip — you can set it later in .env")
120 .run()?;
121
122 // ── 8. Single-keypress select ─────────────────────────────────────────────
123 let action = select_key("Apply changes?")
124 .option('y', "Yes, apply")
125 .hint("write to disk")
126 .option('n', "No, abort")
127 .hint("discard everything")
128 .option('d', "Diff first")
129 .hint("preview the changes")
130 .run()?;
131 log::info(format!("you chose: {} ({})", action.key, action.label));
132
133 note(
134 "What happens next",
135 "We'll scaffold your project, install dependencies,\nand initialise git if you opted in.",
136 );
137
138 // ── 9. Spinner + tasks runner ─────────────────────────────────────────────
139 let s = spinner();
140 s.start("Resolving dependencies");
141 std::thread::sleep(std::time::Duration::from_millis(700));
142 s.message("Downloading 42 packages");
143 std::thread::sleep(std::time::Duration::from_millis(700));
144 s.stop("Resolved 42 packages");
145
146 // ── 10. Progress bar ──────────────────────────────────────────────────────
147 // Filled/empty bar rendered inside a spinner line. Advance in a loop
148 // (or from a worker thread) with `.advance(step, Some(message))`.
149 let pb = progress().style(ProgressStyle::Heavy).size(30).max(50);
150 pb.start("Downloading assets");
151 for i in 1..=50 {
152 std::thread::sleep(std::time::Duration::from_millis(30));
153 let msg = format!("asset {i}/50");
154 pb.advance(1, Some(msg));
155 }
156 pb.stop("Downloaded 50 assets");
157
158 tasks(vec![
159 Task::new("Compiling", |s| {
160 std::thread::sleep(std::time::Duration::from_millis(400));
161 s.message("crate cli-ui");
162 std::thread::sleep(std::time::Duration::from_millis(400));
163 Some("Compiled in 0.8s".into())
164 }),
165 Task::new("Running tests", |_| {
166 std::thread::sleep(std::time::Duration::from_millis(500));
167 Some("All 12 tests passed".into())
168 }),
169 ]);
170
171 log::success("Project ready");
172 outro("Happy hacking!");
173
174 let _ = prompt::Result::<()>::Ok(());
175
176 // ── Summary ───────────────────────────────────────────────────────────────
177 let feat_str = if features.is_empty() {
178 paint(DIM, "none")
179 } else {
180 paint(OK, &features.values().join(", "))
181 };
182
183 let tool_str = if tools.is_empty() {
184 paint(DIM, "none")
185 } else {
186 paint(OK, &tools.values().join(", "))
187 };
188
189 summary! {
190 done: "Project configured",
191 "template" => paint(CYAN, template.value()),
192 "directory" => paint(CYAN, &dir),
193 section,
194 "features" => feat_str,
195 "tools" => tool_str,
196 "ui package" => paint(CYAN, pkg.value()),
197 section,
198 "git" => paint(if git { OK } else { DIM }, if git { "yes" } else { "no" }),
199 }
200
201 Ok(())
202}