mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-28 05:40:25 +02:00
558 lines
19 KiB
Rust
558 lines
19 KiB
Rust
use tauri::command;
|
|
|
|
pub struct DefaultBrowser {}
|
|
|
|
impl DefaultBrowser {
|
|
fn new() -> Self {
|
|
Self {}
|
|
}
|
|
|
|
pub fn instance() -> &'static DefaultBrowser {
|
|
&DEFAULT_BROWSER
|
|
}
|
|
|
|
pub async fn is_default_browser(&self) -> Result<bool, String> {
|
|
#[cfg(target_os = "macos")]
|
|
return macos::is_default_browser();
|
|
|
|
#[cfg(target_os = "windows")]
|
|
return windows::is_default_browser();
|
|
|
|
// Linux answers this by running `xdg-mime`, a shell script that forks
|
|
// further. That is blocking work with no upper bound, and this command
|
|
// runs on the same async runtime as every other command, the REST API and
|
|
// the sync scheduler — so doing it inline occupies a worker for as long as
|
|
// the desktop takes to answer. The Settings page polls this on a timer.
|
|
#[cfg(target_os = "linux")]
|
|
return blocking(linux::is_default_browser).await;
|
|
|
|
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
|
Err("Unsupported platform".to_string())
|
|
}
|
|
|
|
pub async fn set_as_default_browser(&self) -> Result<(), String> {
|
|
#[cfg(target_os = "macos")]
|
|
return macos::set_as_default_browser();
|
|
|
|
#[cfg(target_os = "windows")]
|
|
return windows::set_as_default_browser();
|
|
|
|
// Same reasoning, and this one additionally sleeps 500ms before verifying.
|
|
#[cfg(target_os = "linux")]
|
|
return blocking(linux::set_as_default_browser).await;
|
|
|
|
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
|
Err("Unsupported platform".to_string())
|
|
}
|
|
}
|
|
|
|
/// Run blocking work off the async runtime's worker threads.
|
|
#[cfg(target_os = "linux")]
|
|
async fn blocking<T, F>(work: F) -> Result<T, String>
|
|
where
|
|
F: FnOnce() -> Result<T, String> + Send + 'static,
|
|
T: Send + 'static,
|
|
{
|
|
tokio::task::spawn_blocking(work)
|
|
.await
|
|
.map_err(|e| format!("Default browser check did not run: {e}"))?
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
mod macos {
|
|
use core_foundation::base::OSStatus;
|
|
use core_foundation::string::CFStringRef;
|
|
use core_foundation::{base::TCFType, string::CFString};
|
|
|
|
#[link(name = "CoreServices", kind = "framework")]
|
|
extern "C" {
|
|
fn LSSetDefaultHandlerForURLScheme(scheme: CFStringRef, bundle_id: CFStringRef) -> OSStatus;
|
|
fn LSCopyDefaultHandlerForURLScheme(scheme: CFStringRef) -> CFStringRef;
|
|
}
|
|
|
|
pub fn is_default_browser() -> Result<bool, String> {
|
|
let schemes = ["http", "https"];
|
|
let bundle_id = "com.donutbrowser";
|
|
|
|
for scheme in schemes {
|
|
let scheme_str = CFString::new(scheme);
|
|
unsafe {
|
|
let current_handler = LSCopyDefaultHandlerForURLScheme(scheme_str.as_concrete_TypeRef());
|
|
if current_handler.is_null() {
|
|
return Ok(false);
|
|
}
|
|
|
|
let current_handler_cf = CFString::wrap_under_create_rule(current_handler);
|
|
let current_handler_str = current_handler_cf.to_string();
|
|
|
|
if current_handler_str != bundle_id {
|
|
return Ok(false);
|
|
}
|
|
}
|
|
}
|
|
Ok(true)
|
|
}
|
|
|
|
pub fn set_as_default_browser() -> Result<(), String> {
|
|
let bundle_id = CFString::new("com.donutbrowser");
|
|
let schemes = ["http", "https"];
|
|
|
|
for scheme in schemes {
|
|
let scheme_str = CFString::new(scheme);
|
|
unsafe {
|
|
let status = LSSetDefaultHandlerForURLScheme(
|
|
scheme_str.as_concrete_TypeRef(),
|
|
bundle_id.as_concrete_TypeRef(),
|
|
);
|
|
if status != 0 {
|
|
let error_msg = match status {
|
|
-54 => format!(
|
|
"Failed to set as default browser for scheme '{scheme}'. The app is not properly registered as a browser. Please:\n1. Build and install the app properly\n2. Manually set Donut Browser as default in System Settings > General > Default web browser\n3. Make sure the app is in your Applications folder"
|
|
),
|
|
_ => format!(
|
|
"Failed to set as default browser for scheme '{scheme}'. Status code: {status}. Please manually set Donut Browser as default in System Settings > General > Default web browser."
|
|
)
|
|
};
|
|
return Err(error_msg);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
#[allow(clippy::needless_borrows_for_generic_args)]
|
|
mod windows {
|
|
use std::path::Path;
|
|
use winreg::enums::*;
|
|
use winreg::RegKey;
|
|
|
|
const APP_NAME: &str = "DonutBrowser";
|
|
const PROG_ID: &str = "DonutBrowser.HTML";
|
|
|
|
pub fn is_default_browser() -> Result<bool, String> {
|
|
let schemes = ["http", "https"];
|
|
|
|
for scheme in schemes {
|
|
// Check if our browser is set as the default handler for this scheme
|
|
if !is_default_for_scheme(scheme)? {
|
|
return Ok(false);
|
|
}
|
|
}
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
pub fn set_as_default_browser() -> Result<(), String> {
|
|
// Get the current executable path
|
|
let exe_path = std::env::current_exe()
|
|
.map_err(|e| format!("Failed to get current executable path: {}", e))?;
|
|
|
|
let exe_path_str = exe_path
|
|
.to_str()
|
|
.ok_or("Failed to convert executable path to string")?;
|
|
|
|
// Verify the executable exists
|
|
if !Path::new(exe_path_str).exists() {
|
|
return Err(format!("Executable not found at: {}", exe_path_str));
|
|
}
|
|
|
|
// Register the application
|
|
register_application(exe_path_str)?;
|
|
|
|
// Set as default for HTTP and HTTPS
|
|
set_default_for_scheme("http")?;
|
|
set_default_for_scheme("https")?;
|
|
|
|
// Register file associations for HTML files
|
|
register_html_file_association(exe_path_str)?;
|
|
|
|
// Notify the system of changes
|
|
notify_system_of_changes();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn is_default_for_scheme(scheme: &str) -> Result<bool, String> {
|
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
|
|
|
// Check Software\Microsoft\Windows\Shell\Associations\UrlAssociations\{scheme}\UserChoice
|
|
let path = format!(
|
|
"Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\{}\\UserChoice",
|
|
scheme
|
|
);
|
|
|
|
match hkcu.open_subkey(&path) {
|
|
Ok(key) => match key.get_value::<String, _>("ProgId") {
|
|
Ok(prog_id) => Ok(prog_id == PROG_ID),
|
|
Err(_) => Ok(false),
|
|
},
|
|
Err(_) => Ok(false),
|
|
}
|
|
}
|
|
|
|
fn register_application(exe_path: &str) -> Result<(), String> {
|
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
|
|
|
// Register in Software\RegisteredApplications
|
|
let (registered_apps, _) = hkcu
|
|
.create_subkey("Software\\RegisteredApplications")
|
|
.map_err(|e| format!("Failed to create RegisteredApplications key: {}", e))?;
|
|
|
|
registered_apps
|
|
.set_value(APP_NAME, &format!("Software\\{}", APP_NAME))
|
|
.map_err(|e| format!("Failed to set registered application: {}", e))?;
|
|
|
|
// Create application key
|
|
let (app_key, _) = hkcu
|
|
.create_subkey(&format!("Software\\{}", APP_NAME))
|
|
.map_err(|e| format!("Failed to create application key: {}", e))?;
|
|
|
|
// Set application properties
|
|
app_key
|
|
.set_value("ApplicationName", &APP_NAME)
|
|
.map_err(|e| format!("Failed to set ApplicationName: {}", e))?;
|
|
|
|
app_key
|
|
.set_value(
|
|
"ApplicationDescription",
|
|
&"Donut Browser - Simple Yet Powerful Anti-Detect Browser",
|
|
)
|
|
.map_err(|e| format!("Failed to set ApplicationDescription: {}", e))?;
|
|
|
|
app_key
|
|
.set_value("ApplicationIcon", &format!("\"{}\",0", exe_path))
|
|
.map_err(|e| format!("Failed to set ApplicationIcon: {}", e))?;
|
|
|
|
// Create Capabilities key
|
|
let (capabilities, _) = app_key
|
|
.create_subkey("Capabilities")
|
|
.map_err(|e| format!("Failed to create Capabilities key: {}", e))?;
|
|
|
|
capabilities
|
|
.set_value(
|
|
"ApplicationDescription",
|
|
&"Donut Browser - Simple Yet Powerful Anti-Detect Browser",
|
|
)
|
|
.map_err(|e| format!("Failed to set Capabilities description: {}", e))?;
|
|
|
|
// Set URL associations
|
|
let (url_assoc, _) = capabilities
|
|
.create_subkey("URLAssociations")
|
|
.map_err(|e| format!("Failed to create URLAssociations key: {}", e))?;
|
|
|
|
url_assoc
|
|
.set_value("http", &PROG_ID)
|
|
.map_err(|e| format!("Failed to set http association: {}", e))?;
|
|
|
|
url_assoc
|
|
.set_value("https", &PROG_ID)
|
|
.map_err(|e| format!("Failed to set https association: {}", e))?;
|
|
|
|
// Set file associations
|
|
let (file_assoc, _) = capabilities
|
|
.create_subkey("FileAssociations")
|
|
.map_err(|e| format!("Failed to create FileAssociations key: {}", e))?;
|
|
|
|
file_assoc
|
|
.set_value(".html", &PROG_ID)
|
|
.map_err(|e| format!("Failed to set .html association: {}", e))?;
|
|
|
|
file_assoc
|
|
.set_value(".htm", &PROG_ID)
|
|
.map_err(|e| format!("Failed to set .htm association: {}", e))?;
|
|
|
|
// Register the ProgID
|
|
register_prog_id(exe_path)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn register_prog_id(exe_path: &str) -> Result<(), String> {
|
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
|
|
|
// Create ProgID key
|
|
let (prog_id_key, _) = hkcu
|
|
.create_subkey(&format!("Software\\Classes\\{}", PROG_ID))
|
|
.map_err(|e| format!("Failed to create ProgID key: {}", e))?;
|
|
|
|
prog_id_key
|
|
.set_value("", &"Donut Browser Document")
|
|
.map_err(|e| format!("Failed to set ProgID default value: {}", e))?;
|
|
|
|
prog_id_key
|
|
.set_value("FriendlyTypeName", &"Donut Browser Document")
|
|
.map_err(|e| format!("Failed to set FriendlyTypeName: {}", e))?;
|
|
|
|
// Create DefaultIcon key
|
|
let (icon_key, _) = prog_id_key
|
|
.create_subkey("DefaultIcon")
|
|
.map_err(|e| format!("Failed to create DefaultIcon key: {}", e))?;
|
|
|
|
icon_key
|
|
.set_value("", &format!("\"{}\",0", exe_path))
|
|
.map_err(|e| format!("Failed to set default icon: {}", e))?;
|
|
|
|
// Create shell\open\command key
|
|
let (command_key, _) = prog_id_key
|
|
.create_subkey("shell\\open\\command")
|
|
.map_err(|e| format!("Failed to create command key: {}", e))?;
|
|
|
|
command_key
|
|
.set_value("", &format!("\"{}\" \"%1\"", exe_path))
|
|
.map_err(|e| format!("Failed to set command: {}", e))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn set_default_for_scheme(scheme: &str) -> Result<(), String> {
|
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
|
|
|
// Set in Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.html\UserChoice
|
|
// Note: On Windows 10+, this might require elevated permissions or user interaction
|
|
// through the Settings app due to security restrictions
|
|
|
|
// Try to set the association in the user's choice
|
|
let user_choice_path = format!(
|
|
"Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\{}\\UserChoice",
|
|
scheme
|
|
);
|
|
|
|
// Note: Setting UserChoice directly may not work on Windows 10+ due to hash verification
|
|
// The user may need to manually set the default browser through Windows Settings
|
|
match hkcu.create_subkey(&user_choice_path) {
|
|
Ok((user_choice, _)) => {
|
|
// Attempt to set the ProgId
|
|
if user_choice.set_value("ProgId", &PROG_ID).is_err() {
|
|
// If we can't set UserChoice, that's expected on newer Windows versions
|
|
// The registration is still valuable for the "Open with" menu
|
|
}
|
|
}
|
|
Err(_) => {
|
|
// Expected on newer Windows versions - user must set manually
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn register_html_file_association(_exe_path: &str) -> Result<(), String> {
|
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
|
|
|
// Register .html and .htm file associations
|
|
for ext in &[".html", ".htm"] {
|
|
let ext_path = format!("Software\\Classes\\{}", ext);
|
|
|
|
match hkcu.create_subkey(&ext_path) {
|
|
Ok((ext_key, _)) => {
|
|
// Set the default value to our ProgID
|
|
let _ = ext_key.set_value("", &PROG_ID);
|
|
}
|
|
Err(_) => {
|
|
// Continue if we can't set the file association
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tell the shell that the association it has cached is stale.
|
|
///
|
|
/// `SHChangeNotify` is the documented announcement for an association change,
|
|
/// and the `WM_SETTINGCHANGE` broadcast is what the shell's own settings UI
|
|
/// sends alongside it, so both go out.
|
|
///
|
|
/// This used to hand-declare `SendMessageTimeoutA` with `lpdwResult` typed as
|
|
/// `*mut u32` and pass it a `u32`. The real parameter is `PDWORD_PTR`, eight
|
|
/// bytes on x64, so every call wrote four bytes past a stack slot. The result
|
|
/// was a corrupted stack at the exact moment a user set Donut as their default
|
|
/// browser, and the process died with nothing in the log. Go through the
|
|
/// `windows` crate instead, which types the out-parameter correctly and cannot
|
|
/// drift from the real ABI.
|
|
fn notify_system_of_changes() {
|
|
use windows::core::w;
|
|
use windows::Win32::Foundation::{LPARAM, WPARAM};
|
|
use windows::Win32::UI::Shell::{SHChangeNotify, SHCNE_ASSOCCHANGED, SHCNF_IDLIST};
|
|
use windows::Win32::UI::WindowsAndMessaging::{
|
|
SendMessageTimeoutW, HWND_BROADCAST, SMTO_ABORTIFHUNG, WM_SETTINGCHANGE,
|
|
};
|
|
|
|
unsafe {
|
|
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, None, None);
|
|
|
|
// The broadcast is best-effort: a hung top-level window elsewhere on the
|
|
// desktop must not hold up the click that triggered this, hence the
|
|
// timeout and SMTO_ABORTIFHUNG. `WM_SETTINGCHANGE`'s lParam string is
|
|
// marshalled cross-process by the window manager, and this one is
|
|
// 'static, so it stays valid for the whole call.
|
|
let mut result: usize = 0;
|
|
SendMessageTimeoutW(
|
|
HWND_BROADCAST,
|
|
WM_SETTINGCHANGE,
|
|
WPARAM(0),
|
|
LPARAM(w!("Software\\Classes").as_ptr() as isize),
|
|
SMTO_ABORTIFHUNG,
|
|
1000,
|
|
Some(&mut result),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
mod linux {
|
|
use std::process::Command;
|
|
|
|
const APP_DESKTOP_NAME: &str = "donutbrowser.desktop";
|
|
|
|
pub fn is_default_browser() -> Result<bool, String> {
|
|
// Check if xdg-mime is available
|
|
if !is_xdg_mime_available() {
|
|
return Err("xdg-mime utility not found. Please install xdg-utils package.".to_string());
|
|
}
|
|
|
|
let schemes = ["http", "https"];
|
|
|
|
for scheme in schemes {
|
|
let mime_type = format!("x-scheme-handler/{}", scheme);
|
|
|
|
// Query the current default handler for this scheme
|
|
let output = Command::new("xdg-mime")
|
|
.args(["query", "default", &mime_type])
|
|
.output()
|
|
.map_err(|e| format!("Failed to query default handler for {}: {}", scheme, e))?;
|
|
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
return Err(format!("xdg-mime query failed for {}: {}", scheme, stderr));
|
|
}
|
|
|
|
let current_handler = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
|
|
// Check if our app is the default handler
|
|
if current_handler != APP_DESKTOP_NAME {
|
|
return Ok(false);
|
|
}
|
|
}
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
pub fn set_as_default_browser() -> Result<(), String> {
|
|
// Check if xdg-mime is available
|
|
if !is_xdg_mime_available() {
|
|
return Err("xdg-mime utility not found. Please install xdg-utils package.".to_string());
|
|
}
|
|
|
|
// Check if the desktop file exists in common locations
|
|
if !check_desktop_file_exists() {
|
|
return Err(format!(
|
|
"Desktop file '{}' not found in standard locations. Please ensure the application is properly installed. You can manually set Donut Browser as the default browser in your system settings.",
|
|
APP_DESKTOP_NAME
|
|
));
|
|
}
|
|
|
|
let schemes = ["http", "https"];
|
|
let mut all_succeeded = true;
|
|
let mut error_messages = Vec::new();
|
|
|
|
for scheme in schemes {
|
|
let mime_type = format!("x-scheme-handler/{}", scheme);
|
|
|
|
// Set our app as the default handler for this scheme
|
|
let output = Command::new("xdg-mime")
|
|
.args(["default", APP_DESKTOP_NAME, &mime_type])
|
|
.output()
|
|
.map_err(|e| format!("Failed to set default handler for {}: {}", scheme, e))?;
|
|
|
|
if !output.status.success() {
|
|
all_succeeded = false;
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
error_messages.push(format!("Failed to set default for {}: {}", scheme, stderr));
|
|
}
|
|
}
|
|
|
|
if !all_succeeded {
|
|
return Err(format!(
|
|
"Some xdg-mime commands failed:\n{}\n\nYou may need to:\n1. Run with appropriate permissions\n2. Manually set the default browser in your desktop environment settings\n3. Restart your desktop session",
|
|
error_messages.join("\n")
|
|
));
|
|
}
|
|
|
|
// Give the system a moment to process the changes
|
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
|
|
|
// Verify the changes took effect
|
|
match is_default_browser() {
|
|
Ok(true) => Ok(()),
|
|
Ok(false) => {
|
|
// This is the common case where commands succeed but verification fails
|
|
Err(format!(
|
|
"The xdg-mime commands completed successfully, but Donut Browser is not yet set as the default. This is common on some Linux distributions. Please try one of these options:\n\n1. Restart your desktop session and try again\n2. Log out and log back in\n3. Manually set Donut Browser as the default in your system settings:\n - GNOME: Settings > Default Applications > Web\n - KDE: System Settings > Applications > Default Applications > Web Browser\n - XFCE: Settings > Preferred Applications > Web Browser\n - Or run: xdg-settings set default-web-browser {}\n\nThe changes may take effect automatically after a desktop restart.",
|
|
APP_DESKTOP_NAME
|
|
))
|
|
}
|
|
Err(e) => Err(format!(
|
|
"Set as default completed, but verification failed: {}. The changes may still be in effect after restarting your desktop session.",
|
|
e
|
|
))
|
|
}
|
|
}
|
|
|
|
fn is_xdg_mime_available() -> bool {
|
|
Command::new("which")
|
|
.arg("xdg-mime")
|
|
.output()
|
|
.map(|output| output.status.success())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn check_desktop_file_exists() -> bool {
|
|
let desktop_locations = [
|
|
"~/.local/share/applications/",
|
|
"/usr/share/applications/",
|
|
"/usr/local/share/applications/",
|
|
"/var/lib/flatpak/exports/share/applications/",
|
|
"~/.local/share/flatpak/exports/share/applications/",
|
|
];
|
|
|
|
for location in &desktop_locations {
|
|
let path = if location.starts_with('~') {
|
|
if let Ok(home) = std::env::var("HOME") {
|
|
location.replace('~', &home)
|
|
} else {
|
|
continue;
|
|
}
|
|
} else {
|
|
location.to_string()
|
|
};
|
|
|
|
let full_path = format!("{}{}", path, APP_DESKTOP_NAME);
|
|
if std::path::Path::new(&full_path).exists() {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
}
|
|
|
|
// Global singleton instance
|
|
lazy_static::lazy_static! {
|
|
static ref DEFAULT_BROWSER: DefaultBrowser = DefaultBrowser::new();
|
|
}
|
|
|
|
#[command]
|
|
pub async fn is_default_browser() -> Result<bool, String> {
|
|
let default_browser = DefaultBrowser::instance();
|
|
default_browser.is_default_browser().await
|
|
}
|
|
|
|
#[command]
|
|
pub async fn set_as_default_browser() -> Result<(), String> {
|
|
let default_browser = DefaultBrowser::instance();
|
|
default_browser.set_as_default_browser().await
|
|
}
|