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