Use proper Windows APIs for checking/setting default app (#5000)

# Description of Changes
Use proper Windows APIs for checking/setting default app

---------

Co-authored-by: Connor Yoh <con.yoh13@gmail.com>
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
This commit is contained in:
James Brunton
2025-11-25 21:31:02 +00:00
committed by GitHub
parent 8016d271aa
commit d8a99fcb07
7 changed files with 160 additions and 57 deletions

View File

@@ -1,8 +1,5 @@
use crate::utils::add_log;
#[cfg(any(target_os = "windows", target_os = "linux"))]
use std::process::Command;
/// Check if Stirling PDF is the default PDF handler
#[tauri::command]
pub fn is_default_pdf_handler() -> Result<bool, String> {
@@ -51,50 +48,80 @@ pub fn set_as_default_pdf_handler() -> Result<String, String> {
#[cfg(target_os = "windows")]
fn check_default_windows() -> Result<bool, String> {
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
use windows::core::HSTRING;
use windows::Win32::Foundation::RPC_E_CHANGED_MODE;
use windows::Win32::System::Com::{
CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_INPROC_SERVER,
COINIT_APARTMENTTHREADED,
};
use windows::Win32::UI::Shell::{
IApplicationAssociationRegistration, ApplicationAssociationRegistration,
ASSOCIATIONTYPE, ASSOCIATIONLEVEL,
};
// Query the default handler for .pdf extension
let output = Command::new("cmd")
.args(["/C", "assoc .pdf"])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|e| format!("Failed to check default app: {}", e))?;
unsafe {
// Initialize COM for this thread
let hr = CoInitializeEx(None, COINIT_APARTMENTTHREADED);
// RPC_E_CHANGED_MODE means COM is already initialized, which is fine
if hr.is_err() && hr != RPC_E_CHANGED_MODE {
return Err(format!("Failed to initialize COM: {:?}", hr));
}
let assoc = String::from_utf8_lossy(&output.stdout);
add_log(format!("Windows PDF association: {}", assoc.trim()));
let result = (|| -> Result<bool, String> {
// Create the IApplicationAssociationRegistration instance
let reg: IApplicationAssociationRegistration =
CoCreateInstance(&ApplicationAssociationRegistration, None, CLSCTX_INPROC_SERVER)
.map_err(|e| format!("Failed to create COM instance: {}", e))?;
// Get the ProgID for .pdf files
if let Some(prog_id) = assoc.trim().strip_prefix(".pdf=") {
// Query what application handles this ProgID
let output = Command::new("cmd")
.args(["/C", &format!("ftype {}", prog_id)])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|e| format!("Failed to query file type: {}", e))?;
// Query the current default handler for .pdf extension
let extension = HSTRING::from(".pdf");
let ftype = String::from_utf8_lossy(&output.stdout);
add_log(format!("Windows file type: {}", ftype.trim()));
let default_app = reg.QueryCurrentDefault(
&extension,
ASSOCIATIONTYPE(0), // AT_FILEEXTENSION
ASSOCIATIONLEVEL(1), // AL_EFFECTIVE - gets the effective default (user or machine level)
)
.map_err(|e| format!("Failed to query current default: {}", e))?;
// Check if it contains "Stirling" or our app name
let is_default = ftype.to_lowercase().contains("stirling");
Ok(is_default)
} else {
Ok(false)
// Convert PWSTR to String
let default_str = default_app.to_string()
.map_err(|e| format!("Failed to convert default app string: {}", e))?;
add_log(format!("Windows PDF handler ProgID: {}", default_str));
// Check if it contains "Stirling" (case-insensitive)
// Note: This checks the ProgID registered by the installer
let is_default = default_str.to_lowercase().contains("stirling");
Ok(is_default)
})();
// Clean up COM
CoUninitialize();
result
}
}
#[cfg(target_os = "windows")]
fn set_default_windows() -> Result<String, String> {
// On Windows 10+, we need to open the Default Apps settings
// as programmatic setting requires a signed installer
Command::new("cmd")
.args(["/C", "start", "ms-settings:defaultapps"])
.spawn()
.map_err(|e| format!("Failed to open default apps settings: {}", e))?;
use std::process::Command;
add_log("Opened Windows Default Apps settings".to_string());
Ok("opened_settings".to_string())
// Windows 10+ approach: Open Settings app directly to default apps
// This is more reliable than COM APIs which require pre-registration
// ms-settings:defaultapps opens the default apps settings page
let result = Command::new("cmd")
.args(["/C", "start", "ms-settings:defaultapps"])
.output()
.map_err(|e| format!("Failed to open Windows Settings: {}", e))?;
if result.status.success() {
add_log("Opened Windows default apps settings".to_string());
Ok("opened_dialog".to_string())
} else {
let error = String::from_utf8_lossy(&result.stderr);
add_log(format!("Failed to open settings: {}", error));
Err(format!("Failed to open default apps settings: {}", error))
}
}
// ============================================================================
@@ -184,6 +211,8 @@ fn set_default_macos() -> Result<String, String> {
#[cfg(target_os = "linux")]
fn check_default_linux() -> Result<bool, String> {
use std::process::Command;
// Use xdg-mime to check the default application for PDF files
let output = Command::new("xdg-mime")
.args(["query", "default", "application/pdf"])
@@ -200,6 +229,8 @@ fn check_default_linux() -> Result<bool, String> {
#[cfg(target_os = "linux")]
fn set_default_linux() -> Result<String, String> {
use std::process::Command;
// Use xdg-mime to set the default application for PDF files
let result = Command::new("xdg-mime")
.args(["default", "stirling-pdf.desktop", "application/pdf"])