use iced::futures::channel::oneshot; /// Run a blocking closure on a thread pool thread and await its result. /// Used to offload blocking work (HTTP, disk, process spawning) without /// stalling the iced event loop. pub async fn async_blocking(f: F) -> T where T: Send + 'static, F: FnOnce() -> T + Send + 'static, { let (tx, rx) = oneshot::channel(); std::thread::spawn(move || { let _ = tx.send(f()); }); rx.await.expect("blocking task panicked") } /// Open a native folder picker dialog and return the chosen path, or None if /// the user cancelled. Tries zenity (GNOME/GTK) then kdialog (KDE) in order. pub fn pick_folder(title: &str) -> Option { for (cmd, args) in [ ("zenity", vec!["--file-selection", "--directory", "--title", title]), ("kdialog", vec!["--getexistingdirectory", "/home", "--title", title]), ] { let Ok(out) = std::process::Command::new(cmd).args(&args).output() else { continue; }; if out.status.success() { let s = String::from_utf8_lossy(&out.stdout).trim().to_string(); if !s.is_empty() { return Some(s); } } } None } /// Open a native file picker dialog starting in `start_dir`, or None if /// the user cancelled. Tries zenity then kdialog. pub fn pick_file(title: &str, start_dir: &str) -> Option { // zenity uses --filename with a trailing slash to open a directory let start_slash = format!("{}/", start_dir.trim_end_matches('/')); let zenity_args = vec![ "--file-selection", "--title", title, "--filename", &start_slash, ]; let kdialog_args = vec!["--getopenfilename", start_dir, "--title", title]; for (cmd, args) in [("zenity", zenity_args.as_slice()), ("kdialog", kdialog_args.as_slice())] { let Ok(out) = std::process::Command::new(cmd).args(args).output() else { continue; }; if out.status.success() { let s = String::from_utf8_lossy(&out.stdout).trim().to_string(); if !s.is_empty() { return Some(s); } } } None }