xdg.rs
raw
//! The freedesktop file chooser: `org.freedesktop.portal.FileChooser`.
//!
//! One of [`FileDialog`](super::FileDialog)'s platform backends, and the only
//! one so far. It works under a sandbox, honours the user's bookmarks and
//! settings, and is the dialog every other application on the desktop shows.
use std::collections::HashMap;
use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value};
/// Ask the portal for files, blocking until the user is done.
pub(super) fn open_file(
title: &str,
multiple: bool,
filters: &[(String, String)],
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let connection = zbus::blocking::Connection::session()?;
// The portal answers on a Request object whose path we can predict — so
// we subscribe *before* asking, rather than racing a reply that could
// arrive before we were listening. The spelling is the portal spec's: our
// unique name without its leading colon and with dots as underscores,
// plus a token we choose.
let unique = connection
.unique_name()
.ok_or("the session bus gave us no unique name")?
.to_string();
let sender = unique.trim_start_matches(':').replace('.', "_");
let token = format!("guiduck_{}", std::process::id());
let request_path = format!("/org/freedesktop/portal/desktop/request/{sender}/{token}");
let request = zbus::blocking::Proxy::new(
&connection,
"org.freedesktop.portal.Desktop",
request_path.as_str(),
"org.freedesktop.portal.Request",
)?;
let mut responses = request.receive_signal("Response")?;
let mut options: HashMap<&str, Value<'_>> = HashMap::new();
options.insert("handle_token", Value::from(token.as_str()));
options.insert("multiple", Value::from(multiple));
if !filters.is_empty() {
// A filter is (name, [(kind, pattern)]); kind 0 is a glob.
let filters: Vec<(String, Vec<(u32, String)>)> = filters
.iter()
.map(|(label, glob)| (label.clone(), vec![(0u32, glob.clone())]))
.collect();
options.insert("filters", Value::from(filters));
}
let chooser = zbus::blocking::Proxy::new(
&connection,
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.FileChooser",
)?;
// An empty parent: winit exposes no Wayland window handle we could name
// one with (see `guiduck_core::widget::dialog`). The dialog is still real
// and compositor-managed; it is just not marked as ours.
let _handle: OwnedObjectPath = chooser.call("OpenFile", &("", title, options))?;
// Blocks until the user is done. That is the whole reason for the thread.
let message = responses
.next()
.ok_or("the portal closed without answering")?;
let (response, results): (u32, HashMap<String, OwnedValue>) = message.body().deserialize()?;
if response != 0 {
// 1 is cancelled, 2 is "ended some other way". Neither is an error:
// the user is allowed to change their mind.
return Ok(Vec::new());
}
match results.get("uris") {
Some(value) => Ok(Vec::<String>::try_from(value.try_clone()?)?),
None => Ok(Vec::new()),
}
}