Skip to content

Commit 614cacf

Browse files
committed
Bring consistent behaviour on Unix. Non-terminal browsers are guaranteed to not block the thread
1 parent ecb43e7 commit 614cacf

File tree

1 file changed

+150
-53
lines changed

1 file changed

+150
-53
lines changed

src/unix.rs

Lines changed: 150 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
use crate::{Browser, Error, ErrorKind, Result};
2-
use std::os::unix::process::ExitStatusExt;
3-
use std::process::{Command, ExitStatus};
2+
use std::os::unix::fs::PermissionsExt;
3+
use std::path::PathBuf;
4+
use std::process::{Command, Stdio};
45

5-
mod common;
6-
use common::from_status;
6+
macro_rules! try_browser {
7+
( $name:expr, $( $arg:expr ),+ ) => {
8+
for_matching_path($name, |pb| {
9+
let mut cmd = Command::new(pb);
10+
$(
11+
cmd.arg($arg);
12+
)+
13+
run_command(&mut cmd, !is_text_browser(&pb))
14+
})
15+
}
16+
}
717

818
/// Deal with opening of browsers on Linux and *BSD - currently supports only the default browser
919
///
@@ -12,68 +22,155 @@ use common::from_status;
1222
/// 2. Attempt to open the url via xdg-open, gvfs-open, gnome-open, open, respectively, whichever works
1323
/// first
1424
#[inline]
15-
pub fn open_browser_internal(browser: Browser, url: &str) -> Result<()> {
16-
from_status(open_browser_unix(browser, url))
17-
}
18-
19-
fn open_browser_unix(browser: Browser, url: &str) -> Result<ExitStatus> {
20-
match browser {
21-
Browser::Default => open_on_unix_using_browser_env(url)
22-
.or_else(|_| -> Result<ExitStatus> { Command::new("xdg-open").arg(url).status() })
23-
.or_else(|r| -> Result<ExitStatus> {
24-
if let Ok(desktop) = ::std::env::var("XDG_CURRENT_DESKTOP") {
25-
if desktop == "KDE" {
26-
return Command::new("kioclient").arg("exec").arg(url).status();
27-
}
28-
}
29-
Err(r) // If either `if` check fails, fall through to the next or_else
30-
})
31-
.or_else(|_| -> Result<ExitStatus> { Command::new("gvfs-open").arg(url).status() })
32-
.or_else(|_| -> Result<ExitStatus> { Command::new("gnome-open").arg(url).status() })
33-
.or_else(|_| -> Result<ExitStatus> {
34-
Command::new("kioclient").arg("exec").arg(url).status()
35-
})
36-
.or_else(|e| -> Result<ExitStatus> {
37-
if let Ok(_child) = Command::new("x-www-browser").arg(url).spawn() {
38-
return Ok(ExitStatusExt::from_raw(0));
39-
}
40-
Err(e)
41-
}),
42-
_ => Err(Error::new(
43-
ErrorKind::NotFound,
44-
"Only the default browser is supported on this platform right now",
45-
)),
46-
}
25+
pub fn open_browser_internal(_: Browser, url: &str) -> Result<()> {
26+
// we first try with the $BROWSER env
27+
try_with_browser_env(url)
28+
// then we try with xdg-open
29+
.or_else(|_| try_browser!("xdg-open", url))
30+
// else do desktop specific stuff
31+
.or_else(|r| {
32+
// detect desktop
33+
let desktop_env: String = std::env::var("XDG_CURRENT_DESKTOP")
34+
.unwrap_or_else(|_| String::from("unknown"))
35+
.to_ascii_uppercase();
36+
match desktop_env.as_str() {
37+
"KDE" => try_browser!("kde-open", url).or_else(|_| try_browser!("kde-open5", url)),
38+
"GNOME" | "CINNAMON" => try_browser!("gio", "open", url)
39+
.or_else(|_| try_browser!("gvfs-open", url))
40+
.or_else(|_| try_browser!("gnome-open", url)),
41+
"MATE" => try_browser!("gio", "open", url)
42+
.or_else(|_| try_browser!("gvfs-open", url))
43+
.or_else(|_| try_browser!("mate-open", url)),
44+
"XFCE" => try_browser!("exo-open", url)
45+
.or_else(|_| try_browser!("gio", "open", url))
46+
.or_else(|_| try_browser!("gvfs-open", url)),
47+
_ => Err(r),
48+
}
49+
})
50+
// at the end, we'll try x-www-browser and return the result as is
51+
.or_else(|_| try_browser!("x-www-browser", url))
52+
// and convert the result into a () on success
53+
.and_then(|_| Ok(()))
54+
.or_else(|_| {
55+
Err(Error::new(
56+
ErrorKind::NotFound,
57+
"No valid browsers detected. You can specify one in BROWSERS environment variable",
58+
))
59+
})
4760
}
4861

49-
fn open_on_unix_using_browser_env(url: &str) -> Result<ExitStatus> {
50-
let browsers = ::std::env::var("BROWSER")
51-
.map_err(|_| -> Error { Error::new(ErrorKind::NotFound, "BROWSER env not set") })?;
52-
for browser in browsers.split(':') {
53-
// $BROWSER can contain ':' delimited options, each representing a potential browser command line
62+
#[inline]
63+
fn try_with_browser_env(url: &str) -> Result<()> {
64+
// $BROWSER can contain ':' delimited options, each representing a potential browser command line
65+
for browser in std::env::var("BROWSER")
66+
.unwrap_or_else(|_| String::from(""))
67+
.split(':')
68+
{
5469
if !browser.is_empty() {
5570
// each browser command can have %s to represent URL, while %c needs to be replaced
5671
// with ':' and %% with '%'
5772
let cmdline = browser
5873
.replace("%s", url)
5974
.replace("%c", ":")
6075
.replace("%%", "%");
61-
let cmdarr: Vec<&str> = cmdline.split_whitespace().collect();
62-
let mut cmd = Command::new(&cmdarr[0]);
63-
if cmdarr.len() > 1 {
64-
cmd.args(&cmdarr[1..cmdarr.len()]);
65-
}
66-
if !browser.contains("%s") {
67-
// append the url as an argument only if it was not already set via %s
68-
cmd.arg(url);
69-
}
70-
if let Ok(status) = cmd.status() {
71-
return Ok(status);
76+
let cmdarr: Vec<&str> = cmdline.split_ascii_whitespace().collect();
77+
let browser_cmd = cmdarr[0];
78+
let env_exit = for_matching_path(browser_cmd, |pb| {
79+
let mut cmd = Command::new(pb);
80+
for i in 1..cmdarr.len() {
81+
cmd.arg(cmdarr[i]);
82+
}
83+
if !browser.contains("%s") {
84+
// append the url as an argument only if it was not already set via %s
85+
cmd.arg(url);
86+
}
87+
run_command(&mut cmd, !is_text_browser(&pb))
88+
});
89+
if env_exit.is_ok() {
90+
return Ok(());
7291
}
7392
}
7493
}
7594
Err(Error::new(
7695
ErrorKind::NotFound,
77-
"No valid command in $BROWSER",
96+
"No valid browser configured in BROWSER environment variable",
7897
))
7998
}
99+
100+
/// Returns true if specified command refers to a known list of text browsers
101+
#[inline]
102+
fn is_text_browser(pb: &PathBuf) -> bool {
103+
for browser in TEXT_BROWSERS.iter() {
104+
if pb.ends_with(&browser) {
105+
return true;
106+
}
107+
}
108+
false
109+
}
110+
111+
#[inline]
112+
fn for_matching_path<F>(name: &str, op: F) -> Result<()>
113+
where
114+
F: FnOnce(&PathBuf) -> Result<()>,
115+
{
116+
let err = Err(Error::new(ErrorKind::NotFound, "command not found"));
117+
118+
// if the name already includes path separator, we should not try to do a PATH search on it
119+
// as it's likely an absolutely or relative name, so we treat it as such.
120+
if name.contains(std::path::MAIN_SEPARATOR) {
121+
let pb = std::path::PathBuf::from(name);
122+
if let Ok(metadata) = pb.metadata() {
123+
if metadata.is_file() && metadata.permissions().mode() & 0o111 != 0 {
124+
return op(&pb);
125+
}
126+
} else {
127+
return err;
128+
}
129+
} else {
130+
// search for this name inside PATH
131+
if let Ok(path) = std::env::var("PATH") {
132+
for entry in path.split(":") {
133+
let mut pb = std::path::PathBuf::from(entry);
134+
pb.push(name);
135+
if let Ok(metadata) = pb.metadata() {
136+
if metadata.is_file() && metadata.permissions().mode() & 0o111 != 0 {
137+
return op(&pb);
138+
}
139+
}
140+
}
141+
}
142+
}
143+
// return the not found err, if we didn't find anything above
144+
err
145+
}
146+
147+
/// Run the specified command in foreground/background
148+
#[inline]
149+
fn run_command(cmd: &mut Command, background: bool) -> Result<()> {
150+
if background {
151+
// if we're in background, set stdin/stdout to null and spawn a child, as we're
152+
// not supposed to have any interaction.
153+
cmd.stdin(Stdio::null())
154+
.stdout(Stdio::null())
155+
.stderr(Stdio::null())
156+
.spawn()
157+
.and_then(|_| Ok(()))
158+
} else {
159+
// if we're in foreground, use status() instead of spawn(), as we'd like to wait
160+
// till completion
161+
cmd.status().and_then(|status| {
162+
if status.success() {
163+
Ok(())
164+
} else {
165+
Err(Error::new(
166+
ErrorKind::Other,
167+
"command present but exited unsuccessfully",
168+
))
169+
}
170+
})
171+
}
172+
}
173+
174+
static TEXT_BROWSERS: [&'static str; 9] = [
175+
"lynx", "links", "links2", "elinks", "w3m", "eww", "netrik", "retawq", "curl",
176+
];

0 commit comments

Comments
 (0)