Skip to main content

webbrowser/
lib.rs

1//! Rust library to open URLs and local files in the web browsers available on a platform, with guarantees of [Consistent Behaviour](#consistent-behaviour).
2//!
3//! Inspired by the [webbrowser](https://docs.python.org/2/library/webbrowser.html) python library.
4//!
5//! ## Examples
6//!
7//! ```no_run
8//! use webbrowser;
9//!
10//! if webbrowser::open("http://github.com").is_ok() {
11//!     // ...
12//! }
13//! ```
14//!
15//! ## Platform Support Status
16//!
17//! | Platform              | Supported | Browsers | Test status |
18//! |-----------------------|-----------|----------|-------------|
19//! | macOS                 | ✅        | default + [others](https://docs.rs/webbrowser/latest/webbrowser/enum.Browser.html) | ✅ |
20//! | windows               | ✅        | default only | ✅ |
21//! | linux/wsl             | ✅        | default only (respects $BROWSER env var, so can be used with other browsers) | ✅ |
22//! | android               | ✅        | default only | ✅ |
23//! | iOS/tvOS/visionOS     | ✅        | default only | ✅ |
24//! | wasm                  | ✅        | default only | ✅ |
25//! | unix (*bsd, aix etc.) | ✅        | default only (respects $BROWSER env var, so can be used with other browsers) | Manual |
26//!
27//! ## Consistent Behaviour
28//! `webbrowser` defines consistent behaviour on all platforms as follows:
29//! * **Browser guarantee** - This library guarantees that the browser is opened, even for local files - the only crate to make such guarantees
30//!   at the time of this writing. Alternative libraries rely on existing system commands, which may lead to an editor being opened (instead
31//!   of the browser) for local html files, leading to an inconsistent behaviour for users.
32//! * **Non-Blocking** for GUI based browsers (e.g. Firefox, Chrome etc.), while **Blocking** for text based browser (e.g. lynx etc.)
33//! * **Suppressed output** by default for GUI based browsers, so that their stdout/stderr don't pollute the main program's output. This can be
34//!   overridden by `webbrowser::open_browser_with_options`.
35//!
36//! ## Crate Features
37//! `webbrowser` optionally allows the following features to be configured:
38//! * `hardened` - this disables handling of non-http(s) urls (e.g. `file:///`) as a hard security precaution
39//! * `disable-wsl` - this disables WSL `file` implementation (`http` still works)
40//! * `wasm-console` - this enables logging to wasm console (valid only on wasm platform)
41
42#[cfg_attr(
43    any(target_os = "ios", target_os = "tvos", target_os = "visionos"),
44    path = "ios.rs"
45)]
46#[cfg_attr(target_os = "macos", path = "macos.rs")]
47#[cfg_attr(target_os = "android", path = "android.rs")]
48#[cfg_attr(target_family = "wasm", path = "wasm.rs")]
49#[cfg_attr(windows, path = "windows.rs")]
50#[cfg_attr(
51    all(
52        unix,
53        not(any(
54            target_os = "ios",
55            target_os = "tvos",
56            target_os = "visionos",
57            target_os = "macos",
58            target_os = "android",
59            target_family = "wasm",
60            windows,
61        )),
62    ),
63    path = "unix.rs"
64)]
65mod os;
66
67#[cfg(any(
68    windows,
69    all(
70        unix,
71        not(any(
72            target_os = "ios",
73            target_os = "tvos",
74            target_os = "visionos",
75            target_os = "macos",
76            target_os = "android",
77            target_family = "wasm",
78        )),
79    ),
80))]
81pub(crate) mod common;
82
83use std::fmt::Display;
84use std::io::{Error, ErrorKind, Result};
85use std::ops::Deref;
86use std::str::FromStr;
87use std::{error, fmt};
88
89#[derive(Debug, Default, Eq, PartialEq, Copy, Clone, Hash)]
90/// Browser types available
91pub enum Browser {
92    ///Operating system's default browser
93    #[default]
94    Default,
95
96    ///Mozilla Firefox
97    Firefox,
98
99    ///Microsoft's Internet Explorer
100    InternetExplorer,
101
102    ///Google Chrome
103    Chrome,
104
105    ///Opera
106    Opera,
107
108    ///Mac OS Safari
109    Safari,
110
111    ///Haiku's WebPositive
112    WebPositive,
113}
114
115impl Browser {
116    /// Returns true if there is likely a browser detected in the system
117    pub fn is_available() -> bool {
118        Browser::Default.exists()
119    }
120
121    /// Returns true if this specific browser is detected in the system
122    pub fn exists(&self) -> bool {
123        open_browser_with_options(
124            *self,
125            "https://rootnet.in",
126            BrowserOptions::new().with_dry_run(true),
127        )
128        .is_ok()
129    }
130}
131
132///The Error type for parsing a string into a Browser.
133#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
134pub struct ParseBrowserError;
135
136impl fmt::Display for ParseBrowserError {
137    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
138        f.write_str("Invalid browser given")
139    }
140}
141
142impl error::Error for ParseBrowserError {
143    fn description(&self) -> &str {
144        "invalid browser"
145    }
146}
147
148impl fmt::Display for Browser {
149    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
150        match *self {
151            Browser::Default => f.write_str("Default"),
152            Browser::Firefox => f.write_str("Firefox"),
153            Browser::InternetExplorer => f.write_str("Internet Explorer"),
154            Browser::Chrome => f.write_str("Chrome"),
155            Browser::Opera => f.write_str("Opera"),
156            Browser::Safari => f.write_str("Safari"),
157            Browser::WebPositive => f.write_str("WebPositive"),
158        }
159    }
160}
161
162impl FromStr for Browser {
163    type Err = ParseBrowserError;
164
165    fn from_str(s: &str) -> ::std::result::Result<Self, Self::Err> {
166        match s {
167            "firefox" => Ok(Browser::Firefox),
168            "default" => Ok(Browser::Default),
169            "ie" | "internet explorer" | "internetexplorer" => Ok(Browser::InternetExplorer),
170            "chrome" => Ok(Browser::Chrome),
171            "opera" => Ok(Browser::Opera),
172            "safari" => Ok(Browser::Safari),
173            "webpositive" => Ok(Browser::WebPositive),
174            _ => Err(ParseBrowserError),
175        }
176    }
177}
178
179#[derive(Debug, Eq, PartialEq, Clone, Hash)]
180/// BrowserOptions to override certain default behaviour. Any option named as a `hint` is
181/// not guaranteed to be honoured. Use [BrowserOptions::new()] to create.
182///
183/// e.g. by default, we suppress stdout/stderr, but that behaviour can be overridden here
184pub struct BrowserOptions {
185    suppress_output: bool,
186    target_hint: String,
187    dry_run: bool,
188    #[cfg(target_os = "macos")]
189    dont_switch: bool,
190}
191
192impl fmt::Display for BrowserOptions {
193    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
194        f.write_fmt(format_args!(
195            "BrowserOptions(supress_output={}, target_hint={}, dry_run={})",
196            self.suppress_output, self.target_hint, self.dry_run
197        ))
198    }
199}
200
201impl std::default::Default for BrowserOptions {
202    fn default() -> Self {
203        let target_hint = String::from(option_env!("WEBBROWSER_WASM_TARGET").unwrap_or("_blank"));
204        BrowserOptions {
205            suppress_output: true,
206            target_hint,
207            dry_run: false,
208            #[cfg(target_os = "macos")]
209            dont_switch: false,
210        }
211    }
212}
213
214impl BrowserOptions {
215    /// Create a new instance. Configure it with one of the `with_` methods.
216    pub fn new() -> Self {
217        Self::default()
218    }
219
220    /// Determines whether stdout/stderr of the appropriate browser command is suppressed
221    /// or not
222    pub fn with_suppress_output(&mut self, suppress_output: bool) -> &mut Self {
223        self.suppress_output = suppress_output;
224        self
225    }
226
227    /// Hint to the browser to open the url in the corresponding
228    /// [target](https://www.w3schools.com/tags/att_a_target.asp). Note that this is just
229    /// a hint, it may or may not be honoured (currently guaranteed only in wasm).
230
231    // TODO:remove this lint suppression once we're past the MSRV of 1.63 as that's when
232    // clone_into() became stable.
233    #[allow(clippy::all)]
234    pub fn with_target_hint(&mut self, target_hint: &str) -> &mut Self {
235        self.target_hint = target_hint.to_owned();
236        self
237    }
238
239    /// Do not do an actual execution, just return true if this would've likely
240    /// succeeded. Note the "likely" here - it's still indicative than guaranteed.
241    pub fn with_dry_run(&mut self, dry_run: bool) -> &mut Self {
242        self.dry_run = dry_run;
243        self
244    }
245
246    #[cfg(target_os = "macos")]
247    /// Do not switch to the browser window. This is macOS only right now.
248    pub fn with_dont_switch(&mut self, dont_switch: bool) -> &mut Self {
249        self.dont_switch = dont_switch;
250        self
251    }
252}
253
254/// Opens the URL on the default browser of this platform
255///
256/// Returns Ok(..) so long as the browser invocation was successful. An Err(..) is returned in the
257/// following scenarios:
258/// * The requested browser was not found
259/// * There was an error in opening the browser
260/// * `hardened` feature is enabled, and the URL was not a valid http(s) url, say a `file:///`
261/// * On ios/android/wasm, if the url is not a valid http(s) url
262///
263/// Equivalent to:
264/// ```no_run
265/// # use webbrowser::{Browser, open_browser};
266/// # let url = "http://example.com";
267/// open_browser(Browser::Default, url);
268/// ```
269///
270/// # Examples
271/// ```no_run
272/// use webbrowser;
273///
274/// if webbrowser::open("http://github.com").is_ok() {
275///     // ...
276/// }
277/// ```
278pub fn open(url: &str) -> Result<()> {
279    open_browser(Browser::Default, url)
280}
281
282/// Opens the specified URL on the specific browser (if available) requested. Return semantics are
283/// the same as for [open](fn.open.html).
284///
285/// # Examples
286/// ```no_run
287/// use webbrowser::{open_browser, Browser};
288///
289/// if open_browser(Browser::Firefox, "http://github.com").is_ok() {
290///     // ...
291/// }
292/// ```
293pub fn open_browser(browser: Browser, url: &str) -> Result<()> {
294    open_browser_with_options(browser, url, &BrowserOptions::default())
295}
296
297/// Opens the specified URL on the specific browser (if available) requested, while overriding the
298/// default options.
299///
300/// Return semantics are
301/// the same as for [open](fn.open.html).
302///
303/// # Examples
304/// ```no_run
305/// use webbrowser::{open_browser_with_options, Browser, BrowserOptions};
306///
307/// if open_browser_with_options(Browser::Default, "http://github.com", BrowserOptions::new().with_suppress_output(false)).is_ok() {
308///     // ...
309/// }
310/// ```
311pub fn open_browser_with_options(
312    browser: Browser,
313    url: &str,
314    options: &BrowserOptions,
315) -> Result<()> {
316    let target = TargetType::try_from(url)?;
317
318    // if feature:hardened is enabled, make sure we accept only HTTP(S) URLs
319    #[cfg(feature = "hardened")]
320    if !target.is_http() {
321        return Err(Error::new(
322            ErrorKind::InvalidInput,
323            "only http/https urls allowed",
324        ));
325    }
326
327    if cfg!(any(
328        target_os = "ios",
329        target_os = "tvos",
330        target_os = "visionos",
331        target_os = "macos",
332        target_os = "android",
333        target_family = "wasm",
334        windows,
335        unix,
336    )) {
337        os::open_browser_internal(browser, &target, options)
338    } else {
339        Err(Error::new(ErrorKind::NotFound, "unsupported platform"))
340    }
341}
342
343/// The link we're trying to open, represented as a URL. Local files get represented
344/// via `file://...` URLs
345struct TargetType(url::Url);
346
347impl TargetType {
348    /// Returns true if this target represents an HTTP url, false otherwise
349    #[cfg(any(
350        feature = "hardened",
351        target_os = "android",
352        target_os = "ios",
353        target_os = "tvos",
354        target_os = "visionos",
355        target_family = "wasm"
356    ))]
357    fn is_http(&self) -> bool {
358        matches!(self.0.scheme(), "http" | "https")
359    }
360
361    /// If `target` represents a valid http/https url, return the str corresponding to it
362    /// else return `std::io::Error` of kind `std::io::ErrorKind::InvalidInput`
363    #[cfg(any(
364        target_os = "android",
365        target_os = "ios",
366        target_os = "tvos",
367        target_os = "visionos",
368        target_family = "wasm"
369    ))]
370    fn get_http_url(&self) -> Result<&str> {
371        if self.is_http() {
372            Ok(self.0.as_str())
373        } else {
374            Err(Error::new(ErrorKind::InvalidInput, "not an http url"))
375        }
376    }
377
378    #[cfg(not(target_family = "wasm"))]
379    fn from_file_path(value: &str) -> Result<Self> {
380        let pb = std::path::PathBuf::from(value);
381        let url = url::Url::from_file_path(if pb.is_relative() {
382            std::env::current_dir()?.join(pb)
383        } else {
384            pb
385        })
386        .map_err(|_| Error::new(ErrorKind::InvalidInput, "failed to convert path to url"))?;
387
388        Ok(Self(url))
389    }
390}
391
392impl Deref for TargetType {
393    type Target = str;
394
395    fn deref(&self) -> &Self::Target {
396        self.0.as_str()
397    }
398}
399
400impl Display for TargetType {
401    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402        (self as &str).fmt(f)
403    }
404}
405
406impl std::convert::TryFrom<&str> for TargetType {
407    type Error = Error;
408
409    #[cfg(target_family = "wasm")]
410    fn try_from(value: &str) -> Result<Self> {
411        url::Url::parse(value)
412            .map(|u| Ok(Self(u)))
413            .map_err(|_| Error::new(ErrorKind::InvalidInput, "invalid url for wasm"))?
414    }
415
416    #[cfg(not(target_family = "wasm"))]
417    fn try_from(value: &str) -> Result<Self> {
418        match url::Url::parse(value) {
419            Ok(u) => {
420                if u.scheme().len() == 1 && cfg!(windows) {
421                    // this can happen in windows that C:\abc.html gets parsed as scheme "C"
422                    Self::from_file_path(value)
423                } else {
424                    Ok(Self(u))
425                }
426            }
427            Err(_) => Self::from_file_path(value),
428        }
429    }
430}
431
432#[test]
433#[ignore]
434fn test_open_firefox() {
435    assert!(open_browser(Browser::Firefox, "http://github.com").is_ok());
436}
437
438#[test]
439#[ignore]
440fn test_open_chrome() {
441    assert!(open_browser(Browser::Chrome, "http://github.com").is_ok());
442}
443
444#[test]
445#[ignore]
446fn test_open_safari() {
447    assert!(open_browser(Browser::Safari, "http://github.com").is_ok());
448}
449
450#[test]
451#[ignore]
452fn test_open_webpositive() {
453    assert!(open_browser(Browser::WebPositive, "http://github.com").is_ok());
454}