diff --git a/src/helpers.rs b/src/helpers.rs index 679328cd..5d6d350a 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,10 +1,9 @@ use std::{ - io::{stderr, stdin, Write}, path::{Path, PathBuf}, }; use cargo_toml::Manifest; -use log::{debug, info}; +use log::debug; use reqwest::Method; use serde::Serialize; use tinytemplate::TinyTemplate; @@ -18,6 +17,9 @@ pub use async_extracter::extract_archive_stream; mod auto_abort_join_handle; pub use auto_abort_join_handle::AutoAbortJoinHandle; +mod ui_thread; +pub use ui_thread::UIThread; + mod extracter; mod readable_rx; @@ -129,23 +131,6 @@ pub fn get_install_path>(install_path: Option

) -> Option Result<(), BinstallError> { - loop { - info!("Do you wish to continue? yes/[no]"); - eprint!("? "); - stderr().flush().ok(); - - let mut input = String::new(); - stdin().read_line(&mut input).unwrap(); - - match input.as_str().trim() { - "yes" | "y" | "YES" | "Y" => break Ok(()), - "no" | "n" | "NO" | "N" | "" => break Err(BinstallError::UserAbort), - _ => continue, - } - } -} - pub trait Template: Serialize { fn render(&self, template: &str) -> Result where diff --git a/src/helpers/ui_thread.rs b/src/helpers/ui_thread.rs new file mode 100644 index 00000000..8daf945c --- /dev/null +++ b/src/helpers/ui_thread.rs @@ -0,0 +1,97 @@ +use std::io::{self, BufRead, Write}; + +use tokio::sync::mpsc; +use tokio::task::spawn_blocking; + +use crate::BinstallError; + +#[derive(Debug)] +struct UIThreadInner { + /// Request for confirmation + request_tx: mpsc::Sender<()>, + + /// Confirmation + confirm_rx: mpsc::Receiver>, +} + +impl UIThreadInner { + fn new() -> Self { + let (request_tx, mut request_rx) = mpsc::channel(1); + let (confirm_tx, confirm_rx) = mpsc::channel(10); + + spawn_blocking(move || { + // This task should be the only one able to + // access stdin + let mut stdin = io::stdin().lock(); + let mut input = String::with_capacity(16); + + loop { + if request_rx.blocking_recv().is_none() { + break; + } + + // Lock stdout so that nobody can interfere + // with confirmation. + let mut stdout = io::stdout().lock(); + + let res = loop { + writeln!(&mut stdout, "Do you wish to continue? yes/[no]").unwrap(); + write!(&mut stdout, "? ").unwrap(); + stdout.flush().unwrap(); + + input.clear(); + stdin.read_line(&mut input).unwrap(); + + match input.as_str().trim() { + "yes" | "y" | "YES" | "Y" => break Ok(()), + "no" | "n" | "NO" | "N" | "" => break Err(BinstallError::UserAbort), + _ => continue, + } + }; + + confirm_tx + .blocking_send(res) + .expect("entry exits when confirming request"); + } + }); + + Self { + request_tx, + confirm_rx, + } + } + + async fn confirm(&mut self) -> Result<(), BinstallError> { + self.request_tx + .send(()) + .await + .map_err(|_| BinstallError::UserAbort)?; + + self.confirm_rx + .recv() + .await + .unwrap_or(Err(BinstallError::UserAbort)) + } +} + +#[derive(Debug)] +pub struct UIThread(Option); + +impl UIThread { + /// * `enable` - `true` to enable confirmation, `false` to disable it. + pub fn new(enable: bool) -> Self { + Self(if enable { + Some(UIThreadInner::new()) + } else { + None + }) + } + + pub async fn confirm(&mut self) -> Result<(), BinstallError> { + if let Some(inner) = self.0.as_mut() { + inner.confirm().await + } else { + Ok(()) + } + } +} diff --git a/src/main.rs b/src/main.rs index 55ce22bb..5b89353c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -191,6 +191,8 @@ async fn entry() -> Result<()> { ) .unwrap(); + let mut uithread = UIThread::new(!opts.no_confirm); + // Compute install directory let install_path = get_install_path(opts.install_path.as_deref()).ok_or_else(|| { error!("No viable install path found of specified, try `--install-path`"); @@ -228,8 +230,8 @@ async fn entry() -> Result<()> { }) ); - if !opts.no_confirm && !opts.dry_run { - confirm()?; + if !opts.dry_run { + uithread.confirm().await?; } } @@ -303,6 +305,7 @@ async fn entry() -> Result<()> { opts, package, temp_dir, + &mut uithread, ) .await } @@ -317,7 +320,7 @@ async fn entry() -> Result<()> { .first() .ok_or_else(|| miette!("No viable targets found, try with `--targets`"))?; - install_from_source(opts, package, target).await + install_from_source(opts, package, target, &mut uithread).await } } } @@ -331,6 +334,7 @@ async fn install_from_package( opts: Options, package: Package, temp_dir: TempDir, + uithread: &mut UIThread, ) -> Result<()> { // Prompt user for third-party source if fetcher.is_third_party() { @@ -338,8 +342,8 @@ async fn install_from_package( "The package will be downloaded from third-party source {}", fetcher.source_name() ); - if !opts.no_confirm && !opts.dry_run { - confirm()?; + if !opts.dry_run { + uithread.confirm().await?; } } else { info!( @@ -429,9 +433,7 @@ async fn install_from_package( return Ok(()); } - if !opts.no_confirm { - confirm()?; - } + uithread.confirm().await?; info!("Installing binaries..."); for file in &bin_files { @@ -456,11 +458,16 @@ async fn install_from_package( Ok(()) } -async fn install_from_source(opts: Options, package: Package, target: &str) -> Result<()> { +async fn install_from_source( + opts: Options, + package: Package, + target: &str, + uithread: &mut UIThread, +) -> Result<()> { // Prompt user for source install warn!("The package will be installed from source (with cargo)",); - if !opts.no_confirm && !opts.dry_run { - confirm()?; + if !opts.dry_run { + uithread.confirm().await?; } if opts.dry_run {