upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-13 17:42:18 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-13 17:42:18 +0000
commit4303f9f23f69555f306758fb8920cf4069420a76 (patch)
tree8deb1a7fb6e00bee0a208ef1d015e6f52ec88a0b /src/lib
parent9b9d2a2fa2a34ca46f17b821550fc8c972671bd7 (diff)
fix: defer bar finish until reveal to show all bars
indicatif does not re-render bars that called finish_with_message while the draw target was hidden. Instead of trying to force a redraw, defer the finish_with_message call until after the draw target switches to stderr. A BarRevealState coordinates between relay tasks and the timer: bars that complete before the 5s reveal store their finish state in a mutex-protected list, which the timer flushes after switching the draw target. Bars completing after reveal finish immediately as before.
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/client.rs136
1 files changed, 104 insertions, 32 deletions
diff --git a/src/lib/client.rs b/src/lib/client.rs
index 9fd668c..4acccb6 100644
--- a/src/lib/client.rs
+++ b/src/lib/client.rs
@@ -17,7 +17,7 @@ use std::{
17 path::Path, 17 path::Path,
18 sync::{ 18 sync::{
19 Arc, Mutex, RwLock, 19 Arc, Mutex, RwLock,
20 atomic::{AtomicU64, Ordering}, 20 atomic::{AtomicBool, AtomicU64, Ordering},
21 }, 21 },
22 time::Duration, 22 time::Duration,
23}; 23};
@@ -71,6 +71,50 @@ pub fn is_verbose() -> bool {
71 71
72const SPINNER_EXPAND_DELAY_MS: u64 = 5000; 72const SPINNER_EXPAND_DELAY_MS: u64 = 5000;
73 73
74/// Holds the final state of a progress bar that finished before the detail
75/// view was revealed. The style and prefix are already set on the bar; only
76/// the `finish_with_message` call is deferred.
77struct DeferredFinish {
78 bar: ProgressBar,
79 message: String,
80}
81
82/// Coordinates the transition from spinner to detail progress bars.
83/// While `revealed` is false, `finish_bar` stores finish operations in
84/// `deferred`. The background timer sets `revealed` to true, switches the
85/// draw target, and flushes all deferred finishes so every bar appears.
86struct BarRevealState {
87 revealed: AtomicBool,
88 deferred: Mutex<Vec<DeferredFinish>>,
89}
90
91/// Finish a progress bar, deferring the operation if the detail view has not
92/// yet been revealed. When `reveal_state` is `None` (verbose or test mode),
93/// the bar is finished immediately.
94fn finish_bar(bar: &ProgressBar, message: String, reveal_state: &Option<Arc<BarRevealState>>) {
95 match reveal_state {
96 None => bar.finish_with_message(message),
97 Some(state) => {
98 // Lock the deferred list and check `revealed` while holding the
99 // lock. The timer also holds this lock when it sets `revealed`
100 // and drains the list, so there is no window where a bar could
101 // be pushed after the drain.
102 let mut deferred = state.deferred.lock().unwrap();
103 if state.revealed.load(Ordering::Acquire) {
104 drop(deferred);
105 bar.finish_with_message(message);
106 } else {
107 // Style and prefix are already set on the bar. Store the
108 // pending finish so the timer can apply it after reveal.
109 deferred.push(DeferredFinish {
110 bar: bar.clone(),
111 message,
112 });
113 }
114 }
115 }
116}
117
74#[allow(clippy::struct_field_names)] 118#[allow(clippy::struct_field_names)]
75pub struct Client { 119pub struct Client {
76 client: nostr_sdk::Client, 120 client: nostr_sdk::Client,
@@ -407,14 +451,22 @@ impl Connect for Client {
407 MultiProgress::with_draw_target(ProgressDrawTarget::hidden()) 451 MultiProgress::with_draw_target(ProgressDrawTarget::hidden())
408 }; 452 };
409 453
410 // Collect all progress bars so the timer can force a redraw after 454 // Track whether the detail view has been revealed. Bars that finish
411 // switching the draw target (finished bars won't redraw on their own) 455 // before reveal have their finish_with_message deferred so they render
412 let all_bars: Arc<Mutex<Vec<ProgressBar>>> = Arc::new(Mutex::new(Vec::new())); 456 // correctly once the draw target switches from hidden to stderr.
457 let reveal_state: Option<Arc<BarRevealState>> = if !verbose && !is_test {
458 Some(Arc::new(BarRevealState {
459 revealed: AtomicBool::new(false),
460 deferred: Mutex::new(Vec::new()),
461 }))
462 } else {
463 None
464 };
413 465
414 // Spawn a background timer that transitions from spinner to detail view 466 // Spawn a background timer that transitions from spinner to detail view
415 let detail_multi_for_timer = progress_reporter.clone(); 467 let detail_multi_for_timer = progress_reporter.clone();
416 let spinner_for_timer = spinner_multi.as_ref().map(|(_, s)| s.clone()); 468 let spinner_for_timer = spinner_multi.as_ref().map(|(_, s)| s.clone());
417 let all_bars_for_timer = all_bars.clone(); 469 let reveal_state_for_timer = reveal_state.clone();
418 let timer_handle = if !verbose && !is_test { 470 let timer_handle = if !verbose && !is_test {
419 let handle = tokio::spawn(async move { 471 let handle = tokio::spawn(async move {
420 tokio::time::sleep(Duration::from_millis(SPINNER_EXPAND_DELAY_MS)).await; 472 tokio::time::sleep(Duration::from_millis(SPINNER_EXPAND_DELAY_MS)).await;
@@ -431,16 +483,19 @@ impl Connect for Client {
431 ), 483 ),
432 ); 484 );
433 heading.finish_with_message("fetching updates..."); 485 heading.finish_with_message("fetching updates...");
486 // Switch draw target to make bars visible
434 detail_multi_for_timer 487 detail_multi_for_timer
435 .set_draw_target(ProgressDrawTarget::stderr()); 488 .set_draw_target(ProgressDrawTarget::stderr());
436 // Force a full redraw of the multi progress (including bars 489 // Mark as revealed and flush all bars that finished while
437 // that finished while the draw target was hidden). 490 // the draw target was hidden. Hold the lock across the flag
438 // We must use force_draw() rather than tick() because tick() 491 // update and drain so no bar can slip through unseen (see
439 // is a no-op on bars that have enable_steady_tick() active. 492 // the corresponding lock in finish_bar).
440 // A single force_draw() on any bar is sufficient since it 493 if let Some(state) = reveal_state_for_timer {
441 // triggers MultiState::draw() which renders all bars. 494 let mut deferred = state.deferred.lock().unwrap();
442 if let Some(bar) = all_bars_for_timer.lock().unwrap().first() { 495 state.revealed.store(true, Ordering::Release);
443 bar.force_draw(); 496 for df in deferred.drain(..) {
497 df.bar.finish_with_message(df.message);
498 }
444 } 499 }
445 }); 500 });
446 Some(handle) 501 Some(handle)
@@ -516,7 +571,7 @@ impl Connect for Client {
516 let current_timeout_clone = current_timeout_for_loop.clone(); 571 let current_timeout_clone = current_timeout_for_loop.clone();
517 let progress_reporter_clone = progress_reporter.clone(); 572 let progress_reporter_clone = progress_reporter.clone();
518 let total_relays_clone = total_relays; 573 let total_relays_clone = total_relays;
519 let all_bars_clone = all_bars.clone(); 574 let reveal_state_clone = reveal_state.clone();
520 async move { 575 async move {
521 let relay_column_width = request.relay_column_width; 576 let relay_column_width = request.relay_column_width;
522 577
@@ -541,15 +596,23 @@ impl Connect for Client {
541 .with_style(pb_style(current_timeout_clone.clone())?), 596 .with_style(pb_style(current_timeout_clone.clone())?),
542 ); 597 );
543 pb.enable_steady_tick(Duration::from_millis(300)); 598 pb.enable_steady_tick(Duration::from_millis(300));
544 all_bars_clone.lock().unwrap().push(pb.clone());
545 let pb = Some(pb); 599 let pb = Some(pb);
546 600
547 fn update_progress_bar_with_error( 601 /// Set error styling on a progress bar without finishing
602 /// it. Returns the error message so the caller can
603 /// finish the bar through the deferred mechanism.
604 fn style_progress_bar_with_error(
548 relay_column_width: usize, 605 relay_column_width: usize,
549 relay_url: &RelayUrl, 606 relay_url: &RelayUrl,
550 pb: Option<ProgressBar>, 607 pb: &Option<ProgressBar>,
551 error: &anyhow::Error, 608 error: &anyhow::Error,
552 ) { 609 ) -> String {
610 let msg = console::style(
611 error.to_string().replace("relay pool error:", "error:"),
612 )
613 .for_stderr()
614 .red()
615 .to_string();
553 if let Some(pb) = pb { 616 if let Some(pb) = pb {
554 pb.set_style(pb_after_style(false)); 617 pb.set_style(pb_after_style(false));
555 pb.set_prefix( 618 pb.set_prefix(
@@ -558,24 +621,20 @@ impl Connect for Client {
558 .apply_to(format!("{: <relay_column_width$}", &relay_url)) 621 .apply_to(format!("{: <relay_column_width$}", &relay_url))
559 .to_string(), 622 .to_string(),
560 ); 623 );
561 pb.finish_with_message(
562 console::style(
563 error.to_string().replace("relay pool error:", "error:"),
564 )
565 .for_stderr()
566 .red()
567 .to_string(),
568 );
569 } 624 }
625 msg
570 } 626 }
571 627
572 if let Some(reason) = self.is_relay_skipped_for_session(&relay_url) { 628 if let Some(reason) = self.is_relay_skipped_for_session(&relay_url) {
573 update_progress_bar_with_error( 629 let msg = style_progress_bar_with_error(
574 relay_column_width, 630 relay_column_width,
575 &relay_url, 631 &relay_url,
576 pb, 632 &pb,
577 &anyhow!("{reason}"), 633 &anyhow!("{reason}"),
578 ); 634 );
635 if let Some(ref bar) = pb {
636 finish_bar(bar, msg, &reveal_state_clone);
637 }
579 bail!("{reason}"); 638 bail!("{reason}");
580 } 639 }
581 640
@@ -628,15 +687,26 @@ impl Connect for Client {
628 if error.to_string().contains("connection timeout") || error.to_string().contains("timeout after") { 687 if error.to_string().contains("connection timeout") || error.to_string().contains("timeout after") {
629 self.skip_relay_for_session(relay_url.clone(), error.to_string()); 688 self.skip_relay_for_session(relay_url.clone(), error.to_string());
630 } 689 }
631 update_progress_bar_with_error( 690 let msg = style_progress_bar_with_error(
632 relay_column_width, 691 relay_column_width,
633 &relay_url, 692 &relay_url,
634 pb, 693 &pb,
635 &error, 694 &error,
636 ); 695 );
696 if let Some(ref bar) = pb {
697 finish_bar(bar, msg, &reveal_state_clone);
698 }
637 Err(error) 699 Err(error)
638 } 700 }
639 Ok(res) => Ok(res), 701 Ok(res) => {
702 // The bar's style and prefix were already set
703 // by fetch_all_from_relay; finish it through
704 // the deferred mechanism.
705 if let Some(ref bar) = pb {
706 finish_bar(bar, String::new(), &reveal_state_clone);
707 }
708 Ok(res)
709 }
640 } 710 }
641 } 711 }
642 }) 712 })
@@ -786,7 +856,9 @@ impl Connect for Client {
786 format!("new events: {report}") 856 format!("new events: {report}")
787 }, 857 },
788 )); 858 ));
789 pb.finish_with_message(""); 859 // Don't call finish_with_message here — the caller handles
860 // finishing through the deferred mechanism so bars that complete
861 // before the detail view is revealed still appear correctly.
790 } 862 }
791 Ok(report) 863 Ok(report)
792 } 864 }