use std::ffi::OsStr;
#[cfg(target_os = "macos")]
use std::path::Path;
use std::process::Command;
use miette::{IntoDiagnostic, WrapErr, miette};
use crate::failure::Failure;
#[cfg(target_os = "macos")]
const LAUNCH_SERVICES_PLIST: &str =
"Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist";
#[cfg(target_os = "macos")]
const SAFARI_BUNDLE_ID: &str = "com.apple.Safari";
pub struct DefaultBrowser;
impl DefaultBrowser {
pub fn open(target: &OsStr) -> Result<(), Failure> {
let target_display = target.to_string_lossy();
let mut opener = Self::opener();
let opener_program = opener.get_program().to_string_lossy().into_owned();
let opener_status = opener
.arg(target)
.status()
.into_diagnostic()
.wrap_err_with(|| format!("cannot run `{opener_program}` to open {target_display}"))
.map_err(Failure::Io)?;
let is_opened = opener_status.success();
if is_opened {
return Ok(());
}
let opener_report = miette!("`{opener_program}` could not open {target_display}");
Err(Failure::Io(opener_report))
}
#[cfg(target_os = "macos")]
fn opener() -> Command {
let browser_bundle_id = Self::https_handler_bundle_id();
let mut opener = Command::new("open");
opener.args(["-b", &browser_bundle_id]);
opener
}
#[cfg(target_os = "windows")]
fn opener() -> Command {
let mut opener = Command::new("cmd");
opener.args(["/C", "start", ""]);
opener
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
fn opener() -> Command {
Command::new("xdg-open")
}
#[cfg(target_os = "macos")]
fn https_handler_bundle_id() -> String {
let Some(home) = std::env::var_os("HOME") else {
return SAFARI_BUNDLE_ID.to_owned();
};
let plist_path = Path::new(&home).join(LAUNCH_SERVICES_PLIST);
let conversion = Command::new("plutil")
.args(["-convert", "json", "-o", "-"])
.arg(&plist_path)
.output();
let Ok(conversion) = conversion else {
return SAFARI_BUNDLE_ID.to_owned();
};
let handler_table = serde_json::from_slice::<serde_json::Value>(&conversion.stdout);
let Ok(handler_table) = handler_table else {
return SAFARI_BUNDLE_ID.to_owned();
};
let handlers = handler_table["LSHandlers"].as_array();
let https_handler = handlers
.into_iter()
.flatten()
.find(|handler| handler["LSHandlerURLScheme"] == "https");
let bundle_id = https_handler.and_then(|handler| {
let role_all = handler["LSHandlerRoleAll"].as_str();
role_all.or_else(|| handler["LSHandlerRoleViewer"].as_str())
});
bundle_id.unwrap_or(SAFARI_BUNDLE_ID).to_owned()
}
}