Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
term: Use indicatif spinner and improve API
Erik Kundt committed 10 months ago
commit 18a1d1db705356032996d1712a14b2f7e605c5a1
parent a78f968834dc74be8f1a5e20bd95ee6c415e8521
4 files changed +206 -186
modified Cargo.lock
@@ -454,6 +454,19 @@ dependencies = [
]

[[package]]
+
name = "console"
+
version = "0.16.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d"
+
dependencies = [
+
 "encode_unicode",
+
 "libc",
+
 "once_cell",
+
 "unicode-width 0.2.1",
+
 "windows-sys 0.60.2",
+
]
+

+
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -809,6 +822,12 @@ dependencies = [
]

[[package]]
+
name = "encode_unicode"
+
version = "1.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+

+
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1729,6 +1748,19 @@ dependencies = [
]

[[package]]
+
name = "indicatif"
+
version = "0.18.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd"
+
dependencies = [
+
 "console",
+
 "portable-atomic",
+
 "unicode-width 0.2.1",
+
 "unit-prefix",
+
 "web-time",
+
]
+

+
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1752,7 +1784,7 @@ dependencies = [
 "once_cell",
 "tempfile",
 "unicode-segmentation",
-
 "unicode-width",
+
 "unicode-width 0.1.11",
]

[[package]]
@@ -2792,6 +2824,7 @@ dependencies = [
 "crossbeam-channel",
 "crossterm 0.29.0",
 "git2",
+
 "indicatif",
 "inquire",
 "pretty_assertions",
 "radicle-signals",
@@ -3725,6 +3758,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"

[[package]]
+
name = "unicode-width"
+
version = "0.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
+

+
[[package]]
+
name = "unit-prefix"
+
version = "0.5.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817"
+

+
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3873,6 +3918,16 @@ dependencies = [
]

[[package]]
+
name = "web-time"
+
version = "1.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+
dependencies = [
+
 "js-sys",
+
 "wasm-bindgen",
+
]
+

+
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4004,6 +4059,15 @@ dependencies = [
]

[[package]]
+
name = "windows-sys"
+
version = "0.60.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+
dependencies = [
+
 "windows-targets 0.53.2",
+
]
+

+
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4027,7 +4091,7 @@ dependencies = [
 "windows_aarch64_gnullvm 0.52.6",
 "windows_aarch64_msvc 0.52.6",
 "windows_i686_gnu 0.52.6",
-
 "windows_i686_gnullvm",
+
 "windows_i686_gnullvm 0.52.6",
 "windows_i686_msvc 0.52.6",
 "windows_x86_64_gnu 0.52.6",
 "windows_x86_64_gnullvm 0.52.6",
@@ -4035,6 +4099,22 @@ dependencies = [
]

[[package]]
+
name = "windows-targets"
+
version = "0.53.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
+
dependencies = [
+
 "windows_aarch64_gnullvm 0.53.0",
+
 "windows_aarch64_msvc 0.53.0",
+
 "windows_i686_gnu 0.53.0",
+
 "windows_i686_gnullvm 0.53.0",
+
 "windows_i686_msvc 0.53.0",
+
 "windows_x86_64_gnu 0.53.0",
+
 "windows_x86_64_gnullvm 0.53.0",
+
 "windows_x86_64_msvc 0.53.0",
+
]
+

+
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4047,6 +4127,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"

[[package]]
+
name = "windows_aarch64_gnullvm"
+
version = "0.53.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+

+
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4059,6 +4145,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"

[[package]]
+
name = "windows_aarch64_msvc"
+
version = "0.53.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+

+
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4071,12 +4163,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"

[[package]]
+
name = "windows_i686_gnu"
+
version = "0.53.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+

+
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"

[[package]]
+
name = "windows_i686_gnullvm"
+
version = "0.53.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+

+
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4089,6 +4193,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"

[[package]]
+
name = "windows_i686_msvc"
+
version = "0.53.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+

+
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4101,6 +4211,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"

[[package]]
+
name = "windows_x86_64_gnu"
+
version = "0.53.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+

+
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4113,6 +4229,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"

[[package]]
+
name = "windows_x86_64_gnullvm"
+
version = "0.53.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+

+
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4125,6 +4247,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

[[package]]
+
name = "windows_x86_64_msvc"
+
version = "0.53.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+

+
[[package]]
name = "winnow"
version = "0.6.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
modified crates/radicle-term/Cargo.toml
@@ -16,7 +16,11 @@ default = ["git2"]
anyhow = { workspace = true }
anstyle-query = "1.0.0"
crossterm = "0.29.0"
-
inquire = { version = "0.7.4", default-features = false, features = ["crossterm", "editor"] }
+
indicatif = { version = "0.18.0" }
+
inquire = { version = "0.7.4", default-features = false, features = [
+
    "crossterm",
+
    "editor",
+
] }
thiserror = { workspace = true }
unicode-display-width = "0.3.0"
unicode-segmentation = "1.7.1"
modified crates/radicle-term/src/io.rs
@@ -17,6 +17,7 @@ use crate::{style, Paint, Size};
pub use inquire;
pub use inquire::Select;

+
pub const SUCCESS_PREFIX: Paint<&str> = Paint::green("✓");
pub const ERROR_PREFIX: Paint<&str> = Paint::red("✗");
pub const ERROR_HINT_PREFIX: Paint<&str> = Paint::yellow("✗ Hint:");
pub const WARNING_PREFIX: Paint<&str> = Paint::yellow("!");
@@ -41,6 +42,24 @@ pub static CONFIG: LazyLock<RenderConfig> = LazyLock::new(|| RenderConfig {
    ..RenderConfig::default_colored()
});

+
/// Render target
+
#[derive(Clone)]
+
pub enum RenderTarget {
+
    Stdout,
+
    Stderr,
+
    Hidden,
+
}
+

+
impl RenderTarget {
+
    pub fn writer(&self) -> Box<dyn io::Write> {
+
        match self {
+
            RenderTarget::Stdout => Box::new(io::stdout()),
+
            RenderTarget::Stderr => Box::new(io::stderr()),
+
            RenderTarget::Hidden => Box::new(io::sink()),
+
        }
+
    }
+
}
+

#[macro_export]
macro_rules! info {
    ($writer:expr; $($arg:tt)*) => ({
@@ -107,7 +126,7 @@ macro_rules! notice {

pub use info;
pub use notice;
-
pub use success;
+
pub use success as success_macro;
pub use tip;

pub fn success_args<W: io::Write>(w: &mut W, args: fmt::Arguments) {
@@ -217,6 +236,10 @@ pub fn subcommand(msg: impl fmt::Display) {
    println!("{}", style(format!("Running `{msg}`...")).dim());
}

+
pub fn success(success: impl fmt::Display) {
+
    println!("{} {success}", SUCCESS_PREFIX);
+
}
+

pub fn warning(warning: impl fmt::Display) {
    println!(
        "{} {} {warning}",
modified crates/radicle-term/src/spinner.rs
@@ -1,10 +1,10 @@
use std::io::IsTerminal;
-
use std::mem::ManuallyDrop;
-
use std::sync::{Arc, Mutex};
-
use std::{fmt, io, thread, time};
+
use std::time::Duration;
+
use std::{fmt, io, time};

-
use crate::io::{ERROR_PREFIX, WARNING_PREFIX};
-
use crate::Paint;
+
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
+

+
use crate::{error, success, warning, Paint, RenderTarget};

/// How much time to wait between spinner animation updates.
pub const DEFAULT_TICK: time::Duration = time::Duration::from_millis(99);
@@ -16,92 +16,54 @@ pub const DEFAULT_STYLE: [Paint<&'static str>; 4] = [
    Paint::blue("◥"),
];

-
const CLEAR_UNTIL_NEWLINE: crossterm::terminal::Clear =
-
    crossterm::terminal::Clear(crossterm::terminal::ClearType::UntilNewLine);
-

-
struct Progress {
-
    state: State,
-
    message: Paint<String>,
-
}
-

-
impl Progress {
-
    fn new(message: Paint<String>) -> Self {
-
        Self {
-
            state: State::Running { cursor: 0 },
-
            message,
+
impl From<RenderTarget> for ProgressDrawTarget {
+
    fn from(value: RenderTarget) -> Self {
+
        match value {
+
            RenderTarget::Stdout => ProgressDrawTarget::stdout(),
+
            RenderTarget::Stderr => ProgressDrawTarget::stderr(),
+
            RenderTarget::Hidden => ProgressDrawTarget::hidden(),
        }
    }
}

-
enum State {
-
    Running { cursor: usize },
-
    Canceled,
-
    Done,
-
    Warn,
-
    Error,
-
}
-

/// A progress spinner.
pub struct Spinner {
-
    progress: Arc<Mutex<Progress>>,
-
    handle: ManuallyDrop<thread::JoinHandle<()>>,
-
}
-

-
impl Drop for Spinner {
-
    fn drop(&mut self) {
-
        if let Ok(mut progress) = self.progress.lock() {
-
            if let State::Running { .. } = progress.state {
-
                progress.state = State::Canceled;
-
            }
-
        }
-
        unsafe { ManuallyDrop::take(&mut self.handle) }
-
            .join()
-
            .unwrap();
-
    }
+
    inner: ProgressBar,
+
    completion: RenderTarget,
}

impl Spinner {
    /// Mark the spinner as successfully completed.
    pub fn finish(self) {
-
        if let Ok(mut progress) = self.progress.lock() {
-
            progress.state = State::Done;
-
        }
+
        let msg = self.inner.message();
+
        self.inner.finish_and_clear();
+
        success!(&mut self.completion.writer(); "{msg}");
    }

    /// Mark the spinner as failed. This cancels the spinner.
    pub fn failed(self) {
-
        if let Ok(mut progress) = self.progress.lock() {
-
            progress.state = State::Error;
-
        }
+
        let msg = self.inner.message();
+
        self.inner.finish_and_clear();
+
        error!(&mut self.completion.writer(); "{msg}");
    }

    /// Cancel the spinner with an error.
    pub fn error(self, msg: impl fmt::Display) {
-
        if let Ok(mut progress) = self.progress.lock() {
-
            progress.state = State::Error;
-
            progress.message = Paint::new(format!(
-
                "{} {} {}",
-
                progress.message,
-
                Paint::red("error:"),
-
                msg
-
            ));
-
        }
+
        self.inner.finish_and_clear();
+
        error!(&mut self.completion.writer(); "{msg}");
    }

    /// Cancel the spinner with a warning sign.
    pub fn warn(self) {
-
        if let Ok(mut progress) = self.progress.lock() {
-
            progress.state = State::Warn;
-
        }
+
        let msg = self.inner.message();
+
        self.inner.finish_and_clear();
+
        warning!(&mut self.completion.writer(); "{msg}");
    }

    /// Set the spinner's message.
    pub fn message(&mut self, msg: impl fmt::Display) {
        let msg = msg.to_string();
-

-
        if let Ok(mut progress) = self.progress.lock() {
-
            progress.message = Paint::new(msg);
-
        }
+
        self.inner.set_message(msg);
    }
}

@@ -109,11 +71,10 @@ impl Spinner {
/// failure messages to `stdout`. This function handles signals, with there being only one
/// element handling signals at a time, and is a wrapper to [`spinner_to()`].
pub fn spinner(message: impl ToString) -> Spinner {
-
    let (stdout, stderr) = (io::stdout(), io::stderr());
-
    if stderr.is_terminal() {
-
        spinner_to(message, stdout, stderr)
+
    if io::stderr().is_terminal() {
+
        spinner_to(message, RenderTarget::Stderr, RenderTarget::Stdout)
    } else {
-
        spinner_to(message, stdout, io::sink())
+
        spinner_to(message, RenderTarget::Hidden, RenderTarget::Stdout)
    }
}

@@ -126,120 +87,24 @@ pub fn spinner(message: impl ToString) -> Spinner {
/// handlers, then it will not attempt to install handlers again, and continue running.
pub fn spinner_to(
    message: impl ToString,
-
    mut completion: impl io::Write + Send + 'static,
-
    mut animation: impl io::Write + Send + 'static,
+
    progress: RenderTarget,
+
    completion: RenderTarget,
) -> Spinner {
-
    let message = message.to_string();
-
    let progress = Arc::new(Mutex::new(Progress::new(Paint::new(message))));
-

-
    #[cfg(unix)]
-
    let (sig_tx, sig_rx) = crossbeam_channel::unbounded();
-

-
    #[cfg(unix)]
-
    let sig_result = radicle_signals::install(sig_tx);
-

-
    let handle = thread::Builder::new()
-
        .name(String::from("spinner"))
-
        .spawn({
-
            let progress = progress.clone();
-

-
            move || {
-
                write!(animation, "{}", crossterm::cursor::Hide).ok();
-

-
                loop {
-
                    let Ok(mut progress) = progress.lock() else {
-
                        break;
-
                    };
-
                    // If were unable to install handles, skip signal processing entirely.
-
                    #[cfg(unix)]
-
                    if sig_result.is_ok() {
-
                        match sig_rx.try_recv() {
-
                            Ok(sig)
-
                                if sig == radicle_signals::Signal::Interrupt
-
                                    || sig == radicle_signals::Signal::Terminate =>
-
                            {
-
                                write!(animation, "\r{CLEAR_UNTIL_NEWLINE}").ok();
-
                                writeln!(
-
                                    completion,
-
                                    "{ERROR_PREFIX} {} {}",
-
                                    &progress.message,
-
                                    Paint::red("<canceled>")
-
                                )
-
                                .ok();
-
                                drop(animation);
-
                                std::process::exit(-1);
-
                            }
-
                            Ok(_) => {}
-
                            Err(_) => {}
-
                        }
-
                    }
-
                    match &mut *progress {
-
                        Progress {
-
                            state: State::Running { cursor },
-
                            message,
-
                        } => {
-
                            let spinner = DEFAULT_STYLE[*cursor];
-

-
                            write!(animation, "\r{CLEAR_UNTIL_NEWLINE}{spinner} {message}",).ok();
-

-
                            *cursor += 1;
-
                            *cursor %= DEFAULT_STYLE.len();
-
                        }
-
                        Progress {
-
                            state: State::Done,
-
                            message,
-
                        } => {
-
                            write!(animation, "\r{CLEAR_UNTIL_NEWLINE}").ok();
-
                            writeln!(completion, "{} {message}", Paint::green("✓")).ok();
-
                            break;
-
                        }
-
                        Progress {
-
                            state: State::Canceled,
-
                            message,
-
                        } => {
-
                            write!(animation, "\r{CLEAR_UNTIL_NEWLINE}").ok();
-
                            writeln!(
-
                                completion,
-
                                "{ERROR_PREFIX} {message} {}",
-
                                Paint::red("<canceled>")
-
                            )
-
                            .ok();
-
                            break;
-
                        }
-
                        Progress {
-
                            state: State::Warn,
-
                            message,
-
                        } => {
-
                            write!(animation, "\r{CLEAR_UNTIL_NEWLINE}").ok();
-
                            writeln!(completion, "{WARNING_PREFIX} {message}").ok();
-
                            break;
-
                        }
-
                        Progress {
-
                            state: State::Error,
-
                            message,
-
                        } => {
-
                            write!(animation, "\r{CLEAR_UNTIL_NEWLINE}").ok();
-
                            writeln!(completion, "{ERROR_PREFIX} {message}").ok();
-
                            break;
-
                        }
-
                    }
-
                    drop(progress);
-
                    thread::sleep(DEFAULT_TICK);
-
                }
-

-
                write!(animation, "{}", crossterm::cursor::Show).ok();
-

-
                #[cfg(unix)]
-
                if sig_result.is_ok() {
-
                    let _ = radicle_signals::uninstall();
-
                }
-
            }
-
        })
-
        // SAFETY: Only panics if the thread name contains `null` bytes, which isn't the case here.
-
        .unwrap();
-

-
    Spinner {
-
        progress,
-
        handle: ManuallyDrop::new(handle),
-
    }
+
    let inner = ProgressBar::new_spinner();
+

+
    inner.enable_steady_tick(Duration::from_millis(120));
+
    inner.set_draw_target(progress.into());
+
    inner.set_message(message.to_string());
+
    inner.set_style(
+
        ProgressStyle::with_template("{spinner:.blue} {msg}")
+
            .unwrap()
+
            .tick_strings(&[
+
                "\x1b[0;35m◢\x1b[0m",
+
                "\x1b[0;36m◣\x1b[0m",
+
                "\x1b[0;35m◤\x1b[0m",
+
                "\x1b[0;34m◥\x1b[0m",
+
            ]),
+
    );
+

+
    Spinner { inner, completion }
}