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" ))] |
31 | mod freedesktop; |
32 | #[cfg (not(any(target_os = "windows" , target_os = "macos" )))] |
33 | mod linux_and_more; |
34 | #[cfg (target_os = "macos" )] |
35 | mod macos; |
36 | #[cfg (target_os = "windows" )] |
37 | mod windows; |
38 | |
39 | #[cfg (not(any(target_os = "windows" , target_os = "macos" )))] |
40 | use crate::linux_and_more as sys; |
41 | #[cfg (target_os = "macos" )] |
42 | use crate::macos as sys; |
43 | #[cfg (target_os = "windows" )] |
44 | use crate::windows as sys; |
45 | |
46 | use std::error::Error; |
47 | use std::ffi::{OsStr, OsString}; |
48 | use std::fmt::{self, Display, Formatter}; |
49 | use std::process::{Command, ExitStatus, Stdio}; |
50 | use 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/ |
73 | pub fn open<P>(path: P) -> Result<(), OpenError> |
74 | where |
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()`]. |
85 | pub fn open_browser<P>(path: P) -> Result<(), OpenError> |
86 | where |
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" )] |
131 | pub fn reveal<P>(path: P) -> Result<(), OpenError> |
132 | where |
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)] |
142 | pub 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 | |
168 | impl 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 | |
195 | impl 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" )] |
206 | fn is_wsl() -> bool { |
207 | sys::is_wsl() |
208 | } |
209 | |
210 | #[cfg (not(target_os = "linux" ))] |
211 | fn is_wsl() -> bool { |
212 | false |
213 | } |
214 | |
215 | #[cfg (target_os = "linux" )] |
216 | fn 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" ))] |
237 | fn wsl_to_windows_path(_path: &OsStr) -> Option<OsString> { |
238 | unreachable!() |
239 | } |
240 | |
241 | #[cfg (not(target_os = "windows" ))] |
242 | fn 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 | |