/// Run a blocking closure on the tokio blocking thread pool 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, { tokio::task::spawn_blocking(f) .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 }