pistonite_cu/process/pio/spinner.rs
1use std::process::Stdio;
2use std::sync::Arc;
3
4use spin::mutex::SpinMutex;
5use tokio::process::{Child as TokioChild, ChildStderr, ChildStdout, Command as TokioCommand};
6
7use crate::cli::{ProgressBar, ProgressBarBuilder};
8use crate::lv::Lv;
9use crate::{Atomic, BoxedFuture};
10
11use super::{ChildOutConfig, ChildOutTask, Driver, DriverOutput};
12
13/// Display child process's status as a progress bar spinner.
14///
15/// # Example
16/// Spawn a `git-clone` process, and use a spinner to show progress updates.
17/// ```rust,no_run
18/// # use pistonite_cu as cu;
19/// use cu::pre::*;
20///
21/// # fn main() -> cu::Result<()> {
22/// cu::which("git")?.command()
23/// .args(["clone", "--progress", "https://example1.git"])
24/// // stdout should be empty, but if there are any messages,
25/// // we will print them
26/// .stdout(cu::lv::P)
27/// // use spinner to show the bar
28/// .stderr(cu::pio::spinner("cloning example1"))
29/// .stdin_null()
30/// .spawn()?.0
31/// .wait_nz()?;
32/// # Ok(()) }
33/// ```
34///
35/// # Behavior
36/// The stream is splited into lines by `\r` and `\n` (`\r\n` is turned into single `\n`).
37/// If the line ends with `\r`, it's considered a progress update.
38///
39/// By default, no matter if the line ends with `\r`, the bar will be updated
40/// with the line. You can also configure the bar to only display
41/// updates (end with `\r`), and print the other messages (end with `\n`)
42/// as normal messages.
43///
44/// ```rust,no_run
45/// # use pistonite_cu as cu;
46/// use cu::pre::*;
47///
48/// # fn main() -> cu::Result<()> {
49/// cu::which("git")?.command()
50/// .args(["clone", "--progress", "https://example1.git"])
51/// // feed both stdout and stderr into the same bar
52/// // when a progress update is done, it will also be printed as
53/// // an info message
54/// .stdoe(cu::pio::spinner("cloning example1").info())
55/// .stdin_null()
56/// .spawn()?.0
57/// .wait_nz()?;
58/// # Ok(()) }
59/// ```
60///
61/// # Output
62/// The progress bar handle is returned when you `spawn` the child.
63/// If the stdout and stderr are configured to the same spinner, then either
64/// handle can be used to update the bar.
65#[inline(always)]
66pub fn spinner(name: impl Into<String>) -> Spinner {
67 Spinner {
68 config: Arc::new(SpinnerInner {
69 lv: Atomic::new_u8(Lv::Off as u8),
70 bar: SpinMutex::new(Err(crate::progress(name))),
71 }),
72 }
73}
74
75#[derive(Clone)]
76#[doc(hidden)]
77pub struct Spinner {
78 config: Arc<SpinnerInner>,
79}
80#[rustfmt::skip]
81impl Spinner {
82 /// Print any non-progress outputs as error messages
83 pub fn error(self) -> Self { self.config.lv.set(crate::lv::E); self }
84 /// Print any non-progress outputs as hint messages
85 pub fn hint(self) -> Self { self.config.lv.set(crate::lv::H); self }
86 /// Print any non-progress outputs as print messages
87 pub fn print(self) -> Self { self.config.lv.set(crate::lv::P); self }
88 /// Print any non-progress outputs as warning messages
89 pub fn warn(self) -> Self { self.config.lv.set(crate::lv::W); self }
90 /// Print any non-progress outputs as info messages
91 pub fn info(self) -> Self { self.config.lv.set(crate::lv::I); self }
92 /// Print any non-progress outputs as debug messages
93 pub fn debug(self) -> Self { self.config.lv.set(crate::lv::D); self }
94 /// Print any non-progress outputs as trace messages
95 pub fn trace(self) -> Self { self.config.lv.set(crate::lv::T); self }
96
97 /// Configure the progress bar that will be spawned
98 #[inline(always)]
99 pub fn configure_spinner<F: FnOnce(ProgressBarBuilder) -> ProgressBarBuilder>(
100 self,
101 f: F,
102 ) -> Self {
103 let mut bar = self.config.bar.lock();
104 if let Err(builder) = bar.clone() {
105 *bar = Err(f(builder));
106 }
107 drop(bar);
108 self
109 }
110}
111struct SpinnerInner {
112 lv: Atomic<u8, Lv>,
113 // the bar spawned when calling take() for the first time,
114 // using a spin lock because it should be VERY rare that
115 // we get contention
116 bar: SpinMutex<Result<Arc<ProgressBar>, ProgressBarBuilder>>,
117}
118pub struct SpinnerTask {
119 lv: Lv,
120 prefix: String,
121 bar: Arc<ProgressBar>,
122 out: Option<ChildStdout>,
123 err: Option<ChildStderr>,
124}
125impl ChildOutConfig for Spinner {
126 type Task = SpinnerTask;
127 type __Null = super::__OCNonNull;
128 fn configure_stdout(&mut self, command: &mut TokioCommand) {
129 command.stdout(Stdio::piped());
130 }
131 fn configure_stderr(&mut self, command: &mut TokioCommand) {
132 command.stderr(Stdio::piped());
133 }
134 fn take(
135 self,
136 child: &mut TokioChild,
137 name: Option<&str>,
138 is_out: bool,
139 ) -> crate::Result<Self::Task> {
140 let lv = self.config.lv.get();
141 let log_prefix = if lv.enabled() {
142 let name = name.unwrap_or_default();
143 if name.is_empty() {
144 String::new()
145 } else {
146 format!("[{name}] ")
147 }
148 } else {
149 String::new()
150 };
151 let bar = {
152 let mut bar_arc = self.config.bar.lock();
153 match bar_arc.as_mut() {
154 // if already created, then just use the bar (i.e. if created
155 // by the same spinner configured for multiple outputs
156 Ok(bar) => Arc::clone(bar),
157 Err(e) => {
158 let bar = e.clone().spawn();
159 *bar_arc = Ok(Arc::clone(&bar));
160 bar
161 }
162 }
163 };
164 Ok(SpinnerTask {
165 lv,
166 prefix: log_prefix,
167 bar,
168 out: if is_out { child.stdout.take() } else { None },
169 err: if !is_out { child.stderr.take() } else { None },
170 })
171 }
172}
173impl ChildOutTask for SpinnerTask {
174 type Output = Arc<ProgressBar>;
175
176 fn run(self) -> (Option<BoxedFuture<()>>, Self::Output) {
177 let bar = Arc::clone(&self.bar);
178 (Some(Box::pin(self.main())), bar)
179 }
180}
181impl SpinnerTask {
182 async fn main(self) {
183 let bar = self.bar;
184 let lv = self.lv;
185 let prefix = self.prefix;
186 // if we are printing, then let the driver only return the last
187 // line if more than one line is found
188 let mut driver = Driver::new(self.out, self.err, lv == Lv::Off);
189 loop {
190 match driver.next().await {
191 DriverOutput::Line(line) => {
192 if lv != Lv::Off {
193 crate::cli::__print_with_level(lv, format_args!("{prefix}{line}"));
194 if lv.enabled() {
195 // erase the progress line if we decide to print it out
196 crate::progress!(bar, "")
197 } else {
198 // the level is not visible in due to log level setting,
199 // so we still print it as a progress line
200 crate::progress!(bar, "{line}")
201 }
202 } else {
203 crate::progress!(bar, "{line}")
204 }
205 }
206 DriverOutput::Progress(line) => {
207 crate::progress!(bar, "{line}")
208 }
209 DriverOutput::Done => break,
210 _ => {}
211 }
212 }
213 }
214}