use crate::{Browser, BrowserOptions, Error, ErrorKind, Result};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
macro_rules! try_browser {
( $options: expr, $name:expr, $( $arg:expr ),+ ) => {
for_matching_path($name, |pb| {
let mut cmd = Command::new(pb);
$(
cmd.arg($arg);
)+
run_command(&mut cmd, !is_text_browser(&pb), $options)
})
}
}
#[inline]
pub fn open_browser_internal(browser: Browser, url: &str, options: &BrowserOptions) -> Result<()> {
match browser {
Browser::Default => open_browser_default(url, options),
_ => Err(Error::new(
ErrorKind::NotFound,
"only default browser supported",
)),
}
}
#[inline]
fn open_browser_default(url: &str, options: &BrowserOptions) -> Result<()> {
try_with_browser_env(url, options)
.or_else(|_| try_haiku(options, url))
.or_else(|_| try_browser!(options, "xdg-open", url))
.or_else(|r| match guess_desktop_env() {
"kde" => try_browser!(options, "kde-open", url)
.or_else(|_| try_browser!(options, "kde-open5", url))
.or_else(|_| try_browser!(options, "kfmclient", "newTab", url)),
"gnome" => try_browser!(options, "gio", "open", url)
.or_else(|_| try_browser!(options, "gvfs-open", url))
.or_else(|_| try_browser!(options, "gnome-open", url)),
"mate" => try_browser!(options, "gio", "open", url)
.or_else(|_| try_browser!(options, "gvfs-open", url))
.or_else(|_| try_browser!(options, "mate-open", url)),
"xfce" => try_browser!(options, "exo-open", url)
.or_else(|_| try_browser!(options, "gio", "open", url))
.or_else(|_| try_browser!(options, "gvfs-open", url)),
_ => Err(r),
})
.or_else(|_| try_browser!(options, "x-www-browser", url))
.map_err(|_| {
Error::new(
ErrorKind::NotFound,
"No valid browsers detected. You can specify one in BROWSERS environment variable",
)
})
.map(|_| ())
}
#[inline]
fn try_with_browser_env(url: &str, options: &BrowserOptions) -> Result<()> {
for browser in std::env::var("BROWSER")
.unwrap_or_else(|_| String::from(""))
.split(':')
{
if !browser.is_empty() {
let cmdline = browser
.replace("%s", url)
.replace("%c", ":")
.replace("%%", "%");
let cmdarr: Vec<&str> = cmdline.split_ascii_whitespace().collect();
let browser_cmd = cmdarr[0];
let env_exit = for_matching_path(browser_cmd, |pb| {
let mut cmd = Command::new(pb);
for arg in cmdarr.iter().skip(1) {
cmd.arg(arg);
}
if !browser.contains("%s") {
cmd.arg(url);
}
run_command(&mut cmd, !is_text_browser(pb), options)
});
if env_exit.is_ok() {
return Ok(());
}
}
}
Err(Error::new(
ErrorKind::NotFound,
"No valid browser configured in BROWSER environment variable",
))
}
#[inline]
fn guess_desktop_env() -> &'static str {
let unknown = "unknown";
let xcd: String = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_else(|_| unknown.into())
.to_ascii_lowercase();
let dsession: String = std::env::var("DESKTOP_SESSION")
.unwrap_or_else(|_| unknown.into())
.to_ascii_lowercase();
if xcd.contains("gnome") || xcd.contains("cinnamon") || dsession.contains("gnome") {
"gnome"
} else if xcd.contains("kde")
|| std::env::var("KDE_FULL_SESSION").is_ok()
|| std::env::var("KDE_SESSION_VERSION").is_ok()
{
"kde"
} else if xcd.contains("mate") || dsession.contains("mate") {
"mate"
} else if xcd.contains("xfce") || dsession.contains("xfce") {
"xfce"
} else {
unknown
}
}
#[inline]
fn try_haiku(options: &BrowserOptions, url: &str) -> Result<()> {
if cfg!(target_os = "haiku") {
try_browser!(options, "open", url).map(|_| ())
} else {
Err(Error::new(ErrorKind::NotFound, "Not on haiku"))
}
}
#[inline]
fn is_text_browser(pb: &Path) -> bool {
for browser in TEXT_BROWSERS.iter() {
if pb.ends_with(&browser) {
return true;
}
}
false
}
#[inline]
fn for_matching_path<F>(name: &str, op: F) -> Result<()>
where
F: FnOnce(&PathBuf) -> Result<()>,
{
let err = Err(Error::new(ErrorKind::NotFound, "command not found"));
if name.contains(std::path::MAIN_SEPARATOR) {
let pb = std::path::PathBuf::from(name);
if let Ok(metadata) = pb.metadata() {
if metadata.is_file() && metadata.permissions().mode() & 0o111 != 0 {
return op(&pb);
}
} else {
return err;
}
} else {
if let Ok(path) = std::env::var("PATH") {
for entry in path.split(':') {
let mut pb = std::path::PathBuf::from(entry);
pb.push(name);
if let Ok(metadata) = pb.metadata() {
if metadata.is_file() && metadata.permissions().mode() & 0o111 != 0 {
return op(&pb);
}
}
}
}
}
err
}
#[inline]
fn run_command(cmd: &mut Command, background: bool, options: &BrowserOptions) -> Result<()> {
if options.dry_run {
return Ok(());
}
if background {
if options.suppress_output {
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
} else {
cmd
}
.spawn()
.map(|_| ())
} else {
cmd.status().and_then(|status| {
if status.success() {
Ok(())
} else {
Err(Error::new(
ErrorKind::Other,
"command present but exited unsuccessfully",
))
}
})
}
}
static TEXT_BROWSERS: [&str; 9] = [
"lynx", "links", "links2", "elinks", "w3m", "eww", "netrik", "retawq", "curl",
];