ashpd

repository·main·Indexed 18 days ago

https://github.com/bilelmoussaoui/ashpd

A Rust wrapper for XDG desktop portals via DBus, built on top of zbus. It provides a type-safe, async-friendly, Rust-native alternative to libportal for accessing system features such as cameras, color pickers, screen casting, GameMode, and global shortcuts.

Tokens
14.6K
Snippets
56
Records
72
Agent score
63%

What's inside ashpd

  1. What is ASHPD?

    main
    ASHPD (Aperture Science Handheld Portal Device) is a Rust wrapper for the XDG portals DBus interfaces, built on top of zbus. It provides a high-level, easy-to-use API for interacting with various desktop portals as defined by the XDG specifications. It serves as a Rust-native alternative to the C library libportal.
  2. Requirements for writing a real portal backend

    main

    When moving from a demo to a production portal backend, you must satisfy two requirements:

    1. Installation: The backend's .portal file must be installed under {DATADIR}/xdg-desktop-portal/portals/.
    2. Configuration: Your desktop environment's configuration file must explicitly reference the new portal for the interfaces it implements.
  3. Test the portal backend demo locally

    main

    You can test the ashpd-backend-demo without installing it by using the $XDG_DESKTOP_PORTAL_DIR environment variable. This variable instructs the frontend where to look for the portal and configuration files.

    Warning: Setting this variable temporarily overrides your system's default portals (like GNOME or KDE). If the demo backend does not implement a specific interface that your applications expect, those interfaces will be unavailable during testing.

    XDG_DESKTOP_PORTAL_DIR=$PWD/demo/backend/data /usr/libexec/xdg-desktop-portal -v -r
  4. Core modules and types in ASHPD

    main

    ASHPD provides several modules for interacting with desktop services:

    • desktop: Interact with the user's desktop (e.g., screenshots, background settings, location).
    • documents: (Requires documents feature) Interact with the documents store or transfer files across apps.
    • flatpak: (Requires flatpak feature) Spawn commands outside the sandbox or monitor/install updates.
    • backend: (Requires backend feature) Build your own custom portals backend.

    Commonly used types:

    • AppID: Represents an application ID.
    • Uri: Represents a URI.
    • FilePath: Represents a file path.
    • ActivationToken: Used for activation-related tasks.
    • WindowIdentifier: Used to identify specific windows.
    • Error / PortalError: Error types for handling failures.
  5. How WindowIdentifier works for portal requests

    main

    Most portals (like file pickers or permission dialogs) need to be displayed on top of the application window that triggered them. To achieve this, the compositor requires a WindowIdentifier to associate the portal dialog with the parent application window.

    WindowIdentifier abstracts the differences between display protocols:

    • X11: Uses the format x11:XID, where XID is the hexadecimal XID of the window.
    • Wayland: Uses the format wayland:HANDLE, where HANDLE is a surface handle obtained via the xdg-foreign protocol.

    Depending on your toolkit and features, you can create these identifiers from GTK 4 natives, Wayland surfaces, or raw window handles.

    // Example: Creating an identifier from an X11 XID
    let identifier = WindowIdentifier::from_xid(212321);
    
    // Example: Creating an identifier from a Wayland surface (requires `wayland` feature)
    // let identifier = WindowIdentifier::from_wayland(wl_surface).await;
  6. Manage GameMode status with GameMode

    main

    The GameMode struct provides an interface for sandboxed applications to access the org.freedesktop.portal.GameMode DBus interface. It allows applications to register themselves (or other processes) as games to request GameMode activation, unregister them, and query the current status of a process.

    Key features:

    • PID Translation: If running in a sandbox with PID namespace isolation, the portal transparently translates sandbox PIDs to host PIDs.
    • Automatic Unregistration: If a registered client terminates without calling unregister, GameMode will automatically unregister it after a small delay.
    • Multiple Identification Methods: You can identify processes using raw PIDs, PID file descriptors (pidfd), or a combination of a target PID/FD and a requester PID/FD.
    use ashpd::desktop::game_mode::GameMode;
    
    async fn run() -> ashpd::Result<()> {
        let proxy = GameMode::new().await?;
    
        // Register a process
        proxy.register(246612).await?;
    
        // Query status
        let status = proxy.query_status(246612).await?;
        println!("{:#?}", status);
    
        // Unregister a process
        proxy.unregister(246612).await?;
    
        Ok(())
    }
  7. Manage long-lived interactions with the `Session` type

    main

    The Session<T> type is a wrapper around the org.freedesktop.portal.Session DBus interface. It is used by portals that involve long-lived interactions (such as ScreenCast, Remote Desktop, or Global Shortcuts). When a portal method creates a session, it returns a session handle (an object path) that identifies a Session object which remains alive for the duration of that interaction.

    Key capabilities:

    • Closing a session: You can end the session and all related user interactions (like active dialogs) by calling .close().
    • Monitoring closure: You can listen for the session being closed (either by your application or by the system/user) using the .receive_closed() stream.

    Note: You should not create a Session instance manually. Instead, it is provided as a response to a session-creating method from a SessionPortal.

    // Example of interacting with an existing session
    // (Assuming `session` is obtained from a SessionPortal call)
    
    // Listen for the session closing
    let mut closed_stream = session.receive_closed().await?;
    while let Some(details) = closed_stream.next().await {
        println!("Session closed with details: {:?}", details);
    }
    
    // Or manually close the session
    session.close().await?;
  8. Configure ASHPD optional features

    main

    ASHPD uses Cargo features to enable specific functionalities. By default, tokio is enabled. Other features include:

    • tracing: Enables debug information via the tracing library.
    • async-io: Enables compatibility with async-io crates (e.g., smol or glib).
    • frontend: Enables all frontend APIs. You can also enable individual APIs like account, camera, or screencast.
    • glib: Makes all enums derive glib::Enum (flags not yet supported).
    • gtk4: Implements From<Color> for gdk4::RGBA and provides WindowIdentifier::from_native for IsA<gtk4::Native>.
    • gtk4_wayland / gtk4_x11: Specialized WindowIdentifier::from_native support for specific backends.
    • pipewire: Provides ashpd::desktop::camera::pipewire_streams to help retrieve camera streams from a file descriptor.
    • raw_handle: Provides WindowIdentifier integration with the raw-window-handle crate.
    • wayland: Provides WindowIdentifier::from_wayland for the wayland-client crate.
    • backend: Enables support for implementing portal backend interfaces.
  9. Use the FileTransfer API for file exchange

    main

    The FileTransfer API acts as a middle-man for transferring files between applications, typically via drag-and-drop or copy-paste.

    Workflow:

    1. Sender:
      • Call start_transfer() to obtain a unique key.
      • Call add_files() using that key and a list of file descriptors to register files in the session.
      • Transmit the key to the target application (e.g., via the application/vnd.portal.filetransfer mimetype).
    2. Receiver:
      • Receive the key.
      • Call retrieve_files() with the key to obtain the list of file paths. The portal handles exporting the files so they are accessible to the receiver.
    3. Cleanup:
      • Call stop_transfer() to end the session.

    Note: Only regular files (not directories) can be added via add_files.

    use std::{fs::File, os::fd::AsFd};
    use ashpd::documents::file_transfer::{FileTransfer, StartTransferOptions};
    
    async fn run() -> ashpd::Result<()> {
        let proxy = FileTransfer::new().await?;
    
        // 1. Start transfer and get key
        let key = proxy
            .start_transfer(
                StartTransferOptions::default()
                    .set_writable(true)
                    .set_auto_stop(true),
            )
            .await?;
    
        // 2. Add files to the session
        let file = File::open("/path/to/file.jpg").unwrap();
        proxy
            .add_files(&key, &[&file.as_fd()], Default::default()),
            .await?;
    
        // 3. Receiver retrieves files using the key
        let files = proxy.retrieve_files(&key, Default::default()).await?;
        println!("{:#?}", files);
    
        // 4. Stop the transfer
        proxy.stop_transfer(&key).await?;
    
        Ok(())
    }
  10. Configure async runtime features

    main

    ASHPD requires an asynchronous runtime. You must enable either the tokio feature or the async-io feature. You cannot enable both at the same time.

    Error conditions:

    • Enabling both tokio and async-io will cause a compilation error.
    • Enabling neither tokio nor async-io will cause a compilation error.
  11. Use the NetworkMonitor API to check connectivity

    main

    The NetworkMonitor provides information about the host's network status, including availability, metered status, and detailed connectivity levels. It is a wrapper for the org.freedesktop.portal.NetworkMonitor DBus interface.

    Note: This portal does not work for sandboxed applications. Applications are typically expected to use this interface indirectly via libraries like GLib's gio::NetworkMonitor.

    use ashpd::desktop::network_monitor::NetworkMonitor;
    
    async fn run() -> ashpd::Result<()> {
        let proxy = NetworkMonitor::new().await?;
    
        println!("{}", proxy.can_reach("www.google.com", 80).await?);
        println!("{}", proxy.is_available().await?);
        println!("{:#?}", proxy.connectivity().await?);
        println!("{}", proxy.is_metered().await?);
        println!("{:#?}", proxy.status().await?);
    
        Ok(())
    }