use crate::{Browser, BrowserOptions, Error, ErrorKind, Result, TargetType};
use objc2_app_kit::{NSWorkspace, NSWorkspaceLaunchOptions};
use objc2_foundation::{NSArray, NSDictionary, NSError, NSString, NSURL};
#[allow(deprecated)]
pub(super) fn open_browser_internal(
browser: Browser,
target: &TargetType,
options: &BrowserOptions,
) -> Result<()> {
let workspace = NSWorkspace::sharedWorkspace();
let app_url = match browser {
Browser::Default => resolve_default_browser(&workspace),
_ => {
let bundle_id = bundle_id_for_browser(browser)
.ok_or_else(|| Error::new(ErrorKind::NotFound, "browser not supported on macos"))?;
let bundle_id = NSString::from_str(bundle_id);
workspace.URLForApplicationWithBundleIdentifier(&bundle_id)
}
}
.ok_or_else(|| Error::new(ErrorKind::NotFound, "browser not found"))?;
if options.dry_run {
log::debug!("dry-run: not actually opening the browser {browser}");
return Ok(());
}
let target_string = NSString::from_str(target.as_ref());
let target_url = NSURL::URLWithString(&target_string)
.ok_or_else(|| Error::other("failed to create target NSURL"))?;
let urls = NSArray::from_retained_slice(&[target_url]);
let mut launch_options = NSWorkspaceLaunchOptions::Default;
if options.dont_switch {
launch_options |= NSWorkspaceLaunchOptions::WithoutActivation;
}
let config = NSDictionary::new();
log::trace!("about to start browser: {browser} for {target} using app at {app_url:?}");
let result = unsafe {
workspace.openURLs_withApplicationAtURL_options_configuration_error(
&urls,
&app_url,
launch_options,
&config,
)
};
match result {
Ok(running_app) => {
log::trace!(
"launched {browser}: bundle_id={:?}, url={:?}, finished_launching={}",
running_app.bundleIdentifier(),
running_app.bundleURL(),
running_app.isFinishedLaunching(),
);
Ok(())
}
Err(err) => Err(map_launch_error(&err)),
}
}
fn map_launch_error(err: &NSError) -> Error {
let kind = match err.code() {
-43 | -10814 => ErrorKind::NotFound,
-10826 => ErrorKind::PermissionDenied,
_ => ErrorKind::Other,
};
Error::new(kind, err.localizedDescription().to_string())
}
fn resolve_default_browser(workspace: &NSWorkspace) -> Option<objc2::rc::Retained<NSURL>> {
let https = NSString::from_str("https://");
let resolved =
NSURL::URLWithString(&https).and_then(|url| workspace.URLForApplicationToOpenURL(&url));
match resolved {
Some(url) => {
log::trace!("default browser is {url:?}");
Some(url)
}
None => {
log::error!("failed to get default browser, falling back to Safari");
let safari = NSString::from_str(SAFARI_BUNDLE_ID);
workspace.URLForApplicationWithBundleIdentifier(&safari)
}
}
}
fn bundle_id_for_browser(browser: Browser) -> Option<&'static str> {
match browser {
Browser::Firefox => Some("org.mozilla.firefox"),
Browser::Chrome => Some("com.google.Chrome"),
Browser::Opera => Some("com.operasoftware.Opera"),
Browser::Safari => Some(SAFARI_BUNDLE_ID),
_ => None,
}
}
const SAFARI_BUNDLE_ID: &str = "com.apple.Safari";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_non_existing_browser() {
let _ = env_logger::try_init();
if let Err(err) = open_browser_internal(
Browser::Opera,
&TargetType::try_from("https://github.com").expect("failed to parse url"),
&BrowserOptions::default(),
) {
assert_eq!(err.kind(), ErrorKind::NotFound);
} else {
panic!("expected opening non-existing browser to fail");
}
}
#[test]
fn test_existence() {
let _ = env_logger::try_init();
assert!(Browser::Safari.exists());
assert!(!Browser::Opera.exists());
}
}