//! Drive N concurrent futures and render them as a single progress line. //! //! [`Join`] owns its inner futures and polls them concurrently. Its [`Progressive::progress`] is //! the completion fraction (`completed / total`), so the bar fills from 0/N to N/N as each future //! resolves. Push one into a [`Group`](super::Group) to render many futures as one line alongside //! other independent rows, or call [`with_theme`](Join::with_theme) for a self-contained row. use std::borrow::Cow; use std::fmt::Display; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; use futures_lite::stream::Pending; use futures_lite::{stream, Stream}; use owo_colors::Style; use pin_project_lite::pin_project; use crate::progress::Progress; use crate::progressive::Progressive; use crate::Theme; pin_project! { /// N concurrent futures collapsed into a single [`Progressive`] row. /// /// The bar fills from 0/N to N/N as each inner future completes. Results are collected in /// completion order. With zero inputs, awaiting resolves immediately with an empty `Vec` and no /// progress is reported. pub struct Join<'a, F: Future, M = Pending<&'static str>> { futs: Vec>>, results: Vec, completed: usize, total: usize, #[pin] messages: M, core: Progress<'a>, } } /// Construct a [`Join`] from an iterable of futures sharing an `Output` type. /// /// Accepts any [`IntoIterator`] of futures — `Vec`, arrays, or any adapter chain. Without /// [`with_theme`](Join::with_theme) the result inherits the parent `Group`'s theme when pushed, /// with `with_theme` it renders standalone or overrides the Group's theme per-row. pub fn join(futs: I) -> Join<'static, I::Item> where I: IntoIterator, I::Item: Future, { Join::new(futs) } impl Join<'_, F> { /// Construct a `Join` with no theme set. Awaited directly it renders with [`Theme::default()`]; /// chain [`with_theme`](Self::with_theme) for a custom theme, or push it into a /// [`Group`](super::Group) to inherit the Group's theme. pub fn new(futs: I) -> Self where I: IntoIterator, { let futs: Vec>> = futs.into_iter().map(Box::pin).collect(); let total = futs.len(); Self { results: Vec::with_capacity(total), futs, completed: 0, total, messages: stream::pending(), core: Progress::new(), } } } impl<'a, F: Future, M> Join<'a, F, M> { /// Render this row with `theme`. Used for both the standalone path (drives the spinner / /// bar / cursor on its own line when awaited) and the per-row override path inside a /// [`Group`](super::Group). pub fn with_theme(mut self, theme: impl Into>) -> Self { self.core.set_theme(theme.into()); self } /// Apply `style` to the spinner character on this row, overriding the parent Group's default. pub fn with_spinner_style(mut self, style: Style) -> Self { self.core.set_spinner_style(style); self } /// Apply `style` to the annotation (label) text on this row, overriding the parent Group's /// default. pub fn with_annotation_style(mut self, style: Style) -> Self { self.core.set_annotation_style(style); self } /// Set the static label shown in the [`Label`](crate::layout::Segment::Label) segment. pub fn with_label(mut self, label: impl Display) -> Self { self.core.set_label(label.to_string()); self } /// Prepend the elapsed time to the line. pub fn with_elapsed_time(mut self) -> Self { self.core.enable_elapsed_time(); self } /// Replace the displayed message each time `messages` yields a value. The item type is /// anything that converts into a `Cow<'static, str>`: `&'static str` and `String` are /// zero-copy; other formatted values should be `format!`'d at the call site. pub fn with_messages(self, messages: S) -> Join<'a, F, S> where S: Stream, S::Item: Into>, { Join { futs: self.futs, results: self.results, completed: self.completed, total: self.total, messages, core: self.core, } } } impl<'a, F: Future, M> Progressive<'a> for Join<'a, F, M> { fn label(&self) -> Option<&str> { self.core.label() } fn message(&self) -> Option<&str> { self.core.message() } fn progress(&self) -> Option { if self.total == 0 { None } else { Some(self.completed as f64 / self.total as f64) } } fn detach_rendering(&mut self) { self.core.detach_rendering(); } fn theme(&self) -> Option<&Theme<'a>> { self.core.theme() } fn spinner_style(&self) -> Option