From ecfbf66daa0cc139bd557bd7899a183bd6575990 Mon Sep 17 00:00:00 2001 From: Amod Malviya Date: Sat, 19 Feb 2022 12:44:01 +0530 Subject: [PATCH] Document consistent behaviour, and provide way of overriding suppression of output --- README.md | 5 ++++ src/android.rs | 6 ++-- src/lib.rs | 79 ++++++++++++++++++++++++++++++++++++++++---------- src/macos.rs | 23 +++++++++++---- src/unix.rs | 68 +++++++++++++++++++++++-------------------- src/wasm.rs | 19 +++++++----- src/windows.rs | 8 +++-- 7 files changed, 143 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 9604218..ac28244 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,11 @@ if webbrowser::open("http://github.com").is_ok() { | haiku | ✅ (experimental) | default only | ❌ | | ios | ❌ | default only | ❌ | +## Consistent Behaviour +`webbrowser` defines consistent behaviour on all platforms as follows: +* **Non-Blocking** for GUI based browsers (e.g. Firefox, Chrome etc.), while **Blocking** for text based browser (e.g. lynx etc.) +* **Suppressed output** by default for GUI based browsers, so that their stdout/stderr don't pollute the main program's output. This can be overridden by `webbrowser::open_browser_with_options`. + ## Looking to contribute? PRs invited for diff --git a/src/android.rs b/src/android.rs index f31cb56..d0fc7cc 100644 --- a/src/android.rs +++ b/src/android.rs @@ -1,11 +1,11 @@ -use crate::{Browser, Error, ErrorKind, Result}; +use crate::{Browser, BrowserOptions, Error, ErrorKind, Result}; use jni::objects::JValue; pub use std::os::unix::process::ExitStatusExt; use std::process::ExitStatus; -/// Deal with opening of browsers on Android +/// Deal with opening of browsers on Android. BrowserOptions are ignored here. #[inline] -pub fn open_browser_internal(_: Browser, url: &str) -> Result<()> { +pub fn open_browser_internal(_: Browser, url: &str, _: &BrowserOptions) -> Result<()> { // Create a VM for executing Java calls let native_activity = ndk_glue::native_activity(); let vm_ptr = native_activity.vm(); diff --git a/src/lib.rs b/src/lib.rs index 75fb153..0d8420f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,17 @@ //! //! Inspired by the [webbrowser](https://docs.python.org/2/library/webbrowser.html) python library. //! -//! ### Platform Support Status +//! ## Examples +//! +//! ```no_run +//! use webbrowser; +//! +//! if webbrowser::open("http://github.com").is_ok() { +//! // ... +//! } +//! ``` +//! +//! ## Platform Support Status //! //! | Platform | Supported | Browsers | Test status | //! |----------|-----------|----------|-------------| @@ -14,20 +24,10 @@ //! | haiku | ✅ (experimental) | default only | ❌ | //! | ios | ❌ | unsupported | ❌ | //! -//! Important note: -//! -//! * This library requires availability of browsers and a graphical environment during runtime -//! * `cargo test` will actually open the browser locally. -//! -//! # Examples -//! -//! ```no_run -//! use webbrowser; -//! -//! if webbrowser::open("http://github.com").is_ok() { -//! // ... -//! } -//! ``` +//! ## Consistent Behaviour +//! `webbrowser` defines consistent behaviour on all platforms as follows: +//! * **Non-Blocking** for GUI based browsers (e.g. Firefox, Chrome etc.), while **Blocking** for text based browser (e.g. lynx etc.) +//! * **Suppressed output** by default for GUI based browsers, so that their stdout/stderr don't pollute the main program's output. This can be overridden by `webbrowser::open_browser_with_options`. #[cfg_attr(target_os = "macos", path = "macos.rs")] #[cfg_attr(target_os = "android", path = "android.rs")] @@ -141,6 +141,31 @@ impl FromStr for Browser { } } +#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] +/// BrowserOptions to override certain default behaviour +/// +/// e.g. by default, we suppress stdout/stderr, but that behaviour can be overridden here +pub struct BrowserOptions { + pub suppress_output: bool, +} + +impl fmt::Display for BrowserOptions { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_fmt(format_args!( + "BrowserOptions(supress_output={})", + self.suppress_output + )) + } +} + +impl std::default::Default for BrowserOptions { + fn default() -> Self { + BrowserOptions { + suppress_output: true, + } + } +} + /// Opens the URL on the default browser of this platform /// /// Returns Ok(..) so long as the browser invocation was successful. An Err(..) is returned only if @@ -177,11 +202,33 @@ pub fn open(url: &str) -> Result<()> { /// } /// ``` pub fn open_browser(browser: Browser, url: &str) -> Result<()> { + open_browser_with_options(browser, url, &BrowserOptions::default()) +} + +/// Opens the specified URL on the specific browser (if available) requested, while overriding the +/// default options. +/// +/// Return semantics are +/// the same as for [open](fn.open.html). +/// +/// # Examples +/// ```no_run +/// use webbrowser::{open_browser_with_options, Browser, BrowserOptions}; +/// +/// if open_browser_with_options(Browser::Default, "http://github.com", &BrowserOptions { suppress_output: false }).is_ok() { +/// // ... +/// } +/// ``` +pub fn open_browser_with_options( + browser: Browser, + url: &str, + options: &BrowserOptions, +) -> Result<()> { let url_s: String = match url::Url::parse(url) { Ok(u) => u.as_str().into(), Err(_) => url.into(), }; - os::open_browser_internal(browser, &url_s) + os::open_browser_internal(browser, &url_s, options) } #[test] diff --git a/src/macos.rs b/src/macos.rs index e00f700..c642bc9 100644 --- a/src/macos.rs +++ b/src/macos.rs @@ -1,12 +1,16 @@ -use crate::{Browser, Error, ErrorKind, Result}; -use std::process::Command; +use crate::{Browser, BrowserOptions, Error, ErrorKind, Result}; +use std::process::{Command, Stdio}; mod common; use common::from_status; /// Deal with opening of browsers on Mac OS X, using `open` command #[inline] -pub fn open_browser_internal(browser: Browser, url_raw: &str) -> Result<()> { +pub fn open_browser_internal( + browser: Browser, + url_raw: &str, + options: &BrowserOptions, +) -> Result<()> { let url_s: String = match url::Url::parse(url_raw) { Ok(u) => u.as_str().into(), Err(_) => url_raw.into(), @@ -14,7 +18,7 @@ pub fn open_browser_internal(browser: Browser, url_raw: &str) -> Result<()> { let url = &url_s; let mut cmd = Command::new("open"); match browser { - Browser::Default => from_status(cmd.arg(url).status()), + Browser::Default => run_command(cmd.arg(url), options), _ => { let app: Option<&str> = match browser { Browser::Firefox => Some("Firefox"), @@ -25,7 +29,7 @@ pub fn open_browser_internal(browser: Browser, url_raw: &str) -> Result<()> { _ => None, }; match app { - Some(name) => from_status(cmd.arg("-a").arg(name).arg(url).status()), + Some(name) => run_command(cmd.arg("-a").arg(name).arg(url), options), None => Err(Error::new( ErrorKind::NotFound, format!("Unsupported browser {:?}", browser), @@ -34,3 +38,12 @@ pub fn open_browser_internal(browser: Browser, url_raw: &str) -> Result<()> { } } } + +fn run_command(cmd: &mut Command, options: &BrowserOptions) -> Result<()> { + if options.suppress_output { + cmd.stdout(Stdio::null()) + .stdin(Stdio::null()) + .stderr(Stdio::null()); + } + from_status(cmd.status()) +} diff --git a/src/unix.rs b/src/unix.rs index f2733cc..fe71ed1 100644 --- a/src/unix.rs +++ b/src/unix.rs @@ -1,16 +1,16 @@ -use crate::{Browser, Error, ErrorKind, Result}; +use crate::{Browser, BrowserOptions, Error, ErrorKind, Result}; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; macro_rules! try_browser { - ( $name:expr, $( $arg:expr ),+ ) => { + ( $options: expr, $name:expr, $( $arg:expr ),+ ) => { for_matching_path($name, |pb| { let mut cmd = Command::new(pb); $( cmd.arg($arg); )+ - run_command(&mut cmd, !is_text_browser(&pb)) + run_command(&mut cmd, !is_text_browser(&pb), $options) }) } } @@ -23,35 +23,35 @@ macro_rules! try_browser { /// 3. Attempt to use window manager specific commands, like gnome-open, kde-open etc. /// 4. Fallback to x-www-browser #[inline] -pub fn open_browser_internal(_: Browser, url: &str) -> Result<()> { +pub fn open_browser_internal(_: Browser, url: &str, options: &BrowserOptions) -> Result<()> { // we first try with the $BROWSER env - try_with_browser_env(url) + try_with_browser_env(url, options) // allow for haiku's open specifically - .or_else(|_| try_haiku(url)) + .or_else(|_| try_haiku(options, url)) // then we try with xdg-open - .or_else(|_| try_browser!("xdg-open", url)) + .or_else(|_| try_browser!(options, "xdg-open", url)) // else do desktop specific stuff .or_else(|r| match guess_desktop_env() { - "kde" => try_browser!("kde-open", url) - .or_else(|_| try_browser!("kde-open5", url)) - .or_else(|_| try_browser!("kfmclient", "newTab", url)), + "kde" => try_browser!(options, "kde-open", url) + .or_else(|_| try_browser!(options, "kde-open5", url)) + .or_else(|_| try_browser!(options, "kfmclient", "newTab", url)), - "gnome" => try_browser!("gio", "open", url) - .or_else(|_| try_browser!("gvfs-open", url)) - .or_else(|_| try_browser!("gnome-open", url)), + "gnome" => try_browser!(options, "gio", "open", url) + .or_else(|_| try_browser!(options, "gvfs-open", url)) + .or_else(|_| try_browser!(options, "gnome-open", url)), - "mate" => try_browser!("gio", "open", url) - .or_else(|_| try_browser!("gvfs-open", url)) - .or_else(|_| try_browser!("mate-open", url)), + "mate" => try_browser!(options, "gio", "open", url) + .or_else(|_| try_browser!(options, "gvfs-open", url)) + .or_else(|_| try_browser!(options, "mate-open", url)), - "xfce" => try_browser!("exo-open", url) - .or_else(|_| try_browser!("gio", "open", url)) - .or_else(|_| try_browser!("gvfs-open", url)), + "xfce" => try_browser!(options, "exo-open", url) + .or_else(|_| try_browser!(options, "gio", "open", url)) + .or_else(|_| try_browser!(options, "gvfs-open", url)), _ => Err(r), }) // at the end, we'll try x-www-browser and return the result as is - .or_else(|_| try_browser!("x-www-browser", url)) + .or_else(|_| try_browser!(options, "x-www-browser", url)) // if all above failed, map error to not found .map_err(|_| { Error::new( @@ -64,7 +64,7 @@ pub fn open_browser_internal(_: Browser, url: &str) -> Result<()> { } #[inline] -fn try_with_browser_env(url: &str) -> Result<()> { +fn try_with_browser_env(url: &str, options: &BrowserOptions) -> Result<()> { // $BROWSER can contain ':' delimited options, each representing a potential browser command line for browser in std::env::var("BROWSER") .unwrap_or_else(|_| String::from("")) @@ -88,7 +88,7 @@ fn try_with_browser_env(url: &str) -> Result<()> { // append the url as an argument only if it was not already set via %s cmd.arg(url); } - run_command(&mut cmd, !is_text_browser(pb)) + run_command(&mut cmd, !is_text_browser(pb), options) }); if env_exit.is_ok() { return Ok(()); @@ -136,9 +136,9 @@ fn guess_desktop_env() -> &'static str { // Handle Haiku explicitly, as it uses an "open" command, similar to macos // but on other Unixes, open ends up translating to shell open fd #[inline] -fn try_haiku(url: &str) -> Result<()> { +fn try_haiku(options: &BrowserOptions, url: &str) -> Result<()> { if cfg!(target_os = "haiku") { - try_browser!("open", url).map(|_| ()) + try_browser!(options, "open", url).map(|_| ()) } else { Err(Error::new(ErrorKind::NotFound, "Not on haiku")) } @@ -193,18 +193,24 @@ where /// Run the specified command in foreground/background #[inline] -fn run_command(cmd: &mut Command, background: bool) -> Result<()> { +fn run_command(cmd: &mut Command, background: bool, options: &BrowserOptions) -> Result<()> { if background { // if we're in background, set stdin/stdout to null and spawn a child, as we're // not supposed to have any interaction. - cmd.stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .map(|_| ()) + if options.suppress_output { + cmd.stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + } else { + cmd + } + .spawn() + .map(|_| ()) } else { // if we're in foreground, use status() instead of spawn(), as we'd like to wait - // till completion + // till completion. + // We also specifically don't supress anything here, because we're running here + // most likely because of a text browser cmd.status().and_then(|status| { if status.success() { Ok(()) diff --git a/src/wasm.rs b/src/wasm.rs index e8b8ac8..078708d 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -1,27 +1,30 @@ -use crate::{Browser, Error, ErrorKind, Result}; +use crate::{Browser, BrowserOptions, Error, ErrorKind, Result}; /// Deal with opening a URL in wasm32. This implementation ignores the browser attribute /// and always opens URLs in the same browser where wasm32 vm is running. #[inline] -pub fn open_browser_internal(_: Browser, url: &str) -> Result<()> { +pub fn open_browser_internal(_: Browser, url: &str, options: &BrowserOptions) -> Result<()> { // we can override the target by the env var WEBBROWSER_WASM_TARGET at compile time let configured_target = option_env!("WEBBROWSER_WASM_TARGET"); let window = web_sys::window(); match window { Some(w) => { let target = configured_target.unwrap_or_else(|| "_blank"); - wasm_console_log(&format!("target for url {} detected as {}", url, target)); + wasm_console_log( + &format!("target for url {} detected as {}", url, target), + options, + ); match w.open_with_url_and_target(url, target) { Ok(x) => match x { Some(_) => Ok(()), None => { - wasm_console_log(POPUP_ERR_MSG); + wasm_console_log(POPUP_ERR_MSG, options); Err(Error::new(ErrorKind::Other, POPUP_ERR_MSG)) } }, Err(_) => { - wasm_console_log("window error while opening url"); + wasm_console_log("window error while opening url", options); Err(Error::new(ErrorKind::Other, "error opening url")) } } @@ -34,9 +37,11 @@ pub fn open_browser_internal(_: Browser, url: &str) -> Result<()> { } /// Print to browser console -fn wasm_console_log(_msg: &str) { +fn wasm_console_log(_msg: &str, options: &BrowserOptions) { #[cfg(all(debug_assertions, feature = "wasm-console"))] - web_sys::console::log_1(&format!("[webbrowser] {}", &_msg).into()); + if !options.suppress_output { + web_sys::console::log_1(&format!("[webbrowser] {}", &_msg).into()); + } } const POPUP_ERR_MSG: &'static str = "popup blocked? window detected, but open_url failed"; diff --git a/src/windows.rs b/src/windows.rs index c55f88a..7a0c19b 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -1,16 +1,18 @@ extern crate widestring; extern crate winapi; -use crate::{Browser, Error, ErrorKind, Result}; +use crate::{Browser, BrowserOptions, Error, ErrorKind, Result}; pub use std::os::windows::process::ExitStatusExt; use std::ptr; use widestring::U16CString; /// Deal with opening of browsers on Windows, using [`ShellExecuteW`]( /// https://docs.microsoft.com/en-us/windows/desktop/api/shellapi/nf-shellapi-shellexecutew) -/// fucntion. +/// function. +/// +/// We ignore BrowserOptions on Windows. #[inline] -pub fn open_browser_internal(browser: Browser, url: &str) -> Result<()> { +pub fn open_browser_internal(browser: Browser, url: &str, _: &BrowserOptions) -> Result<()> { use winapi::shared::winerror::SUCCEEDED; use winapi::um::combaseapi::{CoInitializeEx, CoUninitialize}; use winapi::um::objbase::{COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE};