Skip to main content

qubit_progress/reporter/impls/
writer_progress_reporter.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10use std::{
11    io::Write,
12    sync::{
13        Arc,
14        Mutex,
15    },
16};
17
18use super::format::format_duration;
19use crate::{
20    model::ProgressEvent,
21    reporter::ProgressReporter,
22};
23
24/// Progress reporter that writes human-readable events to a writer.
25///
26/// # Type Parameters
27///
28/// * `W` - Writer receiving formatted progress events.
29///
30/// # Examples
31///
32/// ```
33/// use std::io::Cursor;
34/// use std::sync::{
35///     Arc,
36///     Mutex,
37/// };
38/// use std::time::Duration;
39///
40/// use qubit_progress::{
41///     ProgressCounters,
42///     ProgressEvent,
43///     ProgressReporter,
44///     WriterProgressReporter,
45/// };
46///
47/// let output = Arc::new(Mutex::new(Cursor::new(Vec::new())));
48/// let reporter = WriterProgressReporter::new(output.clone());
49/// reporter.report(&ProgressEvent::running(
50///     ProgressCounters::new(Some(4)).with_completed_count(2),
51///     Duration::from_secs(1),
52/// ));
53///
54/// let bytes = output.lock().expect("output should lock").get_ref().clone();
55/// let text = String::from_utf8(bytes).expect("progress output should be UTF-8");
56/// assert!(text.contains("running"));
57/// assert!(text.contains("2/4"));
58/// ```
59#[derive(Debug)]
60pub struct WriterProgressReporter<W> {
61    /// Shared writer receiving progress lines.
62    writer: Arc<Mutex<W>>,
63}
64
65impl<W> WriterProgressReporter<W> {
66    /// Creates a reporter from a shared writer.
67    ///
68    /// # Parameters
69    ///
70    /// * `writer` - Shared writer receiving progress output.
71    ///
72    /// # Returns
73    ///
74    /// A writer-backed progress reporter.
75    #[inline]
76    pub fn new(writer: Arc<Mutex<W>>) -> Self {
77        Self { writer }
78    }
79
80    /// Creates a reporter from an owned writer.
81    ///
82    /// # Parameters
83    ///
84    /// * `writer` - Owned writer receiving progress output.
85    ///
86    /// # Returns
87    ///
88    /// A writer-backed progress reporter.
89    #[inline]
90    pub fn from_writer(writer: W) -> Self {
91        Self::new(Arc::new(Mutex::new(writer)))
92    }
93
94    /// Returns the shared writer used by this reporter.
95    ///
96    /// # Returns
97    ///
98    /// A shared reference to the writer mutex.
99    #[inline]
100    pub const fn writer(&self) -> &Arc<Mutex<W>> {
101        &self.writer
102    }
103}
104
105impl<W> ProgressReporter for WriterProgressReporter<W>
106where
107    W: Write + Send,
108{
109    /// Writes one progress event as a single human-readable line.
110    ///
111    /// # Parameters
112    ///
113    /// * `event` - Progress event to format and write.
114    ///
115    /// # Panics
116    ///
117    /// Recovers the inner writer when the writer mutex is poisoned, and panics
118    /// only when writing to the configured writer fails.
119    fn report(&self, event: &ProgressEvent) {
120        let mut writer = self
121            .writer
122            .lock()
123            .unwrap_or_else(std::sync::PoisonError::into_inner);
124        writeln!(writer, "{}", format_event(event)).expect("progress reporter should write event");
125    }
126}
127
128/// Formats one progress event.
129///
130/// # Parameters
131///
132/// * `event` - Event to format.
133///
134/// # Returns
135///
136/// A compact human-readable line.
137fn format_event(event: &ProgressEvent) -> String {
138    let counters = event.counters();
139    let progress = match (counters.completed_count(), counters.total_count()) {
140        (completed, Some(total)) => format!(
141            "{completed}/{total} ({:.2}%)",
142            counters.progress_percent().unwrap_or(100.0)
143        ),
144        (completed, None) => format!("{completed} completed"),
145    };
146    let active = counters.active_count();
147    let failed = counters.failed_count();
148    let elapsed = format_duration(event.elapsed());
149    match event.stage() {
150        Some(stage) => format!(
151            "{} [{}] {progress}, active {active}, failed {failed}, elapsed {elapsed}",
152            event.phase(),
153            stage.name(),
154        ),
155        None => format!(
156            "{} {progress}, active {active}, failed {failed}, elapsed {elapsed}",
157            event.phase(),
158        ),
159    }
160}