progressor/
lib.rs

1//! A modern, async-first progress tracking library for Rust.
2//!
3//! This crate provides types and utilities for tracking progress of long-running operations
4//! in an async context. It uses Rust's `Stream` API to emit progress updates with support for
5//! different states (working, paused, completed, cancelled).
6//!
7//! # Features
8//!
9//! - **Async-first**: Built around Rust's async/await and Stream APIs
10//! - **Zero-allocation progress updates**: Efficient progress reporting
11//! - **Flexible progress tracking**: Support for current/total, messages, and cancellation
12//! - **Type-safe**: Full Rust type safety with meaningful error messages
13//! - **Lightweight**: Minimal dependencies and fast compilation
14//! - **Convenient observing**: Extension methods for easy progress monitoring
15//!
16//! # Examples
17//!
18//! ## Using the observe extension (recommended)
19//!
20//! The [`ProgressExt::observe`] method provides a convenient way to monitor progress
21//! without manually managing streams and select macros:
22//!
23//! ```
24//! # #[cfg(feature = "std")]
25//! # {
26//! use progressor::{progress, ProgressExt};
27//!
28//! # async fn example() {
29//! let result = progress(100, |mut updater| async move {
30//!     for i in 0..=100 {
31//!         // Update progress
32//!         updater.update(i);
33//!         
34//!         // Add messages for important milestones
35//!         if i % 25 == 0 {
36//!             updater.update_with_message(i, format!("Milestone: {}%", i));
37//!         }
38//!     }
39//!     "Task completed!"
40//! })
41//! .observe(|update| {
42//!     println!("Progress: {}%", (update.completed_fraction() * 100.0) as u32);
43//!     if let Some(message) = update.message() {
44//!         println!("  {}", message);
45//!     }
46//! })
47//! .await;
48//!
49//! println!("Result: {}", result);
50//! # }
51//! # }
52//! ```
53//!
54//! ## Manual stream monitoring with `tokio::select!`
55//!
56//! For more control, you can manually monitor the progress stream:
57//!
58//! ```
59//! # #[cfg(feature = "std")]
60//! # {
61//! use progressor::{progress, Progress};
62//! use futures_util::StreamExt;
63//!
64//! # async fn example() {
65//! let task = progress(100, |mut updater| async move {
66//!     for i in 0..=100 {
67//!         // Update progress
68//!         updater.update(i);
69//!         
70//!         // Add messages for important milestones
71//!         if i % 25 == 0 {
72//!             updater.update_with_message(i, format!("Milestone: {}%", i));
73//!         }
74//!     }
75//!     "Task completed!"
76//! });
77//!
78//! // Monitor progress concurrently
79//! let mut progress_stream = task.progress();
80//! tokio::select! {
81//!     result = task => {
82//!         println!("Result: {}", result);
83//!     }
84//!     _ = async {
85//!         while let Some(update) = progress_stream.next().await {
86//!             println!("Progress: {}%", (update.completed_fraction() * 100.0) as u32);
87//!             if let Some(message) = update.message() {
88//!                 println!("  {}", message);
89//!             }
90//!         }
91//!     } => {}
92//! }
93//! # }
94//! # }
95//! ```
96//!
97//! ## Advanced usage with state handling
98//!
99//! Monitor different progress states and handle pause/cancel operations:
100//!
101//! ```
102//! # #[cfg(feature = "std")]
103//! # {
104//! use progressor::{progress, ProgressExt, State};
105//!
106//! # async fn example() {
107//! let result = progress(100, |mut updater| async move {
108//!     for i in 0..=100 {
109//!         // Update progress
110//!         updater.update(i);
111//!         
112//!         // Pause at 50%
113//!         if i == 50 {
114//!             updater.pause();
115//!             // Simulate some async work during pause
116//!             tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
117//!         }
118//!     }
119//!     "Task completed!"
120//! })
121//! .observe(|update| {
122//!     match update.state() {
123//!         State::Working => println!("Working: {}%", (update.completed_fraction() * 100.0) as u32),
124//!         State::Paused => println!("Paused at {}%", (update.completed_fraction() * 100.0) as u32),
125//!         State::Completed => println!("Completed!"),
126//!         State::Cancelled => println!("Cancelled!"),
127//!     }
128//! })
129//! .await;
130//!
131//! println!("Result: {}", result);
132//! # }
133//! # }
134//! ```
135
136#![cfg_attr(docsrs, feature(doc_cfg))]
137
138mod ext;
139pub use ext::ProgressExt;
140#[cfg(feature = "std")]
141mod updater;
142
143#[cfg(feature = "std")]
144#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
145pub use updater::{ProgressUpdater, progress};
146
147use core::future::Future;
148use futures_core::Stream;
149
150/// A trait for futures that can report progress updates.
151///
152/// This trait extends [`Future`] to provide a method for accessing a stream of progress updates.
153/// The progress updates are emitted as [`ProgressUpdate`] items through a [`Stream`].
154pub trait Progress: Future {
155    /// Returns a stream of progress updates for this operation.
156    ///
157    /// The stream will emit [`ProgressUpdate`] instances as the operation progresses.
158    /// The stream should be polled concurrently with the future to receive updates.
159    fn progress(&self) -> impl Stream<Item = ProgressUpdate> + Unpin + Send + 'static;
160}
161
162/// Represents a single progress update with current status, total, and optional metadata.
163///
164/// This struct contains all the information about the current state of a progress-tracked operation.
165/// It is emitted by progress streams and provides methods to query the current progress state.
166///
167/// You typically don't create instances of this struct directly. Instead, use the [`progress`] function
168/// to create progress-tracked tasks, and receive `ProgressUpdate` instances from the progress stream.
169///
170/// [`progress`]: crate::progress
171#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
172pub struct ProgressUpdate {
173    current: u64,
174    total: u64,
175    state: State,
176    message: Option<String>,
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
180/// Represents the state of a progress-tracked operation.
181pub enum State {
182    /// The operation is in progress.
183    Working,
184    /// The operation has been completed successfully.
185    Completed,
186    /// The operation has been paused.
187    Paused,
188    /// The operation has been cancelled.
189    Cancelled,
190}
191
192impl State {
193    /// Returns `true` if the state is [`Cancelled`](State::Cancelled).
194    #[must_use]
195    pub const fn is_cancelled(&self) -> bool {
196        matches!(self, Self::Cancelled)
197    }
198
199    /// Returns `true` if the state is [`Working`](State::Working).
200    #[must_use]
201    pub const fn is_working(&self) -> bool {
202        matches!(self, Self::Working)
203    }
204
205    /// Returns `true` if the state is [`Completed`](State::Completed).
206    #[must_use]
207    pub const fn is_completed(&self) -> bool {
208        matches!(self, Self::Completed)
209    }
210
211    /// Returns `true` if the state is [`Paused`](State::Paused).
212    #[must_use]
213    pub const fn is_paused(&self) -> bool {
214        matches!(self, Self::Paused)
215    }
216}
217
218impl ProgressUpdate {
219    /// Creates a new progress update.
220    ///
221    /// This method is primarily used internally by the progress tracking system.
222    /// Users should use the [`progress`] function instead of creating updates manually.
223    ///
224    /// [`progress`]: crate::progress
225    #[must_use]
226    pub const fn new(total: u64, current: u64, state: State, message: Option<String>) -> Self {
227        Self {
228            current,
229            total,
230            state,
231            message,
232        }
233    }
234
235    /// Returns the total expected value when the operation will be complete.
236    #[must_use]
237    pub const fn total(&self) -> u64 {
238        self.total
239    }
240
241    /// Returns the current progress value.
242    #[must_use]
243    pub const fn current(&self) -> u64 {
244        self.current
245    }
246
247    /// Returns the completion fraction as a value between 0.0 and 1.0.
248    ///
249    /// If the total is 0, returns 0.0. Otherwise, returns current/total.
250    #[must_use]
251    pub fn completed_fraction(&self) -> f64 {
252        if self.total == 0 {
253            0.0
254        } else {
255            #[allow(clippy::cast_precision_loss)]
256            {
257                self.current as f64 / self.total as f64
258            }
259        }
260    }
261
262    /// Returns the remaining progress (total - current).
263    ///
264    /// Uses saturating subtraction, so if current > total, returns 0.
265    #[must_use]
266    pub const fn remaining(&self) -> u64 {
267        self.total.saturating_sub(self.current)
268    }
269
270    /// Returns `true` if the state is [`Cancelled`](State::Cancelled).
271    #[must_use]
272    pub const fn is_cancelled(&self) -> bool {
273        matches!(self.state, State::Cancelled)
274    }
275
276    /// Returns `true` if the state is [`Working`](State::Working).
277    #[must_use]
278    pub const fn is_working(&self) -> bool {
279        matches!(self.state, State::Working)
280    }
281
282    /// Returns `true` if the state is [`Completed`](State::Completed).
283    #[must_use]
284    pub const fn is_completed(&self) -> bool {
285        matches!(self.state, State::Completed)
286    }
287
288    /// Returns `true` if the state is [`Paused`](State::Paused).
289    #[must_use]
290    pub const fn is_paused(&self) -> bool {
291        matches!(self.state, State::Paused)
292    }
293
294    /// Returns the optional descriptive message about the current progress.
295    #[must_use]
296    pub fn message(&self) -> Option<&str> {
297        self.message.as_deref()
298    }
299
300    /// Returns the current state of the progress operation.
301    #[must_use]
302    pub const fn state(&self) -> State {
303        self.state
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn test_progress_update_new() {
313        let update = ProgressUpdate::new(100, 0, State::Working, None);
314        assert_eq!(update.current(), 0);
315        assert_eq!(update.total(), 100);
316        assert!(!update.is_cancelled());
317        assert_eq!(update.message(), None);
318    }
319
320    #[test]
321    fn test_completed_fraction() {
322        let mut update = ProgressUpdate::new(100, 0, State::Working, None);
323        assert!((update.completed_fraction() - 0.0).abs() < f64::EPSILON);
324
325        update.current = 50;
326        assert!((update.completed_fraction() - 0.5).abs() < f64::EPSILON);
327
328        update.current = 100;
329        assert!((update.completed_fraction() - 1.0).abs() < f64::EPSILON);
330    }
331
332    #[test]
333    fn test_completed_fraction_zero_total() {
334        let update = ProgressUpdate::new(0, 0, State::Working, None);
335        assert!((update.completed_fraction() - 0.0).abs() < f64::EPSILON);
336    }
337
338    #[test]
339    fn test_builder_methods() {
340        let update =
341            ProgressUpdate::new(100, 50, State::Working, Some("Half complete".to_string()));
342
343        assert_eq!(update.current(), 50);
344        assert_eq!(update.message(), Some("Half complete"));
345        assert_eq!(update.state, State::Working);
346        assert!(update.is_working());
347        assert!(!update.is_cancelled());
348        assert!(!update.is_completed());
349        assert!(!update.is_paused());
350    }
351
352    #[test]
353    fn test_is_complete() {
354        let mut update = ProgressUpdate::new(100, 0, State::Working, None);
355        assert!(!update.is_completed());
356        assert!(update.is_working());
357
358        // Setting state to completed
359        update.state = State::Completed;
360        assert!(update.is_completed());
361
362        // Test other states
363        update.state = State::Cancelled;
364        assert!(update.is_cancelled());
365        assert!(!update.is_completed());
366
367        update.state = State::Paused;
368        assert!(update.is_paused());
369        assert!(!update.is_completed());
370    }
371
372    #[test]
373    fn test_remaining() {
374        let mut update = ProgressUpdate::new(100, 0, State::Working, None);
375        assert_eq!(update.remaining(), 100);
376
377        update.current = 30;
378        assert_eq!(update.remaining(), 70);
379
380        update.current = 100;
381        assert_eq!(update.remaining(), 0);
382
383        update.current = 150; // when exceeding total should return 0
384        assert_eq!(update.remaining(), 0);
385    }
386}