diff --git a/Cargo.lock b/Cargo.lock index bd1b36c..6e735f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1033,11 +1033,9 @@ name = "rfd" version = "0.12.1" dependencies = [ "ashpd", - "async-io", "block", "dispatch", "futures", - "futures-util", "glib-sys", "gobject-sys", "gtk-sys", diff --git a/Cargo.toml b/Cargo.toml index 10f7ca2..ea91b07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ documentation = "https://docs.rs/rfd" default = ["gtk3"] file-handle-inner = [] gtk3 = ["gtk-sys", "glib-sys", "gobject-sys"] -xdg-portal = ["ashpd", "urlencoding", "pollster", "async-io", "futures-util"] +xdg-portal = ["ashpd", "urlencoding", "pollster"] common-controls-v6 = ["windows-sys/Win32_UI_Controls"] [dev-dependencies] @@ -41,10 +41,6 @@ windows-sys = { version = "0.48", features = [ ] } [target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd"))'.dependencies] -# Make sure that this is in sync with zbus, to avoid duplicated deps -async-io = { version = "1.3", optional = true } -futures-util = { version = "0.3", optional = true, default-features = false, features = ["io"] } - # XDG Desktop Portal ashpd = { version = "0.6", optional = true } urlencoding = { version = "2.1.0", optional = true } diff --git a/src/backend/linux/async_command.rs b/src/backend/linux/async_command.rs new file mode 100644 index 0000000..a21c545 --- /dev/null +++ b/src/backend/linux/async_command.rs @@ -0,0 +1,55 @@ +use std::{ + io, + pin::Pin, + sync::{Arc, Mutex}, + task::{Context, Poll, Waker}, +}; + +struct State { + waker: Option, + data: Option>, +} + +pub struct AsyncCommand { + state: Arc>, +} + +impl AsyncCommand { + pub fn spawn(mut command: std::process::Command) -> Self { + let state = Arc::new(Mutex::new(State { + waker: None, + data: None, + })); + + std::thread::spawn({ + let state = state.clone(); + move || { + let output = command.output(); + + let mut state = state.lock().unwrap(); + state.data = Some(output); + + if let Some(waker) = state.waker.take() { + waker.wake(); + } + } + }); + + Self { state } + } +} + +impl std::future::Future for AsyncCommand { + type Output = io::Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut state = self.state.lock().unwrap(); + + if state.data.is_some() { + Poll::Ready(state.data.take().unwrap()) + } else { + state.waker = Some(cx.waker().clone()); + Poll::Pending + } + } +} diff --git a/src/backend/linux/child_stdout.rs b/src/backend/linux/child_stdout.rs deleted file mode 100644 index f7593d7..0000000 --- a/src/backend/linux/child_stdout.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::{ - io, - pin::Pin, - task::{Context, Poll}, -}; - -use async_io::Async; -use futures_util::AsyncRead; - -pub struct ChildStdout(Async); - -impl ChildStdout { - pub fn new(stdout: std::process::ChildStdout) -> io::Result { - Async::new(stdout).map(Self) - } -} - -impl AsyncRead for ChildStdout { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut [u8], - ) -> Poll> { - Pin::new(&mut self.0).poll_read(cx, buf) - } -} diff --git a/src/backend/linux/mod.rs b/src/backend/linux/mod.rs index c719238..8bb2b7c 100644 --- a/src/backend/linux/mod.rs +++ b/src/backend/linux/mod.rs @@ -1,2 +1,2 @@ -mod child_stdout; +mod async_command; pub(crate) mod zenity; diff --git a/src/backend/linux/zenity.rs b/src/backend/linux/zenity.rs index 0488227..c8e0541 100644 --- a/src/backend/linux/zenity.rs +++ b/src/backend/linux/zenity.rs @@ -1,19 +1,15 @@ -use futures_util::AsyncReadExt; -use std::{ - error::Error, - fmt::Display, - path::PathBuf, - process::{Command, Stdio}, - time::Duration, -}; +use std::{error::Error, fmt::Display, path::PathBuf, process::Command}; -use super::child_stdout::ChildStdout; -use crate::{file_dialog::Filter, message_dialog::{MessageButtons, MessageLevel}, FileDialog, MessageDialogResult}; +use crate::{ + file_dialog::Filter, + message_dialog::{MessageButtons, MessageLevel}, + FileDialog, MessageDialogResult, +}; #[derive(Debug)] pub enum ZenityError { Io(std::io::Error), - StdOutNotFound, + FromUtf8Error(std::string::FromUtf8Error), } impl Error for ZenityError {} @@ -22,7 +18,7 @@ impl Display for ZenityError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ZenityError::Io(io) => write!(f, "{io}"), - ZenityError::StdOutNotFound => write!(f, "Stdout not found"), + ZenityError::FromUtf8Error(err) => err.fmt(f), } } } @@ -33,6 +29,12 @@ impl From for ZenityError { } } +impl From for ZenityError { + fn from(value: std::string::FromUtf8Error) -> Self { + Self::FromUtf8Error(value) + } +} + pub type ZenityResult = Result; fn command() -> Command { @@ -60,24 +62,11 @@ fn add_filename(command: &mut Command, file_name: &Option) { } } -async fn run(mut command: Command) -> ZenityResult> { - let mut process = command.stdout(Stdio::piped()).spawn()?; - - let stdout = process.stdout.take().ok_or(ZenityError::StdOutNotFound)?; - let mut stdout = ChildStdout::new(stdout)?; - - let mut buffer = String::new(); - stdout.read_to_string(&mut buffer).await?; - - let status = loop { - if let Some(status) = process.try_wait()? { - break status; - } - - async_io::Timer::after(Duration::from_millis(1)).await; - }; +async fn run(command: Command) -> ZenityResult> { + let res = super::async_command::AsyncCommand::spawn(command).await?; + let buffer = String::from_utf8(res.stdout)?; - Ok((status.success() || !buffer.is_empty()).then_some(buffer)) + Ok((res.status.success() || !buffer.is_empty()).then_some(buffer)) } #[allow(unused)] @@ -176,7 +165,11 @@ pub async fn message( }) } -pub async fn question(btns: &MessageButtons, title: &str, description: &str) -> ZenityResult { +pub async fn question( + btns: &MessageButtons, + title: &str, + description: &str, +) -> ZenityResult { let mut command = command(); command.args(["--question", "--title", title, "--text", description]); @@ -188,16 +181,16 @@ pub async fn question(btns: &MessageButtons, title: &str, description: &str) -> MessageButtons::OkCancelCustom(ok, cancel) => { command.args(["--ok-label", ok.as_str()]); command.args(["--cancel-label", cancel.as_str()]); - }, + } MessageButtons::YesNoCancel => { command.args(["--extra-button", "No"]); command.args(["--cancel-label", "Cancel"]); - }, + } MessageButtons::YesNoCancelCustom(yes, no, cancel) => { command.args(["--ok-label", yes.as_str()]); command.args(["--cancel-label", cancel.as_str()]); command.args(["--extra-button", no.as_str()]); - }, + } _ => {} } @@ -205,26 +198,26 @@ pub async fn question(btns: &MessageButtons, title: &str, description: &str) -> MessageButtons::OkCancel => match res { Some(_) => MessageDialogResult::Ok, None => MessageDialogResult::Cancel, - } + }, MessageButtons::YesNo => match res { Some(_) => MessageDialogResult::Yes, None => MessageDialogResult::No, - } + }, MessageButtons::OkCancelCustom(ok, cancel) => match res { Some(_) => MessageDialogResult::Custom(ok.clone()), None => MessageDialogResult::Custom(cancel.clone()), - } + }, MessageButtons::YesNoCancel => match res { Some(output) if output.is_empty() => MessageDialogResult::Yes, Some(_) => MessageDialogResult::No, None => MessageDialogResult::Cancel, - } + }, MessageButtons::YesNoCancelCustom(yes, no, cancel) => match res { Some(output) if output.is_empty() => MessageDialogResult::Custom(yes.clone()), Some(_) => MessageDialogResult::Custom(no.clone()), None => MessageDialogResult::Custom(cancel.clone()), - } - _ => MessageDialogResult::Cancel + }, + _ => MessageDialogResult::Cancel, }) } @@ -235,21 +228,21 @@ mod tests { #[test] #[ignore] fn message() { - async_io::block_on(super::message( + pollster::block_on(super::message( &crate::message_dialog::MessageLevel::Info, &crate::message_dialog::MessageButtons::Ok, "hi", "me", )) .unwrap(); - async_io::block_on(super::message( + pollster::block_on(super::message( &crate::message_dialog::MessageLevel::Warning, &crate::message_dialog::MessageButtons::Ok, "hi", "me", )) .unwrap(); - async_io::block_on(super::message( + pollster::block_on(super::message( &crate::message_dialog::MessageLevel::Error, &crate::message_dialog::MessageButtons::Ok, "hi", @@ -261,13 +254,13 @@ mod tests { #[test] #[ignore] fn question() { - async_io::block_on(super::question( + pollster::block_on(super::question( &crate::message_dialog::MessageButtons::OkCancel, "hi", "me", )) .unwrap(); - async_io::block_on(super::question( + pollster::block_on(super::question( &crate::message_dialog::MessageButtons::YesNo, "hi", "me", @@ -278,28 +271,28 @@ mod tests { #[test] #[ignore] fn pick_file() { - let path = async_io::block_on(super::pick_file(&FileDialog::default())).unwrap(); + let path = pollster::block_on(super::pick_file(&FileDialog::default())).unwrap(); dbg!(path); } #[test] #[ignore] fn pick_files() { - let path = async_io::block_on(super::pick_files(&FileDialog::default())).unwrap(); + let path = pollster::block_on(super::pick_files(&FileDialog::default())).unwrap(); dbg!(path); } #[test] #[ignore] fn pick_folder() { - let path = async_io::block_on(super::pick_folder(&FileDialog::default())).unwrap(); + let path = pollster::block_on(super::pick_folder(&FileDialog::default())).unwrap(); dbg!(path); } #[test] #[ignore] fn save_file() { - let path = async_io::block_on(super::save_file(&FileDialog::default())).unwrap(); + let path = pollster::block_on(super::save_file(&FileDialog::default())).unwrap(); dbg!(path); } }