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()),
    }
}