diff --git a/Cargo.lock b/Cargo.lock index 3d8bca947c..0a9d95c3b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -406,6 +406,19 @@ dependencies = [ "memchr", ] +[[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", + "windows-sys 0.60.2", +] + [[package]] name = "content_inspector" version = "0.2.4" @@ -588,6 +601,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "enum-map" version = "2.7.3" @@ -1168,6 +1187,19 @@ dependencies = [ "hashbrown", ] +[[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", + "unit-prefix", + "web-time", +] + [[package]] name = "io-uring" version = "0.7.8" @@ -1720,6 +1752,12 @@ version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b02ffed1bc8c2234bb6f8e760e34613776c5102a041f25330b869a78153a68c" +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + [[package]] name = "potential_utf" version = "0.1.2" @@ -2195,17 +2233,20 @@ dependencies = [ "chrono", "clap", "clap_complete", + "console", "curl", "effective-limits", "enum-map", "env_proxy", "flate2", "fs_at", + "futures-util", "git-testament", "home", "http-body-util", "hyper", "hyper-util", + "indicatif", "itertools 0.14.0", "libc", "opener", @@ -3012,6 +3053,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[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 = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 5643211032..cc329991bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,14 +46,17 @@ cfg-if = "1.0" chrono = { version = "0.4", default-features = false, features = ["std"] } clap = { version = "4", features = ["derive", "wrap_help"] } clap_complete = "4" +console = "0.16" curl = { version = "0.4.44", optional = true } effective-limits = "0.5.5" enum-map = "2.5.0" env_proxy = { version = "0.4.1", optional = true } flate2 = { version = "1.1.1", default-features = false, features = ["zlib-rs"] } fs_at = "0.2.1" +futures-util = "0.3.31" git-testament = "0.2" home = "0.5.4" +indicatif = "0.18" itertools = "0.14" libc = "0.2" opener = "0.8.0" diff --git a/src/cli/rustup_mode.rs b/src/cli/rustup_mode.rs index b113d6c495..e72f5203b5 100644 --- a/src/cli/rustup_mode.rs +++ b/src/cli/rustup_mode.rs @@ -6,11 +6,15 @@ use std::{ path::{Path, PathBuf}, process::ExitStatus, str::FromStr, + time::Duration, }; use anyhow::{Context, Error, Result, anyhow}; use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum, builder::PossibleValue}; use clap_complete::Shell; +use console::style; +use futures_util::stream::StreamExt; +use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; use itertools::Itertools; use tracing::{info, trace, warn}; use tracing_subscriber::{EnvFilter, Registry, reload::Handle}; @@ -34,7 +38,7 @@ use crate::{ install::{InstallMethod, UpdateStatus}, process::{ Process, - terminalsource::{self, ColorableTerminal}, + terminalsource::{self, ColorChoice, ColorableTerminal}, }, toolchain::{ CustomToolchainName, DistributableToolchain, LocalToolchainName, @@ -792,45 +796,99 @@ async fn default_( } async fn check_updates(cfg: &Cfg<'_>, opts: CheckOpts) -> Result { + let t = cfg.process.stdout().terminal(cfg.process); + let is_a_tty = t.is_a_tty(); + let use_colors = matches!(t.color_choice(), ColorChoice::Auto | ColorChoice::Always); let mut update_available = false; - - let mut t = cfg.process.stdout().terminal(cfg.process); let channels = cfg.list_channels()?; - - for channel in channels { - let (name, distributable) = channel; - let current_version = distributable.show_version()?; - let dist_version = distributable.show_dist_version().await?; - let _ = t.attr(terminalsource::Attr::Bold); - write!(t.lock(), "{name} - ")?; - match (current_version, dist_version) { - (None, None) => { - let _ = t.fg(terminalsource::Color::Red); - writeln!(t.lock(), "Cannot identify installed or update versions")?; - } - (Some(cv), None) => { - let _ = t.fg(terminalsource::Color::Green); - write!(t.lock(), "Up to date")?; - let _ = t.reset(); - writeln!(t.lock(), " : {cv}")?; + let num_channels = channels.len(); + // Ensure that `.buffered()` is never called with 0 as this will cause a hang. + // See: https://github.com/rust-lang/futures-rs/pull/1194#discussion_r209501774 + if num_channels > 0 { + let multi_progress_bars = if is_a_tty { + MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(t))) + } else { + MultiProgress::with_draw_target(ProgressDrawTarget::hidden()) + }; + let channels = tokio_stream::iter(channels.into_iter()).map(|(name, distributable)| { + let pb = multi_progress_bars.add(ProgressBar::new(1)); + pb.set_style( + ProgressStyle::with_template("{msg:.bold} - Checking... {spinner:.green}") + .unwrap() + .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "), + ); + pb.set_message(format!("{name}")); + pb.enable_steady_tick(Duration::from_millis(100)); + async move { + let current_version = distributable.show_version()?; + let dist_version = distributable.show_dist_version().await?; + let mut update_a = false; + + let mut styled_name = style(format!("{name} - ")); + if use_colors { + styled_name = styled_name.bold(); + } + let message = match (current_version, dist_version) { + (None, None) => { + let mut m = style("Cannot identify installed or update versions"); + if use_colors { + m = m.red().bold(); + } + format!("{styled_name}{m}") + } + (Some(cv), None) => { + let mut m = style("Up to date"); + if use_colors { + m = m.green().bold(); + } + format!("{styled_name}{m} : {cv}") + } + (Some(cv), Some(dv)) => { + let mut m = style("Update available"); + if use_colors { + m = m.yellow().bold(); + } + update_a = true; + format!("{styled_name}{m} : {cv} -> {dv}") + } + (None, Some(dv)) => { + let mut m = style("Update available"); + if use_colors { + m = m.yellow().bold(); + } + update_a = true; + format!("{styled_name}{m} : (Unknown version) -> {dv}") + } + }; + pb.set_style(ProgressStyle::with_template(message.as_str()).unwrap()); + pb.finish(); + Ok::<(bool, String), Error>((update_a, message)) } - (Some(cv), Some(dv)) => { + }); + + // If we are running in a TTY, we can use `buffer_unordered` since + // displaying the output in the correct order is already handled by + // `indicatif`. + let channels = if is_a_tty { + channels + .buffer_unordered(num_channels) + .collect::>() + .await + } else { + channels.buffered(num_channels).collect::>().await + }; + + let t = cfg.process.stdout().terminal(cfg.process); + for result in channels { + let (update_a, message) = result?; + if update_a { update_available = true; - let _ = t.fg(terminalsource::Color::Yellow); - write!(t.lock(), "Update available")?; - let _ = t.reset(); - writeln!(t.lock(), " : {cv} -> {dv}")?; } - (None, Some(dv)) => { - update_available = true; - let _ = t.fg(terminalsource::Color::Yellow); - write!(t.lock(), "Update available")?; - let _ = t.reset(); - writeln!(t.lock(), " : (Unknown version) -> {dv}")?; + if !is_a_tty { + writeln!(t.lock(), "{message}")?; } } } - let self_update_mode = cfg.get_self_update_mode()?; // Priority: no-self-update feature > self_update_mode > no-self-update args. // Check for update only if rustup does **not** have the no-self-update feature, diff --git a/src/process/terminalsource.rs b/src/process/terminalsource.rs index a30ca3f8b0..29b90c4d33 100644 --- a/src/process/terminalsource.rs +++ b/src/process/terminalsource.rs @@ -1,3 +1,5 @@ +use console::Term; +use indicatif::TermLike; use std::{ io::{self, Write}, mem::MaybeUninit, @@ -6,8 +8,8 @@ use std::{ sync::{Arc, Mutex, MutexGuard}, }; -pub(crate) use termcolor::Color; -use termcolor::{ColorChoice, ColorSpec, StandardStream, StandardStreamLock, WriteColor}; +pub(crate) use termcolor::{Color, ColorChoice}; +use termcolor::{ColorSpec, StandardStream, StandardStreamLock, WriteColor}; use super::Process; #[cfg(feature = "test")] @@ -53,6 +55,8 @@ pub struct ColorableTerminal { // source is important because otherwise parallel constructed terminals // would not be locked out. inner: Arc>, + is_a_tty: bool, + color_choice: ColorChoice, } /// Internal state for ColorableTerminal @@ -84,10 +88,11 @@ impl ColorableTerminal { /// then color commands will be sent to the stream. /// Otherwise color commands are discarded. pub(super) fn new(stream: StreamSelector, process: &Process) -> Self { + let is_a_tty = stream.is_a_tty(process); let choice = match process.var("RUSTUP_TERM_COLOR") { Ok(s) if s.eq_ignore_ascii_case("always") => ColorChoice::Always, Ok(s) if s.eq_ignore_ascii_case("never") => ColorChoice::Never, - _ if stream.is_a_tty(process) => ColorChoice::Auto, + _ if is_a_tty => ColorChoice::Auto, _ => ColorChoice::Never, }; let inner = match stream { @@ -104,6 +109,8 @@ impl ColorableTerminal { }; ColorableTerminal { inner: Arc::new(Mutex::new(inner)), + is_a_tty, + color_choice: choice, } } @@ -179,6 +186,14 @@ impl ColorableTerminal { }; Ok(()) } + + pub fn is_a_tty(&self) -> bool { + self.is_a_tty + } + + pub fn color_choice(&self) -> ColorChoice { + self.color_choice + } } #[derive(Copy, Clone, Debug)] @@ -223,6 +238,81 @@ impl io::Write for ColorableTerminalLocked { } } +impl TermLike for ColorableTerminal { + fn width(&self) -> u16 { + Term::stdout().size().1 + } + + fn move_cursor_up(&self, n: usize) -> io::Result<()> { + // As the ProgressBar may try to move the cursor up by 0 lines, + // we need to handle that case to avoid writing an escape sequence + // that would mess up the terminal. + if n == 0 { + return Ok(()); + } + let mut t = self.lock(); + write!(t, "\x1b[{n}A")?; + t.flush() + } + + fn move_cursor_down(&self, n: usize) -> io::Result<()> { + if n == 0 { + return Ok(()); + } + let mut t = self.lock(); + write!(t, "\x1b[{n}B")?; + t.flush() + } + + fn move_cursor_right(&self, n: usize) -> io::Result<()> { + if n == 0 { + return Ok(()); + } + let mut t = self.lock(); + write!(t, "\x1b[{n}C")?; + t.flush() + } + + fn move_cursor_left(&self, n: usize) -> io::Result<()> { + if n == 0 { + return Ok(()); + } + let mut t = self.lock(); + write!(t, "\x1b[{n}D")?; + t.flush() + } + + fn write_line(&self, line: &str) -> io::Result<()> { + let mut t = self.lock(); + t.write_all(line.as_bytes())?; + t.write_all(b"\n")?; + t.flush() + } + + fn write_str(&self, s: &str) -> io::Result<()> { + let mut t = self.lock(); + t.write_all(s.as_bytes())?; + t.flush() + } + + fn clear_line(&self) -> io::Result<()> { + let mut t = self.lock(); + t.write_all(b"\r\x1b[2K")?; + t.flush() + } + + fn flush(&self) -> io::Result<()> { + let mut t = self.lock(); + t.flush() + } +} + +impl std::fmt::Debug for ColorableTerminal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ColorableTerminal {{ inner: ... }}") + } +} + #[cfg(test)] mod tests { use std::collections::HashMap;