Fix freeze on file/folder picker dialogs: use the non-blocking dialog API instead of blocking_pick_files()/blocking_pick_folder(), which deadlock when called from a command handler

This commit is contained in:
Renamr Scaffold
2026-08-29 02:10:56 +00:00
parent 07ce595f77
commit bab05826ef
3 changed files with 33 additions and 6 deletions
Generated
+1
View File
@@ -2536,6 +2536,7 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tokio",
"walkdir",
]
+1
View File
@@ -18,3 +18,4 @@ tauri-plugin-dialog = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
walkdir = "2"
tokio = { version = "1", features = ["sync"] }
+31 -6
View File
@@ -71,9 +71,23 @@ fn expand(paths: &[String]) -> Vec<FileEntry> {
}
/// 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<FileEntry> {
let selection = app.dialog().file().blocking_pick_files();
pub async fn pick_files(app: AppHandle) -> Vec<FileEntry> {
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<String> = selection
.into_iter()
.flatten()
@@ -84,9 +98,16 @@ pub fn pick_files(app: AppHandle) -> Vec<FileEntry> {
}
/// 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<FileEntry> {
let selection = app.dialog().file().blocking_pick_folder();
pub async fn pick_folder(app: AppHandle) -> Vec<FileEntry> {
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<String> = selection
.into_iter()
.filter_map(|f| f.into_path().ok())
@@ -96,7 +117,11 @@ pub fn pick_folder(app: AppHandle) -> Vec<FileEntry> {
}
/// 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<String>) -> Vec<FileEntry> {
expand(&paths)
pub async fn expand_dropped_paths(paths: Vec<String>) -> Vec<FileEntry> {
tauri::async_runtime::spawn_blocking(move || expand(&paths))
.await
.unwrap_or_default()
}