From e0ec0fbf4a9377ebee1b0099f9c9b5b419f3a180 Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Tue, 15 Jul 2025 18:29:42 +0100 Subject: [PATCH] Mac specific file handler --- frontend/src-tauri/Cargo.lock | 87 ++++++++++++++++- frontend/src-tauri/Cargo.toml | 6 ++ frontend/src-tauri/src/file_handler.rs | 116 +++++++++++++++++++++++ frontend/src-tauri/src/lib.rs | 44 ++------- frontend/src/hooks/useOpenedFile.ts | 13 ++- frontend/src/services/fileOpenService.ts | 78 +++++++++++++++ 6 files changed, 306 insertions(+), 38 deletions(-) create mode 100644 frontend/src-tauri/src/file_handler.rs diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 32a032e70..7c63b0d41 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -94,7 +94,10 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" name = "app" version = "0.1.0" dependencies = [ + "cocoa", "log", + "objc", + "once_cell", "reqwest 0.11.27", "serde", "serde_json", @@ -195,6 +198,12 @@ dependencies = [ "wyz", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -454,6 +463,36 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.22.3", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + [[package]] name = "combine" version = "4.6.7" @@ -506,6 +545,19 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.3.2", + "libc", +] + [[package]] name = "core-graphics" version = "0.24.0" @@ -514,11 +566,22 @@ checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.9.1", "core-foundation 0.10.1", - "core-graphics-types", + "core-graphics-types 0.2.0", "foreign-types 0.5.0", "libc", ] +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.2.0" @@ -1978,6 +2041,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "markup5ever" version = "0.11.0" @@ -2165,6 +2237,15 @@ dependencies = [ "libc", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -3509,7 +3590,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" dependencies = [ "bytemuck", "cfg_aliases", - "core-graphics", + "core-graphics 0.24.0", "foreign-types 0.5.0", "js-sys", "log", @@ -3687,7 +3768,7 @@ checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82" dependencies = [ "bitflags 2.9.1", "core-foundation 0.10.1", - "core-graphics", + "core-graphics 0.24.0", "crossbeam-channel", "dispatch", "dlopen2", diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 84ae5555c..b12c4b1fd 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -27,3 +27,9 @@ tauri-plugin-shell = "2.1.0" tauri-plugin-fs = "2.0.0" tokio = { version = "1.0", features = ["time"] } reqwest = { version = "0.11", features = ["json"] } + +# macOS-specific dependencies for native file opening +[target.'cfg(target_os = "macos")'.dependencies] +objc = "0.2" +cocoa = "0.24" +once_cell = "1.19" diff --git a/frontend/src-tauri/src/file_handler.rs b/frontend/src-tauri/src/file_handler.rs new file mode 100644 index 000000000..f9ad3c9e8 --- /dev/null +++ b/frontend/src-tauri/src/file_handler.rs @@ -0,0 +1,116 @@ +/// Multi-platform file opening handler +/// +/// This module provides unified file opening support across platforms: +/// - macOS: Uses native NSApplication delegate (proper Apple Events) +/// - Windows/Linux: Uses command line arguments (fallback approach) +/// - All platforms: Runtime event handling via Tauri events + +use crate::utils::add_log; +use crate::commands::set_opened_file; +use tauri::{AppHandle, Runtime}; + +#[cfg(target_os = "macos")] +mod macos_native; + +/// Initialize file handling for the current platform +pub fn initialize_file_handler(app: &AppHandle) { + add_log("🔧 Initializing file handler...".to_string()); + + // Platform-specific initialization + #[cfg(target_os = "macos")] + { + add_log("🍎 Using macOS native file handler".to_string()); + macos_native::register_open_file_handler(app); + } + + #[cfg(not(target_os = "macos"))] + { + add_log("đŸ–Ĩī¸ Using command line argument file handler".to_string()); + let _ = app; // Suppress unused variable warning + } + + // Universal: Check command line arguments (works on all platforms) + check_command_line_args(); +} + +/// Check command line arguments for file paths (universal fallback) +fn check_command_line_args() { + let args: Vec = std::env::args().collect(); + add_log(format!("🔍 DEBUG: All command line args: {:?}", args)); + + // Check command line arguments for file opening + for (i, arg) in args.iter().enumerate() { + add_log(format!("🔍 DEBUG: Arg {}: {}", i, arg)); + if i > 0 && arg.ends_with(".pdf") && std::path::Path::new(arg).exists() { + add_log(format!("📂 File argument detected: {}", arg)); + set_opened_file(arg.clone()); + break; // Only handle the first PDF file + } + } +} + +/// Handle runtime file open events (for future single-instance support) +pub fn handle_runtime_file_open(file_path: String) { + if file_path.ends_with(".pdf") && std::path::Path::new(&file_path).exists() { + add_log(format!("📂 Runtime file open: {}", file_path)); + set_opened_file(file_path); + } +} + +#[cfg(target_os = "macos")] +mod macos_native { + use objc::{class, msg_send, sel, sel_impl}; + use objc::runtime::{Class, Object, Sel}; + use cocoa::appkit::NSApplication; + use cocoa::base::{id, nil}; + use once_cell::sync::Lazy; + use std::sync::Mutex; + use tauri::{AppHandle, Runtime, Manager}; + + use crate::utils::add_log; + use crate::commands::set_opened_file; + + static APP_HANDLE: Lazy>> = Lazy::new(|| Mutex::new(None)); + + extern "C" fn open_file(_self: &Object, _cmd: Sel, _sender: id, filename: id) -> bool { + unsafe { + let cstr = { + let bytes: *const std::os::raw::c_char = msg_send![filename, UTF8String]; + std::ffi::CStr::from_ptr(bytes) + }; + if let Ok(path) = cstr.to_str() { + add_log(format!("📂 macOS native file open event: {}", path)); + if path.ends_with(".pdf") { + set_opened_file(path.to_string()); + + if let Some(app) = APP_HANDLE.lock().unwrap().as_ref() { + let _ = app.emit("macos://open-file", path.to_string()); + add_log(format!("✅ Emitted file open event to frontend: {}", path)); + } + } + } + } + true + } + + pub fn register_open_file_handler(app: &AppHandle) { + add_log("🔧 Registering macOS native file handler...".to_string()); + + *APP_HANDLE.lock().unwrap() = Some(app.clone()); + + unsafe { + let delegate_class = Class::get("AppDelegate").unwrap_or_else(|| { + let superclass = class!(NSObject); + let mut decl = objc::declare::ClassDecl::new("AppDelegate", superclass).unwrap(); + decl.add_method(sel!(application:openFile:), open_file as extern "C" fn(&Object, Sel, id, id) -> bool); + decl.register() + }); + + let delegate: id = msg_send![delegate_class, new]; + let ns_app = NSApplication::sharedApplication(nil); + ns_app.setDelegate_(delegate); + } + + add_log("✅ macOS native file handler registered successfully".to_string()); + } +} \ No newline at end of file diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 836fc560d..b702f4955 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -1,9 +1,10 @@ -use tauri::{RunEvent, WindowEvent, Manager}; +use tauri::{RunEvent, WindowEvent}; mod utils; mod commands; +mod file_handler; -use commands::{start_backend, check_backend_health, get_opened_file, clear_opened_file, cleanup_backend, set_opened_file}; +use commands::{start_backend, check_backend_health, get_opened_file, clear_opened_file, cleanup_backend}; use utils::{add_log, get_tauri_logs}; #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -11,24 +12,13 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_fs::init()) - .setup(|_| { + .setup(|app| { add_log("🚀 Tauri app setup started".to_string()); - // Log all command line arguments for debugging - let args: Vec = std::env::args().collect(); - add_log(format!("🔍 DEBUG: All command line args: {:?}", args)); + // Initialize platform-specific file handler + file_handler::initialize_file_handler(&app.handle()); - // Check command line arguments at startup for macOS file opening - for (i, arg) in args.iter().enumerate() { - add_log(format!("🔍 DEBUG: Arg {}: {}", i, arg)); - if i > 0 && arg.ends_with(".pdf") && std::path::Path::new(arg).exists() { - add_log(format!("📂 File argument detected at startup: {}", arg)); - set_opened_file(arg.clone()); - break; // Only handle the first PDF file - } - } - - add_log("🔍 DEBUG: Setup completed, checking for opened file...".to_string()); + add_log("🔍 DEBUG: Setup completed".to_string()); Ok(()) }) .invoke_handler(tauri::generate_handler![start_backend, check_backend_health, get_opened_file, clear_opened_file, get_tauri_logs]) @@ -47,24 +37,10 @@ pub fn run() { cleanup_backend(); // Allow the window to close } - // Handle file open events (macOS specific) - #[cfg(target_os = "macos")] - RunEvent::OpenUrl { url } => { - add_log(format!("🔍 DEBUG: OpenUrl event received: {}", url)); - // Handle URL-based file opening - if url.starts_with("file://") { - let file_path = url.strip_prefix("file://").unwrap_or(&url); - if file_path.ends_with(".pdf") { - add_log(format!("📂 File opened via URL event: {}", file_path)); - set_opened_file(file_path.to_string()); - - // Emit event to frontend - app_handle.emit_all("file-opened", file_path).unwrap(); - } - } - } _ => { - add_log(format!("🔍 DEBUG: Unhandled event: {:?}", event)); + // Only log unhandled events in debug mode to reduce noise + // #[cfg(debug_assertions)] + // add_log(format!("🔍 DEBUG: Unhandled event: {:?}", event)); } } }); diff --git a/frontend/src/hooks/useOpenedFile.ts b/frontend/src/hooks/useOpenedFile.ts index 0a2921d83..a1ac024e9 100644 --- a/frontend/src/hooks/useOpenedFile.ts +++ b/frontend/src/hooks/useOpenedFile.ts @@ -16,7 +16,7 @@ export function useOpenedFile() { console.log('✅ App opened with file:', filePath); setOpenedFilePath(filePath); - // Clear the file from Tauri state after consuming it + // Clear the file from service state after consuming it await fileOpenService.clearOpenedFile(); } else { console.log('â„šī¸ No file was opened with the app'); @@ -30,6 +30,17 @@ export function useOpenedFile() { }; checkForOpenedFile(); + + // Listen for runtime file open events (abstracted through service) + const unlistenRuntimeEvents = fileOpenService.onFileOpened((filePath) => { + console.log('📂 Runtime file open event:', filePath); + setOpenedFilePath(filePath); + }); + + // Cleanup function + return () => { + unlistenRuntimeEvents(); + }; }, []); return { openedFilePath, loading }; diff --git a/frontend/src/services/fileOpenService.ts b/frontend/src/services/fileOpenService.ts index 02b8729b7..3280ea028 100644 --- a/frontend/src/services/fileOpenService.ts +++ b/frontend/src/services/fileOpenService.ts @@ -4,6 +4,7 @@ export interface FileOpenService { getOpenedFile(): Promise; readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null>; clearOpenedFile(): Promise; + onFileOpened(callback: (filePath: string) => void): () => void; // Returns unlisten function } class TauriFileOpenService implements FileOpenService { @@ -45,6 +46,75 @@ class TauriFileOpenService implements FileOpenService { console.error('❌ Failed to clear opened file:', error); } } + + onFileOpened(callback: (filePath: string) => void): () => void { + let cleanup: (() => void) | null = null; + let isCleanedUp = false; + + const setupEventListeners = async () => { + try { + // Check if already cleaned up before async setup completes + if (isCleanedUp) { + return; + } + + // Only import if in Tauri environment + if (typeof window !== 'undefined' && ('__TAURI__' in window || '__TAURI_INTERNALS__' in window)) { + const { listen } = await import('@tauri-apps/api/event'); + + // Check again after async import + if (isCleanedUp) { + return; + } + + // Listen for macOS native file open events + const unlistenMacOS = await listen('macos://open-file', (event) => { + console.log('📂 macOS native file open event:', event.payload); + callback(event.payload as string); + }); + + // Listen for fallback file open events + const unlistenFallback = await listen('file-opened', (event) => { + console.log('📂 Fallback file open event:', event.payload); + callback(event.payload as string); + }); + + // Set up cleanup function only if not already cleaned up + if (!isCleanedUp) { + cleanup = () => { + try { + unlistenMacOS(); + unlistenFallback(); + console.log('✅ File event listeners cleaned up'); + } catch (error) { + console.error('❌ Error during file event cleanup:', error); + } + }; + } else { + // Clean up immediately if cleanup was called during setup + try { + unlistenMacOS(); + unlistenFallback(); + } catch (error) { + console.error('❌ Error during immediate cleanup:', error); + } + } + } + } catch (error) { + console.error('❌ Failed to setup file event listeners:', error); + } + }; + + setupEventListeners(); + + // Return cleanup function + return () => { + isCleanedUp = true; + if (cleanup) { + cleanup(); + } + }; + } } class WebFileOpenService implements FileOpenService { @@ -61,6 +131,14 @@ class WebFileOpenService implements FileOpenService { async clearOpenedFile(): Promise { // In web mode, no file clearing needed } + + onFileOpened(callback: (filePath: string) => void): () => void { + // In web mode, no file events - return no-op cleanup function + console.log('â„šī¸ Web mode: File event listeners not supported'); + return () => { + // No-op cleanup for web mode + }; + } } // Export the appropriate service based on environment