atomic_progress/
stack.rs

1//! A thread-safe collection for managing multiple progress indicators.
2//!
3//! The [`ProgressStack`] serves as the central registry for "Multi-Bar" applications.
4//! It allows a renderer to iterate over a dynamic list of active tasks without fighting
5//! the worker threads for locks.
6//!
7//! # Synchronization Strategy
8//!
9//! The stack uses a coarse-grained [`RwLock`](parking_lot::RwLock) to protect the *list*
10//! of handles. Individual progress updates (incrementing counters) do **not** lock the stack.
11//!
12//! * **Workers:** Only acquire the stack lock when adding/removing a bar (rare).
13//! * **Renderers:** Acquire a read lock on the stack once per frame to clone the handles,
14//!   then iterate independently.
15
16use std::{fmt, sync::Arc};
17
18use compact_str::CompactString;
19use parking_lot::RwLock;
20
21use crate::{Progress, ProgressSnapshot};
22
23/// A thread-safe, shared-clonable collection of [`Progress`] instances.
24///
25/// `ProgressStack` is designed to manage a dynamic list of progress indicators
26/// (bars, spinners, etc.) that typically render together (e.g., a multi-bar CLI).
27///
28/// # Concurrency
29///
30/// This struct uses internal synchronization (via [`Arc`] and [`RwLock`]), making it
31/// safe to clone and share across threads. Cloning is cheap (pointer copy), and
32/// mutations on one clone are visible to all others.
33#[derive(Clone, Default)]
34pub struct ProgressStack {
35    /// The shared list of progress items.
36    ///
37    /// We use `parking_lot::RwLock` for efficient, fair locking.
38    inner: Arc<RwLock<Vec<Progress>>>,
39}
40
41impl fmt::Debug for ProgressStack {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        // We only print metadata to avoid locking the inner items during debug formatting
44        f.debug_struct("ProgressStack")
45            .field("count", &self.len())
46            .finish()
47    }
48}
49
50impl ProgressStack {
51    /// Creates a new, empty `ProgressStack`.
52    ///
53    /// # Examples
54    ///
55    /// ```
56    /// use atomic_progress::ProgressStack;
57    ///
58    /// let stack = ProgressStack::new();
59    /// assert!(stack.is_empty());
60    /// ```
61    #[must_use]
62    pub fn new() -> Self {
63        Self::default()
64    }
65
66    /// Adds a [`Progress`] instance to the stack.
67    ///
68    /// The progress item is appended to the end of the collection.
69    ///
70    /// # Parameters
71    ///
72    /// * `progress`: The progress indicator to add.
73    pub fn push(&self, progress: Progress) {
74        self.inner.write().push(progress);
75    }
76
77    /// Creates a new progress bar, adds it to the stack, and returns the handle.
78    ///
79    /// This is a shorthand for creating a [`Progress`] via [`Progress::new_pb`]
80    /// and calling [`push`](Self::push).
81    ///
82    /// # Parameters
83    ///
84    /// * `name`: The display name for the bar.
85    /// * `total`: The total count for the bar.
86    #[must_use]
87    pub fn add_pb(&self, name: impl Into<CompactString>, total: impl Into<u64>) -> Progress {
88        let progress = Progress::new_pb(name, total);
89        self.push(progress.clone());
90        progress
91    }
92
93    /// Creates a new spinner, adds it to the stack, and returns the handle.
94    ///
95    /// This is a shorthand for creating a [`Progress`] via [`Progress::new_spinner`]
96    /// and calling [`push`](Self::push).
97    ///
98    /// # Parameters
99    ///
100    /// * `name`: The display name for the spinner.
101    #[must_use]
102    pub fn add_spinner(&self, name: impl Into<CompactString>) -> Progress {
103        let progress = Progress::new_spinner(name);
104        self.push(progress.clone());
105        progress
106    }
107
108    /// Returns a snapshot of the state of all tracked progress instances.
109    ///
110    /// This aggregates the current state of every progress bar in the stack.
111    /// It is typically used by renderers to draw the current UI frame.
112    ///
113    /// # Performance
114    ///
115    /// This acquires a read lock on the stack collection, and subsequently
116    /// acquires read locks on each individual `Progress` state to copy their data.
117    #[must_use]
118    pub fn snapshot(&self) -> ProgressStackSnapshot {
119        // 1. Quick lock just to clone the Arcs
120        let items: Vec<Progress> = self.inner.read().clone();
121
122        // 2. Do the heavy lifting (locking individual bars) without holding the stack lock
123        let snapshots = items.iter().map(Progress::snapshot).collect();
124
125        ProgressStackSnapshot(snapshots)
126    }
127
128    /// Returns a list of handles to all [`Progress`] instances in the stack.
129    ///
130    /// Since [`Progress`] is a cheap-to-clone handle (`Arc`), this returns
131    /// a new vector containing clones of the handles. This allows you to retain
132    /// access to the bars even if the stack is cleared or modified later.
133    #[must_use]
134    pub fn items(&self) -> Vec<Progress> {
135        self.inner.read().clone()
136    }
137
138    /// Checks if all progress instances in the stack are marked as finished.
139    ///
140    /// Returns `true` if the stack is empty or if `is_finished()` returns true
141    /// for every item.
142    #[must_use]
143    pub fn is_all_finished(&self) -> bool {
144        self.inner.read().iter().all(Progress::is_finished)
145    }
146
147    /// Removes all progress instances from the stack.
148    ///
149    /// This clears the internal collection.
150    pub fn clear(&self) {
151        self.inner.write().clear();
152    }
153
154    /// Returns the number of progress instances currently in the stack.
155    #[must_use]
156    pub fn len(&self) -> usize {
157        self.inner.read().len()
158    }
159
160    /// Returns `true` if the stack contains no progress instances.
161    #[must_use]
162    pub fn is_empty(&self) -> bool {
163        self.inner.read().is_empty()
164    }
165}
166
167/// A snapshot of the state of the entire progress stack at a specific point in time.
168///
169/// This wrapper allows for future extension of aggregate calculations (e.g. global ETA,
170/// total throughput) without changing the return signature.
171#[derive(Clone, Debug, Default, Eq, PartialEq)]
172#[cfg_attr(
173    feature = "rkyv",
174    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
175)]
176#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
177#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, Eq, PartialEq)))]
178pub struct ProgressStackSnapshot(pub Vec<ProgressSnapshot>);
179
180#[cfg(test)]
181mod tests {
182    use super::ProgressStack;
183
184    /// Stack Management
185    /// Verifies adding items and checking aggregate state.
186    #[test]
187    fn test_stack_operations() {
188        let stack = ProgressStack::new();
189        assert!(stack.is_empty());
190
191        let _pb = stack.add_pb("bar", 100u64);
192        let _sp = stack.add_spinner("spin");
193
194        assert_eq!(stack.len(), 2);
195        assert!(!stack.is_all_finished());
196
197        let items = stack.items();
198        items[0].finish();
199        items[1].finish();
200
201        assert!(stack.is_all_finished());
202    }
203
204    /// Snapshot Isolation
205    /// Verifies that a snapshot is an owned copy of data at that instant.
206    #[test]
207    fn test_snapshot_isolation() {
208        let stack = ProgressStack::new();
209        let pb = stack.add_pb("test", 100u64);
210
211        pb.inc(10u64);
212        let snap_1 = stack.snapshot();
213
214        pb.inc(20u64);
215        let snap_2 = stack.snapshot();
216
217        assert_eq!(
218            snap_1.0[0].position(),
219            10,
220            "Old snapshot should remain immutable"
221        );
222        assert_eq!(snap_2.0[0].position(), 30, "New snapshot reflects updates");
223    }
224}