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}