Mac specific file handler

This commit is contained in:
Connor Yoh 2025-07-15 18:29:42 +01:00
parent 4d05a1be41
commit e0ec0fbf4a
6 changed files with 306 additions and 38 deletions

View File

@ -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",

View File

@ -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"

View File

@ -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<R: Runtime>(app: &AppHandle<R>) {
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<String> = 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<Mutex<Option<AppHandle>>> = 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<R: Runtime>(app: &AppHandle<R>) {
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());
}
}

View File

@ -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<String> = 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));
}
}
});

View File

@ -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 };

View File

@ -4,6 +4,7 @@ export interface FileOpenService {
getOpenedFile(): Promise<string | null>;
readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null>;
clearOpenedFile(): Promise<void>;
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<void> {
// 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