1 #![doc(html_root_url = "https://docs.rs/opener/0.5.0")]
2 
3 //! This crate provides the [`open`] function, which opens a file or link with the default program
4 //! configured on the system:
5 //!
6 //! ```no_run
7 //! # fn main() -> Result<(), ::opener::OpenError> {
8 //! // open a website
9 //! opener::open("https://www.rust-lang.org")?;
10 //!
11 //! // open a file
12 //! opener::open("../Cargo.toml")?;
13 //! # Ok(())
14 //! # }
15 //! ```
16 //!
17 //! An [`open_browser`] function is also provided, for when you intend on opening a file or link in a
18 //! browser, specifically. This function works like the [`open`] function, but explicitly allows
19 //! overriding the browser launched by setting the `$BROWSER` environment variable.
20 
21 #![warn(
22     rust_2018_idioms,
23     deprecated_in_future,
24     macro_use_extern_crate,
25     missing_debug_implementations,
26     unused_qualifications
27 )]
28 
29 #[cfg(not(any(target_os = "windows", target_os = "macos")))]
30 mod linux_and_more;
31 #[cfg(target_os = "macos")]
32 mod macos;
33 #[cfg(target_os = "windows")]
34 mod windows;
35 
36 #[cfg(not(any(target_os = "windows", target_os = "macos")))]
37 use crate::linux_and_more as sys;
38 #[cfg(target_os = "macos")]
39 use crate::macos as sys;
40 #[cfg(target_os = "windows")]
41 use crate::windows as sys;
42 
43 use std::error::Error;
44 use std::ffi::{OsStr, OsString};
45 use std::fmt::{self, Display, Formatter};
46 use std::process::{Command, ExitStatus, Stdio};
47 use std::{env, io};
48 
49 /// Opens a file or link with the system default program.
50 ///
51 /// Note that a path like "rustup.rs" could potentially refer to either a file or a website. If you
52 /// want to open the website, you should add the "http://" prefix, for example.
53 ///
54 /// Also note that a result of `Ok(())` just means a way of opening the path was found, and no error
55 /// occurred as a direct result of opening the path. Errors beyond that point aren't caught. For
56 /// example, `Ok(())` would be returned even if a file was opened with a program that can't read the
57 /// file, or a dead link was opened in a browser.
58 ///
59 /// ## Platform Implementation Details
60 ///
61 /// - On Windows the `ShellExecuteW` Windows API function is used.
62 /// - On Mac the system `open` command is used.
63 /// - On Windows Subsystem for Linux (WSL), the system `wslview` from [`wslu`] is used if available,
64 /// otherwise the system `xdg-open` is used, if available.
65 /// - On non-WSL Linux and other platforms,
66 /// the system `xdg-open` script is used if available, otherwise an `xdg-open` script embedded in
67 /// this library is used.
68 ///
69 /// [`wslu`]: https://github.com/wslutilities/wslu/
open<P>(path: P) -> Result<(), OpenError> where P: AsRef<OsStr>,70 pub fn open<P>(path: P) -> Result<(), OpenError>
71 where
72     P: AsRef<OsStr>,
73 {
74     sys::open(path.as_ref())
75 }
76 
77 /// Opens a file or link with the system default program, using the `BROWSER` environment variable
78 /// when set.
79 ///
80 /// If the `BROWSER` environment variable is set, the program specified by it is used to open the
81 /// path. If not, behavior is identical to [`open()`].
open_browser<P>(path: P) -> Result<(), OpenError> where P: AsRef<OsStr>,82 pub fn open_browser<P>(path: P) -> Result<(), OpenError>
83 where
84     P: AsRef<OsStr>,
85 {
86     let mut path = path.as_ref();
87     if let Ok(browser_var) = env::var("BROWSER") {
88         let windows_path;
89         if is_wsl() && browser_var.ends_with(".exe") {
90             if let Some(windows_path_2) = wsl_to_windows_path(path) {
91                 windows_path = windows_path_2;
92                 path = &windows_path;
93             }
94         };
95 
96         Command::new(&browser_var)
97             .arg(path)
98             .stdin(Stdio::null())
99             .stdout(Stdio::null())
100             .stderr(Stdio::piped())
101             .spawn()
102             .map_err(OpenError::Io)?;
103 
104         Ok(())
105     } else {
106         sys::open(path)
107     }
108 }
109 
110 /// An error type representing the failure to open a path. Possibly returned by the [`open`]
111 /// function.
112 ///
113 /// The `ExitStatus` variant will never be returned on Windows.
114 #[derive(Debug)]
115 pub enum OpenError {
116     /// An IO error occurred.
117     Io(io::Error),
118 
119     /// A command exited with a non-zero exit status.
120     ExitStatus {
121         /// A string that identifies the command.
122         cmd: &'static str,
123 
124         /// The failed process's exit status.
125         status: ExitStatus,
126 
127         /// Anything the process wrote to stderr.
128         stderr: String,
129     },
130 }
131 
132 impl Display for OpenError {
fmt(&self, f: &mut Formatter<'_>) -> fmt::Result133     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
134         match self {
135             OpenError::Io(_) => {
136                 write!(f, "IO error")?;
137             }
138             OpenError::ExitStatus {
139                 cmd,
140                 status,
141                 stderr,
142             } => {
143                 write!(
144                     f,
145                     "command '{}' did not execute successfully; {}",
146                     cmd, status
147                 )?;
148 
149                 let stderr = stderr.trim();
150                 if !stderr.is_empty() {
151                     write!(f, "\ncommand stderr:\n{}", stderr)?;
152                 }
153             }
154         }
155 
156         Ok(())
157     }
158 }
159 
160 impl Error for OpenError {
source(&self) -> Option<&(dyn Error + 'static)>161     fn source(&self) -> Option<&(dyn Error + 'static)> {
162         match self {
163             OpenError::Io(inner) => Some(inner),
164             OpenError::ExitStatus { .. } => None,
165         }
166     }
167 }
168 
169 #[cfg(target_os = "linux")]
is_wsl() -> bool170 fn is_wsl() -> bool {
171     sys::is_wsl()
172 }
173 
174 #[cfg(not(target_os = "linux"))]
is_wsl() -> bool175 fn is_wsl() -> bool {
176     false
177 }
178 
179 #[cfg(target_os = "linux")]
wsl_to_windows_path(path: &OsStr) -> Option<OsString>180 fn wsl_to_windows_path(path: &OsStr) -> Option<OsString> {
181     use bstr::ByteSlice;
182     use std::os::unix::ffi::OsStringExt;
183 
184     let output = Command::new("wslpath")
185         .arg("-w")
186         .arg(path)
187         .stdin(Stdio::null())
188         .stdout(Stdio::piped())
189         .stderr(Stdio::null())
190         .output()
191         .ok()?;
192 
193     if !output.status.success() {
194         return None;
195     }
196 
197     Some(OsString::from_vec(output.stdout.trim_end().to_vec()))
198 }
199 
200 #[cfg(not(target_os = "linux"))]
wsl_to_windows_path(_path: &OsStr) -> Option<OsString>201 fn wsl_to_windows_path(_path: &OsStr) -> Option<OsString> {
202     unreachable!()
203 }
204 
205 #[cfg(not(target_os = "windows"))]
wait_child(child: &mut std::process::Child, cmd_name: &'static str) -> Result<(), OpenError>206 fn wait_child(child: &mut std::process::Child, cmd_name: &'static str) -> Result<(), OpenError> {
207     use std::io::Read;
208 
209     let exit_status = child.wait().map_err(OpenError::Io)?;
210     if exit_status.success() {
211         Ok(())
212     } else {
213         let mut stderr_output = String::new();
214         if let Some(stderr) = child.stderr.as_mut() {
215             stderr.read_to_string(&mut stderr_output).ok();
216         }
217 
218         Err(OpenError::ExitStatus {
219             cmd: cmd_name,
220             status: exit_status,
221             stderr: stderr_output,
222         })
223     }
224 }
225