diff --git a/Cargo.lock b/Cargo.lock index 97a1565..9e0c52d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2536,6 +2536,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-dialog", + "tokio", "walkdir", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index cf1c6cf..6bbb6cf 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,3 +18,4 @@ tauri-plugin-dialog = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" walkdir = "2" +tokio = { version = "1", features = ["sync"] } diff --git a/src-tauri/src/files.rs b/src-tauri/src/files.rs index 981f37c..76610c4 100644 --- a/src-tauri/src/files.rs +++ b/src-tauri/src/files.rs @@ -71,9 +71,23 @@ fn expand(paths: &[String]) -> Vec { } /// Native "Select Files" dialog, mirroring the original's Load Files button. +/// +/// Uses the non-blocking callback API (not `blocking_pick_files()`): the blocking +/// variant must never be called from a Tauri command handler, because on macOS it has +/// to hand off to the main thread to actually show the panel and then wait for a +/// response - if the command itself is occupying the thread the dialog needs, the app +/// deadlocks the moment the dialog tries to close (this is exactly what "froze after I +/// hit Cancel" looked like). The callback form lets the dialog signal completion +/// asynchronously instead, which we bridge back into an `async fn` with a oneshot +/// channel. #[tauri::command] -pub fn pick_files(app: AppHandle) -> Vec { - let selection = app.dialog().file().blocking_pick_files(); +pub async fn pick_files(app: AppHandle) -> Vec { + let (tx, rx) = tokio::sync::oneshot::channel(); + app.dialog().file().pick_files(move |result| { + let _ = tx.send(result); + }); + let selection = rx.await.unwrap_or(None); + let paths: Vec = selection .into_iter() .flatten() @@ -84,9 +98,16 @@ pub fn pick_files(app: AppHandle) -> Vec { } /// Native "Select Folders" dialog; the folder's contents are expanded recursively. +/// See `pick_files` above for why this uses the callback API instead of +/// `blocking_pick_folder()`. #[tauri::command] -pub fn pick_folder(app: AppHandle) -> Vec { - let selection = app.dialog().file().blocking_pick_folder(); +pub async fn pick_folder(app: AppHandle) -> Vec { + let (tx, rx) = tokio::sync::oneshot::channel(); + app.dialog().file().pick_folder(move |result| { + let _ = tx.send(result); + }); + let selection = rx.await.unwrap_or(None); + let paths: Vec = selection .into_iter() .filter_map(|f| f.into_path().ok()) @@ -96,7 +117,11 @@ pub fn pick_folder(app: AppHandle) -> Vec { } /// Expands paths dropped onto the window (files and/or folders) into their files. +/// Runs off the main thread too - not for a deadlock reason like the dialogs above, +/// just so recursively walking a very large dropped folder doesn't stall the UI. #[tauri::command] -pub fn expand_dropped_paths(paths: Vec) -> Vec { - expand(&paths) +pub async fn expand_dropped_paths(paths: Vec) -> Vec { + tauri::async_runtime::spawn_blocking(move || expand(&paths)) + .await + .unwrap_or_default() }