diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-13 17:42:18 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-13 17:42:18 +0000 |
| commit | 4303f9f23f69555f306758fb8920cf4069420a76 (patch) | |
| tree | 8deb1a7fb6e00bee0a208ef1d015e6f52ec88a0b | |
| parent | 9b9d2a2fa2a34ca46f17b821550fc8c972671bd7 (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.
| -rw-r--r-- | src/lib/client.rs | 136 |
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 | ||
| 72 | const SPINNER_EXPAND_DELAY_MS: u64 = 5000; | 72 | const 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. | ||
| 77 | struct 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. | ||
| 86 | struct 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. | ||
| 94 | fn 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)] |
| 75 | pub struct Client { | 119 | pub 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 | } |