diff --git a/Cargo.lock b/Cargo.lock index 762db806..d169540b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -477,6 +477,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[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" @@ -847,6 +860,12 @@ dependencies = [ "phf", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "env_filter" version = "0.1.3" @@ -1822,6 +1841,19 @@ dependencies = [ "serde", ] +[[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" @@ -1846,7 +1878,7 @@ dependencies = [ "once_cell", "tempfile", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.11", ] [[package]] @@ -3004,6 +3036,7 @@ dependencies = [ "crossbeam-channel", "crossterm 0.29.0", "git2", + "indicatif", "inquire", "libc", "pretty_assertions", @@ -4124,6 +4157,18 @@ version = "0.1.11" 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" @@ -4290,6 +4335,16 @@ dependencies = [ "unicode-ident", ] +[[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" @@ -4421,6 +4476,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[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" @@ -4445,13 +4509,29 @@ 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", "windows_x86_64_msvc 0.52.6", ] +[[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" @@ -4464,6 +4544,12 @@ version = "0.52.6" 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" @@ -4476,6 +4562,12 @@ version = "0.52.6" 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" @@ -4488,12 +4580,24 @@ version = "0.52.6" 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" @@ -4506,6 +4610,12 @@ version = "0.52.6" 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" @@ -4518,6 +4628,12 @@ version = "0.52.6" 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" @@ -4530,6 +4646,12 @@ version = "0.52.6" 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" @@ -4542,6 +4664,12 @@ version = "0.52.6" 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" diff --git a/crates/radicle-term/Cargo.toml b/crates/radicle-term/Cargo.toml index 15504225..01d8d2ca 100644 --- a/crates/radicle-term/Cargo.toml +++ b/crates/radicle-term/Cargo.toml @@ -15,7 +15,11 @@ default = ["git2"] [dependencies] 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" diff --git a/crates/radicle-term/src/spinner.rs b/crates/radicle-term/src/spinner.rs index c5ad8e2e..16575b0d 100644 --- a/crates/radicle-term/src/spinner.rs +++ b/crates/radicle-term/src/spinner.rs @@ -3,11 +3,12 @@ use std::mem::ManuallyDrop; use std::sync::{Arc, Mutex}; use std::{fmt, io, thread, time}; -use crate::io::{PREFIX_ERROR, PREFIX_WARNING}; -use crate::Paint; +use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle}; + +use crate::{Paint, PaintTarget}; /// How much time to wait between spinner animation updates. -pub const DEFAULT_TICK: time::Duration = time::Duration::from_millis(99); +pub const DEFAULT_TICK: time::Duration = time::Duration::from_millis(120); /// The spinner animation strings. pub const DEFAULT_STYLE: [Paint<&'static str>; 4] = [ Paint::magenta("◢"), @@ -16,8 +17,23 @@ pub const DEFAULT_STYLE: [Paint<&'static str>; 4] = [ Paint::blue("◥"), ]; -const CLEAR_UNTIL_NEWLINE: crossterm::terminal::Clear = - crossterm::terminal::Clear(crossterm::terminal::ClearType::UntilNewLine); +impl From for ProgressDrawTarget { + fn from(value: PaintTarget) -> Self { + match value { + PaintTarget::Stdout => ProgressDrawTarget::stdout(), + PaintTarget::Stderr => ProgressDrawTarget::stderr(), + PaintTarget::Hidden => ProgressDrawTarget::hidden(), + } + } +} + +enum State { + Running, + Canceled, + Done, + Warn, + Error, +} struct Progress { state: State, @@ -27,20 +43,12 @@ struct Progress { impl Progress { fn new(message: Paint) -> Self { Self { - state: State::Running { cursor: 0 }, + state: State::Running, message, } } } -enum State { - Running { cursor: usize }, - Canceled, - Done, - Warn, - Error, -} - /// A progress spinner. pub struct Spinner { progress: Arc>, @@ -50,10 +58,11 @@ pub struct Spinner { impl Drop for Spinner { fn drop(&mut self) { if let Ok(mut progress) = self.progress.lock() { - if let State::Running { .. } = progress.state { + if let State::Running = progress.state { progress.state = State::Canceled; } } + unsafe { ManuallyDrop::take(&mut self.handle) } .join() .unwrap(); @@ -109,11 +118,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, PaintTarget::Stderr, PaintTarget::Stdout) } else { - spinner_to(message, stdout, io::sink()) + spinner_to(message, PaintTarget::Hidden, PaintTarget::Stdout) } } @@ -126,11 +134,11 @@ 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_target: PaintTarget, + completion_target: PaintTarget, ) -> Spinner { let message = message.to_string(); - let progress = Arc::new(Mutex::new(Progress::new(Paint::new(message)))); + let progress = Arc::new(Mutex::new(Progress::new(Paint::new(message.clone())))); #[cfg(unix)] let (sig_tx, sig_rx) = crossbeam_channel::unbounded(); @@ -142,10 +150,22 @@ pub fn spinner_to( .name(String::from("spinner")) .spawn({ let progress = progress.clone(); + let spinner = ProgressBar::new_spinner(); + + spinner.set_draw_target(progress_target.into()); + spinner.set_message(message.to_string()); + spinner.set_style( + ProgressStyle::with_template("{spinner:.blue} {msg}") + .unwrap() + .tick_strings(&[ + DEFAULT_STYLE[0].to_string().as_str(), + DEFAULT_STYLE[1].to_string().as_str(), + DEFAULT_STYLE[2].to_string().as_str(), + DEFAULT_STYLE[3].to_string().as_str(), + ]), + ); move || { - write!(animation, "{}", crossterm::cursor::Hide).ok(); - loop { let Ok(mut progress) = progress.lock() else { break; @@ -158,15 +178,14 @@ pub fn spinner_to( if sig == radicle_signals::Signal::Interrupt || sig == radicle_signals::Signal::Terminate => { - write!(animation, "\r{CLEAR_UNTIL_NEWLINE}").ok(); + spinner.finish_and_clear(); writeln!( - completion, - "{PREFIX_ERROR} {} {}", - &progress.message, + completion_target.writer(), + "{} {message} {}", + super::PREFIX_ERROR, Paint::red("") ) .ok(); - drop(animation); std::process::exit(-1); } Ok(_) => {} @@ -175,51 +194,67 @@ pub fn spinner_to( } match &mut *progress { Progress { - state: State::Running { cursor }, + state: State::Running, message, } => { - let spinner = DEFAULT_STYLE[*cursor]; - - write!(animation, "\r{CLEAR_UNTIL_NEWLINE}{spinner} {message}",).ok(); - - *cursor += 1; - *cursor %= DEFAULT_STYLE.len(); + spinner.set_message(message.to_string()); + spinner.inc(1); } + Progress { state: State::Done, message, } => { - write!(animation, "\r{CLEAR_UNTIL_NEWLINE}").ok(); - writeln!(completion, "{} {message}", super::PREFIX_SUCCESS).ok(); + spinner.finish_and_clear(); + writeln!( + completion_target.writer(), + "{} {message}", + super::PREFIX_SUCCESS + ) + .ok(); break; } + Progress { state: State::Canceled, message, } => { - write!(animation, "\r{CLEAR_UNTIL_NEWLINE}").ok(); + spinner.finish_and_clear(); writeln!( - completion, - "{PREFIX_ERROR} {message} {}", + completion_target.writer(), + "{} {message} {}", + super::PREFIX_ERROR, Paint::red("") ) .ok(); break; } + Progress { state: State::Warn, message, } => { - write!(animation, "\r{CLEAR_UNTIL_NEWLINE}").ok(); - writeln!(completion, "{PREFIX_WARNING} {message}").ok(); + spinner.finish_and_clear(); + writeln!( + completion_target.writer(), + "{} {message}", + super::PREFIX_WARNING + ) + .ok(); break; } + Progress { state: State::Error, message, } => { - write!(animation, "\r{CLEAR_UNTIL_NEWLINE}").ok(); - writeln!(completion, "{PREFIX_ERROR} {message}").ok(); + spinner.finish_and_clear(); + writeln!( + completion_target.writer(), + "{} {message}", + super::PREFIX_ERROR + ) + .ok(); break; } } @@ -227,8 +262,6 @@ pub fn spinner_to( thread::sleep(DEFAULT_TICK); } - write!(animation, "{}", crossterm::cursor::Show).ok(); - #[cfg(unix)] if sig_result.is_ok() { let _ = radicle_signals::uninstall();