Skip to main content

paint

Function paint 

Source
pub fn paint(style: Style, s: &str) -> String
Expand 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
Hide additional 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}