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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 160 additions and 57 deletions

View File

@ -63,7 +63,7 @@
}, },
"settingsOpened": { "settingsOpened": {
"title": "Settings Opened", "title": "Settings Opened",
"message": "Please select Stirling PDF in your system settings" "message": "In Windows Settings, search for 'PDF' and select Stirling PDF as your default app"
}, },
"error": { "error": {
"title": "Error", "title": "Error",

View File

@ -4250,6 +4250,7 @@ dependencies = [
"tokio", "tokio",
"url", "url",
"urlencoding", "urlencoding",
"windows 0.58.0",
] ]
[[package]] [[package]]
@ -4437,7 +4438,7 @@ dependencies = [
"tao-macros", "tao-macros",
"unicode-segmentation", "unicode-segmentation",
"url", "url",
"windows", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-version", "windows-version",
"x11-dl", "x11-dl",
@ -4514,7 +4515,7 @@ dependencies = [
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
"window-vibrancy", "window-vibrancy",
"windows", "windows 0.61.3",
] ]
[[package]] [[package]]
@ -4683,7 +4684,7 @@ dependencies = [
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.17", "thiserror 2.0.17",
"url", "url",
"windows", "windows 0.61.3",
"zbus", "zbus",
] ]
@ -4761,7 +4762,7 @@ dependencies = [
"url", "url",
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
"windows", "windows 0.61.3",
] ]
[[package]] [[package]]
@ -4787,7 +4788,7 @@ dependencies = [
"url", "url",
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
"windows", "windows 0.61.3",
"wry", "wry",
] ]
@ -5624,10 +5625,10 @@ checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4"
dependencies = [ dependencies = [
"webview2-com-macros", "webview2-com-macros",
"webview2-com-sys", "webview2-com-sys",
"windows", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-implement", "windows-implement 0.60.2",
"windows-interface", "windows-interface 0.59.3",
] ]
[[package]] [[package]]
@ -5648,7 +5649,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
dependencies = [ dependencies = [
"thiserror 2.0.17", "thiserror 2.0.17",
"windows", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
] ]
@ -5698,6 +5699,16 @@ dependencies = [
"windows-version", "windows-version",
] ]
[[package]]
name = "windows"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core 0.58.0",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows" name = "windows"
version = "0.61.3" version = "0.61.3"
@ -5720,14 +5731,27 @@ dependencies = [
"windows-core 0.61.2", "windows-core 0.61.2",
] ]
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement 0.58.0",
"windows-interface 0.58.0",
"windows-result 0.2.0",
"windows-strings 0.1.0",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.61.2" version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [ dependencies = [
"windows-implement", "windows-implement 0.60.2",
"windows-interface", "windows-interface 0.59.3",
"windows-link 0.1.3", "windows-link 0.1.3",
"windows-result 0.3.4", "windows-result 0.3.4",
"windows-strings 0.4.2", "windows-strings 0.4.2",
@ -5739,8 +5763,8 @@ version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [ dependencies = [
"windows-implement", "windows-implement 0.60.2",
"windows-interface", "windows-interface 0.59.3",
"windows-link 0.2.1", "windows-link 0.2.1",
"windows-result 0.4.1", "windows-result 0.4.1",
"windows-strings 0.5.1", "windows-strings 0.5.1",
@ -5757,6 +5781,17 @@ dependencies = [
"windows-threading", "windows-threading",
] ]
[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
]
[[package]] [[package]]
name = "windows-implement" name = "windows-implement"
version = "0.60.2" version = "0.60.2"
@ -5768,6 +5803,17 @@ dependencies = [
"syn 2.0.108", "syn 2.0.108",
] ]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
]
[[package]] [[package]]
name = "windows-interface" name = "windows-interface"
version = "0.59.3" version = "0.59.3"
@ -5812,6 +5858,15 @@ dependencies = [
"windows-strings 0.4.2", "windows-strings 0.4.2",
] ]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.3.4" version = "0.3.4"
@ -5830,6 +5885,16 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result 0.2.0",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-strings" name = "windows-strings"
version = "0.4.2" version = "0.4.2"
@ -6252,7 +6317,7 @@ dependencies = [
"webkit2gtk", "webkit2gtk",
"webkit2gtk-sys", "webkit2gtk-sys",
"webview2-com", "webview2-com",
"windows", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-version", "windows-version",
"x11-dl", "x11-dl",

View File

@ -45,3 +45,11 @@ rand = "0.8"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.10" core-foundation = "0.10"
core-services = "1.0" core-services = "1.0"
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.58", features = [
"Win32_Foundation",
"Win32_System_Com",
"Win32_UI_Shell",
"Win32_System_ApplicationInstallationAndServicing",
] }

View File

@ -1,8 +1,5 @@
use crate::utils::add_log; 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 /// Check if Stirling PDF is the default PDF handler
#[tauri::command] #[tauri::command]
pub fn is_default_pdf_handler() -> Result<bool, String> { 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")] #[cfg(target_os = "windows")]
fn check_default_windows() -> Result<bool, String> { fn check_default_windows() -> Result<bool, String> {
use std::os::windows::process::CommandExt; use windows::core::HSTRING;
const CREATE_NO_WINDOW: u32 = 0x08000000; 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 unsafe {
let output = Command::new("cmd") // Initialize COM for this thread
.args(["/C", "assoc .pdf"]) let hr = CoInitializeEx(None, COINIT_APARTMENTTHREADED);
.creation_flags(CREATE_NO_WINDOW) // RPC_E_CHANGED_MODE means COM is already initialized, which is fine
.output() if hr.is_err() && hr != RPC_E_CHANGED_MODE {
.map_err(|e| format!("Failed to check default app: {}", e))?; return Err(format!("Failed to initialize COM: {:?}", hr));
}
let assoc = String::from_utf8_lossy(&output.stdout); let result = (|| -> Result<bool, String> {
add_log(format!("Windows PDF association: {}", assoc.trim())); // 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 // Query the current default handler for .pdf extension
if let Some(prog_id) = assoc.trim().strip_prefix(".pdf=") { let extension = HSTRING::from(".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))?;
let ftype = String::from_utf8_lossy(&output.stdout); let default_app = reg.QueryCurrentDefault(
add_log(format!("Windows file type: {}", ftype.trim())); &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 // Convert PWSTR to String
let is_default = ftype.to_lowercase().contains("stirling"); let default_str = default_app.to_string()
Ok(is_default) .map_err(|e| format!("Failed to convert default app string: {}", e))?;
} else {
Ok(false) 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")] #[cfg(target_os = "windows")]
fn set_default_windows() -> Result<String, String> { fn set_default_windows() -> Result<String, String> {
// On Windows 10+, we need to open the Default Apps settings use std::process::Command;
// 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))?;
add_log("Opened Windows Default Apps settings".to_string()); // Windows 10+ approach: Open Settings app directly to default apps
Ok("opened_settings".to_string()) // 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")] #[cfg(target_os = "linux")]
fn check_default_linux() -> Result<bool, String> { fn check_default_linux() -> Result<bool, String> {
use std::process::Command;
// Use xdg-mime to check the default application for PDF files // Use xdg-mime to check the default application for PDF files
let output = Command::new("xdg-mime") let output = Command::new("xdg-mime")
.args(["query", "default", "application/pdf"]) .args(["query", "default", "application/pdf"])
@ -200,6 +229,8 @@ fn check_default_linux() -> Result<bool, String> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn set_default_linux() -> Result<String, String> { fn set_default_linux() -> Result<String, String> {
use std::process::Command;
// Use xdg-mime to set the default application for PDF files // Use xdg-mime to set the default application for PDF files
let result = Command::new("xdg-mime") let result = Command::new("xdg-mime")
.args(["default", "stirling-pdf.desktop", "application/pdf"]) .args(["default", "stirling-pdf.desktop", "application/pdf"])

View File

@ -33,11 +33,11 @@ export const useDefaultApp = () => {
body: t('defaultApp.success.message', 'Stirling PDF is now your default PDF editor'), body: t('defaultApp.success.message', 'Stirling PDF is now your default PDF editor'),
}); });
setIsDefault(true); setIsDefault(true);
} else if (result === 'opened_settings') { } else if (result === 'opened_dialog') {
alert({ alert({
alertType: 'neutral', alertType: 'neutral',
title: t('defaultApp.settingsOpened.title', 'Settings Opened'), title: t('defaultApp.settingsOpened.title', 'Settings Opened'),
body: t('defaultApp.settingsOpened.message', 'Please select Stirling PDF in your system settings'), body: t('defaultApp.settingsOpened.message', 'Please select Stirling PDF in the file association dialogue'),
}); });
} }
} catch (error) { } catch (error) {

View File

@ -88,8 +88,7 @@ export function useEndpointEnabled(endpoint: string): {
try { try {
setError(null); setError(null);
const response = await apiClient.get<boolean>('/api/v1/config/endpoint-enabled', { const response = await apiClient.get<boolean>(`/api/v1/config/endpoint-enabled?endpoint=${encodeURIComponent(endpoint)}`, {
params: { endpoint },
suppressErrorToast: true, suppressErrorToast: true,
}); });

View File

@ -22,10 +22,10 @@ export const defaultAppService = {
* Set or prompt to set Stirling PDF as default PDF handler * Set or prompt to set Stirling PDF as default PDF handler
* Returns a status string indicating what happened * Returns a status string indicating what happened
*/ */
async setAsDefaultPdfHandler(): Promise<'set_successfully' | 'opened_settings' | 'error'> { async setAsDefaultPdfHandler(): Promise<'set_successfully' | 'opened_dialog' | 'error'> {
try { try {
const result = await invoke<string>('set_as_default_pdf_handler'); const result = await invoke<string>('set_as_default_pdf_handler');
return result as 'set_successfully' | 'opened_settings'; return result as 'set_successfully' | 'opened_dialog';
} catch (error) { } catch (error) {
console.error('[DefaultApp] Failed to set default handler:', error); console.error('[DefaultApp] Failed to set default handler:', error);
return 'error'; return 'error';