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}