Skip to main content

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}