@@ -45,6 +45,27 @@ pub fn run_resilient(config_path: &str) -> Result<()> {
4545/// cache and dependency map after all formats have finished.
4646type RenderResult = ( String , String , Result < ( ) > , Option < String > , Option < Vec < FileDependency > > ) ;
4747
48+ /// Progress-bar symbols used in render status messages.
49+ const SYMBOL_SKIP : & str = "↩" ;
50+ const SYMBOL_OK : & str = "✔" ;
51+ const SYMBOL_FAIL : & str = "✘" ;
52+
53+ /// Create and register a per-output spinner progress bar on `mp`.
54+ ///
55+ /// Pre-creating bars before the rayon parallel section avoids calling
56+ /// [`MultiProgress::add`] from multiple threads, which would acquire an
57+ /// internal mutex on every call and could serialise parallel workers.
58+ fn create_output_bar ( mp : & MultiProgress , format_label : & str ) -> ProgressBar {
59+ let bar = mp. add ( ProgressBar :: new_spinner ( ) ) ;
60+ bar. set_style (
61+ ProgressStyle :: with_template ( " {spinner:.blue} [{prefix:.bold.cyan}] {msg}" )
62+ . expect ( "hardcoded per-output progress bar template is valid" ) ,
63+ ) ;
64+ bar. set_prefix ( format_label. to_string ( ) ) ;
65+ bar. set_message ( "queued" ) ;
66+ bar
67+ }
68+
4869fn run_impl ( config_path : & str , dry_run : bool , resilient : bool , optimization : Option < OptimizationMode > ) -> Result < ( ) > {
4970 if dry_run {
5071 info ! ( "Dry-run mode enabled — no files will be created and no commands will be executed" ) ;
@@ -127,18 +148,9 @@ fn run_impl(config_path: &str, dry_run: bool, resilient: bool, optimization: Opt
127148 ) ;
128149
129150 // Pre-create one spinner per output format *before* the parallel rendering
130- // section. Calling mp.add() inside a rayon parallel closure acquires an
131- // internal mutex on every call and can serialise the workers. By creating
132- // all bars up-front each worker can update its own bar without contention.
151+ // section. See [`create_output_bar`] for the rationale.
133152 let output_bars: Vec < ProgressBar > = config. outputs . iter ( ) . map ( |output| {
134- let bar = mp. add ( ProgressBar :: new_spinner ( ) ) ;
135- bar. set_style (
136- ProgressStyle :: with_template ( " {spinner:.blue} [{prefix:.bold.cyan}] {msg}" )
137- . expect ( "hardcoded per-output progress bar template is valid" ) ,
138- ) ;
139- bar. set_prefix ( output. output_type . to_string ( ) ) ;
140- bar. set_message ( "queued" ) ;
141- bar
153+ create_output_bar ( & mp, & output. output_type . to_string ( ) )
142154 } ) . collect ( ) ;
143155
144156 // Transforms are run once per output format (serially, before parallel rendering)
@@ -249,7 +261,7 @@ fn run_impl(config_path: &str, dry_run: bool, resilient: bool, optimization: Opt
249261
250262 if dry_run {
251263 info ! ( "[DRY RUN] Would render {} output to: {}" , format, output_path) ;
252- bar. finish_with_message ( format ! ( "[DRY RUN] Would render to {}" , output_path) ) ;
264+ bar. finish_with_message ( format ! ( "[DRY RUN] {SYMBOL_OK} Would render to {}" , output_path) ) ;
253265 pb. inc ( 1 ) ;
254266 pb. println ( format ! ( "[DRY RUN] Would write output to: {}" , output_path) ) ;
255267 ( format_str, output_path, Ok ( ( ) ) , None , None )
@@ -307,9 +319,9 @@ fn run_impl(config_path: &str, dry_run: bool, resilient: bool, optimization: Opt
307319 {
308320 debug ! ( hash = %output_hash, output = %output_path, "Output cache hit — skipping render" ) ;
309321 info ! ( "Skipping {} render (unchanged)" , format) ;
310- bar. finish_with_message ( format ! ( "↩ unchanged: {}" , output_path) ) ;
322+ bar. finish_with_message ( format ! ( "{SYMBOL_SKIP} unchanged: {}" , output_path) ) ;
311323 pb. inc ( 1 ) ;
312- pb. println ( format ! ( "↩ Skipping {} output (unchanged): {}" , format, output_path) ) ;
324+ pb. println ( format ! ( "{SYMBOL_SKIP} Skipping {} output (unchanged): {}" , format, output_path) ) ;
313325 return ( format_str, output_path, Ok ( ( ) ) , Some ( output_hash) , Some ( file_deps) ) ;
314326 }
315327
@@ -329,16 +341,16 @@ fn run_impl(config_path: &str, dry_run: bool, resilient: bool, optimization: Opt
329341
330342 match & result {
331343 Ok ( _) => {
332- bar. finish_with_message ( format ! ( "✔ {}" , output_path) ) ;
344+ bar. finish_with_message ( format ! ( "{SYMBOL_OK} {}" , output_path) ) ;
333345 pb. inc ( 1 ) ;
334- pb. println ( format ! ( "✔ Output written to: {}" , output_path) ) ;
346+ pb. println ( format ! ( "{SYMBOL_OK} Output written to: {}" , output_path) ) ;
335347 info ! ( output = %output_path, "Pipeline completed for format: {}" , format) ;
336348 }
337349 Err ( e) => {
338350 warn ! ( format = %format, error = %e, "Rendering failed for output format" ) ;
339- bar. finish_with_message ( format ! ( "✘ failed: {:#}" , e) ) ;
351+ bar. finish_with_message ( format ! ( "{SYMBOL_FAIL} failed: {:#}" , e) ) ;
340352 pb. inc ( 1 ) ;
341- pb. println ( format ! ( "✘ Failed to render {} output: {:#}" , format, e) ) ;
353+ pb. println ( format ! ( "{SYMBOL_FAIL} Failed to render {} output: {:#}" , format, e) ) ;
342354 }
343355 }
344356 ( format_str, output_path, result, new_hash, new_deps)
@@ -373,9 +385,9 @@ fn run_impl(config_path: &str, dry_run: bool, resilient: bool, optimization: Opt
373385 . collect ( ) ;
374386
375387 if dry_run {
376- pb. finish_with_message ( "[DRY RUN] ✔ Dry-run complete — no output written" ) ;
388+ pb. finish_with_message ( format ! ( "[DRY RUN] {SYMBOL_OK} Dry-run complete — no output written" ) ) ;
377389 } else if failed_outputs. is_empty ( ) {
378- pb. finish_with_message ( "✔ Build complete") ;
390+ pb. finish_with_message ( format ! ( "{SYMBOL_OK} Build complete") ) ;
379391 } else {
380392 pb. finish_with_message ( format ! ( "⚠ Build completed with {} failure(s)" , failed_outputs. len( ) ) ) ;
381393 let messages: Vec < String > = failed_outputs
0 commit comments