//! Platform folder pickers without helper processes. use std::path::PathBuf; #[cfg(any(target_os = "macos", target_os = "windows"))] pub async fn pick_folder(initial: Option) -> Result, String> { let mut dialog = rfd::AsyncFileDialog::new().set_title("Open Password Store"); if let Some(initial) = initial.filter(|path| path.is_dir()) { dialog = dialog.set_directory(initial); } Ok(dialog .pick_folder() .await .map(|folder| folder.path().to_owned())) } #[cfg(target_os = "linux")] pub async fn pick_folder(initial: Option) -> Result, String> { use ashpd::{ PortalError, desktop::{file_chooser::SelectedFiles, request::ResponseError}, }; let mut request = SelectedFiles::open_file() .title("Open Password Store") .accept_label("Open") .modal(true) .multiple(false) .directory(true); if let Some(initial) = initial.filter(|path| path.is_dir()) { request = request .current_folder(initial) .map_err(|error| error.to_string())?; } let response = request.send().await.and_then(|request| request.response()); let selected = match response { Ok(selected) => selected, Err(ashpd::Error::Response(ResponseError::Cancelled)) | Err(ashpd::Error::Portal(PortalError::Cancelled(_))) => return Ok(None), Err(error) => return Err(error.to_string()), }; selected .uris() .first() .ok_or_else(|| "folder portal returned no selection".to_owned()) .and_then(|uri| file_uri_path(uri.as_str())) .map(Some) } #[cfg(target_os = "linux")] fn file_uri_path(uri: &str) -> Result { let uri = url::Url::parse(uri).map_err(|_| "folder portal returned an invalid URI")?; uri.to_file_path() .map_err(|()| "folder portal returned a non-file URI".to_owned()) } #[cfg(all(test, target_os = "linux"))] mod tests { use super::*; #[test] fn portal_file_uris_are_decoded_and_other_schemes_are_rejected() { assert_eq!( file_uri_path("file:///tmp/password%20store").expect("file URI"), PathBuf::from("/tmp/password store") ); assert!(file_uri_path("https://example.test/store").is_err()); } }