ashpd-0.9.1/.cargo_vcs_info.json0000644000000001360000000000100121420ustar { "git": { "sha1": "ed450a9816b73422794bc46075e729f5b58bfe60" }, "path_in_vcs": "" }ashpd-0.9.1/.editorconfig000064400000000000000000000004761046102023000134160ustar 00000000000000# http://editorconfig.org root = true [*] indent_style = space indent_size = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.xml, *.yml] indent_size = 2 [*.md] trim_trailing_whitespace = false [*.{build,yml,ui,yaml}] indent_size = 2 [*.{json,py}] indent_size = 4 ashpd-0.9.1/.github/dependabot.yml000064400000000000000000000004701046102023000151230ustar 00000000000000# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" ashpd-0.9.1/.github/workflows/CI.yml000064400000000000000000000043141046102023000153470ustar 00000000000000on: push: branches: [master] pull_request: name: CI jobs: check: name: Check runs-on: ubuntu-22.04 container: image: ghcr.io/gtk-rs/gtk4-rs/gtk4:latest steps: - uses: actions/checkout@v4 - name: Install dependencies run: sudo dnf install -y pipewire-devel clang-devel - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - uses: actions-rs/cargo@v1 with: command: check args: --features "gtk4,pipewire,wayland,raw_handle,tracing" test: name: Test Suite runs-on: ubuntu-22.04 container: image: ghcr.io/gtk-rs/gtk4-rs/gtk4:latest steps: - uses: actions/checkout@v4 - name: Install dependencies run: sudo dnf install -y pipewire-devel clang-devel - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - uses: actions-rs/cargo@v1 with: command: test args: --features "gtk4,pipewire,wayland,raw_handle,tracing" fmt: name: Rustfmt runs-on: ubuntu-22.04 container: image: ghcr.io/gtk-rs/gtk4-rs/gtk4:latest steps: - uses: actions/checkout@v4 - name: Install dependencies run: sudo dnf install -y pipewire-devel clang-devel - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - run: rustup component add rustfmt - uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check clippy: name: Clippy runs-on: ubuntu-22.04 container: image: ghcr.io/gtk-rs/gtk4-rs/gtk4:latest steps: - uses: actions/checkout@v4 - name: Install dependencies run: sudo dnf install -y pipewire-devel clang-devel - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - run: rustup component add clippy - uses: actions-rs/cargo@v1 with: command: clippy args: --features "gtk4,pipewire,wayland,raw_handle,tracing" -- -D warnings ashpd-0.9.1/.github/workflows/demo-ci.yml000064400000000000000000000011131046102023000163630ustar 00000000000000on: push: branches: [master] pull_request: name: Demo CI jobs: flatpak: name: "Flatpak" runs-on: ubuntu-22.04 container: image: bilelmoussaoui/flatpak-github-actions:gnome-nightly options: --privileged steps: - uses: actions/checkout@v4 - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 with: bundle: "ashpd-demo.flatpak" repository-name: "flathub" manifest-path: "ashpd-demo/build-aux/com.belmoussaoui.ashpd.demo.Devel.json" run-tests: true cache-key: flatpak-builder-${{ github.sha }} ashpd-0.9.1/.github/workflows/docs.yml000064400000000000000000000032361046102023000160060ustar 00000000000000name: docs on: push: branches: - master workflow_dispatch: permissions: contents: read pages: write id-token: write jobs: build: runs-on: ubuntu-latest container: image: ghcr.io/gtk-rs/gtk4-rs/gtk4:latest steps: - uses: actions/checkout@v4 - name: Install dependencies run: sudo dnf install -y pipewire-devel clang-devel - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: nightly override: true - uses: actions-rs/cargo@v1 env: RUSTFLAGS: --cfg docsrs RUSTDOCFLAGS: --cfg docsrs -Z unstable-options --extern-html-root-url=zbus=https://docs.rs/zbus/latest/ --extern-html-root-url=pipewire=https://pipewire.pages.freedesktop.org/pipewire-rs/ --extern-html-root-url=zvariant=https://docs.rs/zvariant/latest/ --extern-html-root-url=enumflags2=https://docs.rs/enumflags2/latest/ with: command: doc args: --package ashpd --features "gtk4,pipewire,wayland,raw_handle" --no-deps - name: Fix permissions run: | chmod -c -R +rX "target/doc/" | while read line; do echo "::warning title=Invalid file permissions automatically fixed::$line" done - name: Upload Pages artifact uses: actions/upload-pages-artifact@v3 with: path: ./target/doc/ deploy: needs: build environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ashpd-0.9.1/.github/workflows/typos.yml000064400000000000000000000004331046102023000162300ustar 00000000000000on: push: branches: [master] pull_request: name: Typos jobs: run: name: Spell Check with Typos runs-on: ubuntu-latest steps: - name: Checkout Actions Repository uses: actions/checkout@v4 - name: Check spelling uses: crate-ci/typos@master ashpd-0.9.1/.gitignore000064400000000000000000000007551046102023000127310ustar 00000000000000# Generated by Cargo # will have compiled files and executables /target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk ashpd-demo/.flatpak ashpd-demo/.vscode ashpd-demo/_build/ ashpd-demo/builddir/ ashpd-demo/src/config.rs ashpd-demo/target/ .flatpak .vscode _build/ builddir/ session-test ashpd-0.9.1/.rustfmt.toml000064400000000000000000000003121046102023000134050ustar 00000000000000imports_granularity = "Crate" format_code_in_doc_comments = true group_imports = "StdExternalCrate" newline_style = "Unix" normalize_comments = true normalize_doc_attributes = true wrap_comments = true ashpd-0.9.1/.typos.toml000064400000000000000000000000741046102023000130640ustar 00000000000000[default.extend-words] # Ignore false-positives eis = "eis" ashpd-0.9.1/Cargo.toml0000644000000061340000000000100101440ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.75" name = "ashpd" version = "0.9.1" authors = ["Bilal Elmoussaoui "] exclude = [ "interfaces/*.xml", "ashpd-demo/", ] description = "XDG portals wrapper in Rust using zbus" readme = "README.md" keywords = [ "portal", "flatpak", "xdg", "desktop", "dbus", ] categories = [ "gui", "os::linux-apis", "api-bindings", ] license = "MIT" repository = "https://github.com/bilelmoussaoui/ashpd" [package.metadata.docs.rs] features = [ "gtk4", "raw_handle", ] rustc-args = [ "--cfg", "docsrs", ] rustdoc-args = [ "--cfg", "docsrs", "--generate-link-to-definition", ] [dependencies.async-fs] version = "2.1.0" optional = true [dependencies.async-net] version = "2.0.0" optional = true [dependencies.enumflags2] version = "0.7" [dependencies.futures-channel] version = "0.3" [dependencies.futures-util] version = "0.3" [dependencies.gdk4wayland] version = "0.9" optional = true package = "gdk4-wayland" [dependencies.gdk4x11] version = "0.9" optional = true package = "gdk4-x11" [dependencies.glib] version = "0.20" optional = true [dependencies.gtk4] version = "0.9" optional = true [dependencies.pipewire] version = "0.8" optional = true [dependencies.rand] version = "0.8" default-features = false [dependencies.raw-window-handle] version = "0.6" optional = true [dependencies.serde] version = "1.0" features = ["derive"] [dependencies.serde_repr] version = "0.1" [dependencies.tokio] version = "1.21" features = [ "fs", "io-util", ] optional = true default-features = false [dependencies.tracing] version = "0.1" optional = true [dependencies.url] version = "2.3" features = ["serde"] [dependencies.wayland-backend] version = "0.3" features = ["client_system"] optional = true [dependencies.wayland-client] version = "0.31" optional = true [dependencies.wayland-protocols] version = "0.32" features = [ "unstable", "client", ] optional = true [dependencies.zbus] version = "4.0" features = ["url"] default-features = false [dev-dependencies.reis] version = "0.2.0" features = ["tokio"] [dev-dependencies.serde_json] version = "1.0" [features] async-std = [ "zbus/async-io", "dep:async-fs", "dep:async-net", ] default = ["async-std"] glib = ["dep:glib"] gtk4 = [ "gtk4_x11", "gtk4_wayland", ] gtk4_wayland = [ "gdk4wayland", "glib", "dep:gtk4", ] gtk4_x11 = [ "gdk4x11", "glib", "dep:gtk4", ] raw_handle = [ "raw-window-handle", "wayland", ] tokio = [ "zbus/tokio", "dep:tokio", ] wayland = [ "wayland-client", "wayland-protocols", "wayland-backend", ] ashpd-0.9.1/Cargo.toml.orig000064400000000000000000000043231046102023000136230ustar 00000000000000[package] authors = ["Bilal Elmoussaoui "] categories = ["gui", "os::linux-apis", "api-bindings"] description = "XDG portals wrapper in Rust using zbus" edition = "2021" exclude = ["interfaces/*.xml", "ashpd-demo/"] keywords = ["portal", "flatpak", "xdg", "desktop", "dbus"] license = "MIT" name = "ashpd" repository = "https://github.com/bilelmoussaoui/ashpd" version = "0.9.1" rust-version = "1.75" [features] async-std = ["zbus/async-io", "dep:async-fs", "dep:async-net"] default = ["async-std"] gtk4 = ["gtk4_x11", "gtk4_wayland"] gtk4_wayland = ["gdk4wayland", "glib", "dep:gtk4"] gtk4_x11 = ["gdk4x11", "glib", "dep:gtk4"] raw_handle = ["raw-window-handle", "wayland"] tokio = ["zbus/tokio", "dep:tokio"] glib = ["dep:glib"] wayland = ["wayland-client", "wayland-protocols", "wayland-backend"] [dependencies] async-fs = { version = "2.1.0", optional = true } async-net = { version = "2.0.0", optional = true } enumflags2 = "0.7" futures-channel = "0.3" futures-util = "0.3" gdk4wayland = { package = "gdk4-wayland", version = "0.9", optional = true } gdk4x11 = { package = "gdk4-x11", version = "0.9", optional = true } glib = { version = "0.20", optional = true } gtk4 = { version = "0.9", optional = true } pipewire = { version = "0.8", optional = true } rand = { version = "0.8", default-features = false } raw-window-handle = { version = "0.6", optional = true } serde = { version = "1.0", features = ["derive"] } serde_repr = "0.1" tokio = { version = "1.21", features = [ "fs", "io-util", ], optional = true, default-features = false } tracing = { version = "0.1", optional = true } url = { version = "2.3", features = ["serde"] } wayland-backend = { version = "0.3", optional = true, features = [ "client_system", ] } wayland-client = { version = "0.31", optional = true } wayland-protocols = { version = "0.32", optional = true, features = [ "unstable", "client", ] } zbus = { version = "4.0", default-features = false, features = ["url"] } [dev-dependencies] serde_json = "1.0" reis = { version = "0.2.0", features = [ "tokio" ] } [package.metadata.docs.rs] features = ["gtk4", "raw_handle"] rustc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"] ashpd-0.9.1/LICENSE000064400000000000000000000020621046102023000117370ustar 00000000000000MIT License Copyright (c) 2020 Bilal Elmoussaoui Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ashpd-0.9.1/README.md000064400000000000000000000065041046102023000122160ustar 00000000000000# ASHPD [![docs](https://docs.rs/ashpd/badge.svg)](https://docs.rs/ashpd/) [![crates.io](https://img.shields.io/crates/v/ashpd)](https://crates.io/crates/ashpd) ![CI](https://github.com/bilelmoussaoui/ashpd/workflows/CI/badge.svg) ASHPD, acronym of Aperture Science Handheld Portal Device is a Rust & [zbus](https://gitlab.freedesktop.org/dbus/zbus) wrapper of the XDG portals DBus interfaces. The library aims to provide an easy way to interact with the various portals defined per the [specifications](https://flatpak.github.io/xdg-desktop-portal/docs/). It provides an alternative to the C library [https://github.com/flatpak/libportal](https://github.com/flatpak/libportal) ## Examples Ask the compositor to pick a color ```rust,no_run use ashpd::desktop::Color; async fn run() -> ashpd::Result<()> { let color = Color::pick().send().await?.response()?; println!("({}, {}, {})", color.red(), color.green(), color.blue()); Ok(()) } ``` Start a PipeWire stream from the user's camera ```rust,no_run use ashpd::desktop::camera::Camera; pub async fn run() -> ashpd::Result<()> { let camera = Camera::new().await?; if camera.is_present().await? { camera.request_access().await?; let remote_fd = camera.open_pipe_wire_remote().await?; // pass the remote fd to GStreamer for example } Ok(()) } ``` ## Optional features | Feature | Description | Default | | --- | ----------- | ------- | | tracing | Record various debug information using the `tracing` library | No | | tokio | Enable tokio runtime on zbus dependency | No | | async-std | Enable the use of the async-std runtime | Yes | | glib | Make all the enums derive `glib::Enum`. Flags are not supported yet | No | | gtk4 | Implement `From` for [`gdk4::RGBA`](https://gtk-rs.org/gtk4-rs/stable/latest/docs/gdk4/struct.RGBA.html) Provides `WindowIdentifier::from_native` that takes a [`IsA`](https://gtk-rs.org/gtk4-rs/stable/latest/docs/gtk4/struct.Native.html) | No | | gtk4_wayland |Provides `WindowIdentifier::from_native` that takes a [`IsA`](https://gtk-rs.org/gtk4-rs/stable/latest/docs/gtk4/struct.Native.html) with Wayland backend support only | No | | gtk4_x11 |Provides `WindowIdentifier::from_native` that takes a [`IsA`](https://gtk-rs.org/gtk4-rs/stable/latest/docs/gtk4/struct.Native.html) with X11 backend support only | No | | pipewire | Provides `ashpd::desktop::camera::pipewire_streams` that helps you retrieve the various camera streams associated with the retrieved file descriptor| No | | raw_handle | Provides `WindowIdentifier::from_raw_handle` and `WindowIdentifier::as_raw_handle` for [raw-window-handle](https://lib.rs/crates/raw-window-handle) crate | No | | wayland | Provides `WindowIdentifier::from_wayland` for [wayland-client](https://lib.rs/crates/wayland-client) crate | No | ## Demo The library comes with a [demo](./ashpd-demo) built using the [GTK 4 Rust bindings](https://gtk-rs.org/gtk4-rs) and previews most of the portals. It is meant as a test case for the portals (from a distributor perspective) and as a way for the developers to see which portals exists and how to integrate them into their application using ASHPD. ashpd-0.9.1/src/activation_token/mod.rs000064400000000000000000000020131046102023000162030ustar 00000000000000use std::ops::Deref; use serde::{Deserialize, Serialize}; use zbus::zvariant::Type; /// A token that can be used to activate an application. /// /// No guarantees are made for the token structure. #[derive(Debug, Deserialize, Serialize, Type, PartialEq, Eq, Hash, Clone)] pub struct ActivationToken(String); impl From for ActivationToken { fn from(value: String) -> Self { Self(value) } } impl From<&str> for ActivationToken { fn from(value: &str) -> Self { Self(value.to_owned()) } } impl From for String { fn from(value: ActivationToken) -> String { value.0 } } impl Deref for ActivationToken { type Target = str; fn deref(&self) -> &Self::Target { self.0.as_str() } } impl AsRef for ActivationToken { fn as_ref(&self) -> &str { self.0.as_str() } } impl std::fmt::Display for ActivationToken { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.as_ref()) } } ashpd-0.9.1/src/app_id.rs000064400000000000000000000143411046102023000133260ustar 00000000000000use std::{ops::Deref, str::FromStr}; use serde::{Deserialize, Serialize}; use zbus::zvariant::Type; /// The application ID. /// /// See . #[derive(Debug, Serialize, Type, PartialEq, Eq, Hash, Clone)] pub struct AppID(String); impl FromStr for AppID { type Err = crate::Error; fn from_str(value: &str) -> Result { if is_valid_app_id(value) { Ok(Self(value.to_owned())) } else { Err(Self::Err::InvalidAppID) } } } impl TryFrom for AppID { type Error = crate::Error; fn try_from(value: String) -> Result { value.parse::() } } impl TryFrom<&str> for AppID { type Error = crate::Error; fn try_from(value: &str) -> Result { value.parse::() } } impl From for String { fn from(value: AppID) -> String { value.0 } } impl AsRef for AppID { fn as_ref(&self) -> &str { self.0.as_ref() } } impl Deref for AppID { type Target = str; fn deref(&self) -> &Self::Target { &self.0 } } impl std::fmt::Display for AppID { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.as_ref()) } } impl<'de> Deserialize<'de> for AppID { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let app_id = String::deserialize(deserializer)?; app_id .parse::() .map_err(|err| serde::de::Error::custom(err.to_string())) } } /// The ID of a file in the document store. #[derive(Debug, Serialize, Deserialize, Type, PartialEq, Eq, Hash, Clone)] pub struct DocumentID(String); impl From<&str> for DocumentID { fn from(value: &str) -> Self { Self(value.to_owned()) } } impl From for DocumentID { fn from(value: String) -> Self { Self(value) } } impl From for String { fn from(value: DocumentID) -> String { value.0 } } impl AsRef for DocumentID { fn as_ref(&self) -> &str { self.0.as_ref() } } impl Deref for DocumentID { type Target = str; fn deref(&self) -> &Self::Target { &self.0 } } impl std::fmt::Display for DocumentID { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.as_ref()) } } // Helpers fn is_valid_app_id(string: &str) -> bool { let len = string.len(); // The app id has to be between 0 < len <= 255 if len == 0 || 255 < len { return false; } let elements: Vec<&str> = string.split('.').collect(); let segments = elements.len(); if segments < 2 { return false; } for (idx_segment, element) in elements.iter().enumerate() { // No empty segments. if element.is_empty() { return false; } for (idx_char, c) in element.chars().enumerate() { // First char cannot be a digit. if idx_char == 0 && c.is_ascii_digit() { return false; } if !is_valid_app_id_char(c) { return false; } // Only the last segment can contain `-`. if idx_segment < segments - 1 && c == '-' { return false; } } } true } /// Only valid chars are a-z A-Z 0-9 - _ fn is_valid_app_id_char(c: char) -> bool { c.is_ascii_alphanumeric() || matches!(c, '-' | '_') } #[cfg(test)] mod tests { use super::*; #[test] fn test_is_valid_app_id() { assert!(is_valid_app_id("a.b")); assert!(is_valid_app_id("a_c.b_c.h_c")); assert!(is_valid_app_id("a.c-b")); assert!(is_valid_app_id("a.c2.d")); assert!(!is_valid_app_id("a")); assert!(!is_valid_app_id("")); assert!(!is_valid_app_id("a-z.b.c.d")); assert!(!is_valid_app_id("a.b-z.c.d")); assert!(!is_valid_app_id("a.b.c-z.d")); assert!(!is_valid_app_id("a.0b.c")); assert!(!is_valid_app_id("a..c")); assert!(!is_valid_app_id("a.é")); assert!(!is_valid_app_id("a.京")); // Tests from // https://github.com/bilelmoussaoui/flatpak-vscode/blob/master/src/test/suite/extension.test.ts assert!(is_valid_app_id("_org.SomeApp")); assert!(is_valid_app_id("com.org.SomeApp")); assert!(is_valid_app_id("com.org_._SomeApp")); assert!(is_valid_app_id("com.org._1SomeApp")); assert!(is_valid_app_id("com.org._1_SomeApp")); assert!(is_valid_app_id("VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.a111111111111")); assert!(!is_valid_app_id("com.org-._SomeApp")); assert!(!is_valid_app_id("package")); assert!(!is_valid_app_id("NoDot")); assert!(!is_valid_app_id("No-dot")); assert!(!is_valid_app_id("No_dot")); assert!(!is_valid_app_id("Has.Two..Consecutive.Dots")); assert!(!is_valid_app_id("HasThree...Consecutive.Dots")); assert!(!is_valid_app_id(".StartsWith.A.Period")); assert!(!is_valid_app_id(".")); assert!(!is_valid_app_id("Ends.With.A.Period.")); assert!(!is_valid_app_id("0P.Starts.With.A.Digit")); assert!(!is_valid_app_id("com.org.1SomeApp")); assert!(!is_valid_app_id("Element.Starts.With.A.1Digit")); assert!(!is_valid_app_id("VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.a1111111111112")); assert!(!is_valid_app_id("")); assert!(!is_valid_app_id("contains.;nvalid.characters")); assert!(!is_valid_app_id("con\nins.invalid.characters")); assert!(!is_valid_app_id("con/ains.invalid.characters")); assert!(!is_valid_app_id("conta|ns.invalid.characters")); assert!(!is_valid_app_id("contæins.inva_å_lid.characters")); } } ashpd-0.9.1/src/desktop/account.rs000064400000000000000000000071711046102023000152020ustar 00000000000000//! Access to the current logged user information such as the id, name //! or their avatar uri. //! //! Wrapper of the DBus interface: [`org.freedesktop.portal.Account`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Account.html). //! //! ### Examples //! //! ```rust, no_run //! use ashpd::desktop::account::UserInformation; //! //! async fn run() -> ashpd::Result<()> { //! let response = UserInformation::request() //! .reason("App would like to access user information") //! .send() //! .await? //! .response()?; //! //! println!("Name: {}", response.name()); //! println!("ID: {}", response.id()); //! //! Ok(()) //! } //! ``` use zbus::zvariant::{DeserializeDict, SerializeDict, Type}; use super::HandleToken; use crate::{desktop::request::Request, proxy::Proxy, Error, WindowIdentifier}; #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct UserInformationOptions { handle_token: HandleToken, reason: Option, } #[derive(Debug, DeserializeDict, SerializeDict, Type)] /// The response of a [`UserInformationRequest`] request. #[zvariant(signature = "dict")] pub struct UserInformation { id: String, name: String, image: url::Url, } impl UserInformation { /// User identifier. pub fn id(&self) -> &str { &self.id } /// User name. pub fn name(&self) -> &str { &self.name } /// User image uri. pub fn image(&self) -> &url::Url { &self.image } /// Creates a new builder-pattern struct instance to construct /// [`UserInformation`]. /// /// This method returns an instance of [`UserInformationRequest`]. pub fn request() -> UserInformationRequest { UserInformationRequest::default() } } struct AccountProxy<'a>(Proxy<'a>); impl<'a> AccountProxy<'a> { pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.Account").await?; Ok(Self(proxy)) } pub async fn user_information( &self, identifier: &WindowIdentifier, options: UserInformationOptions, ) -> Result, Error> { self.0 .request( &options.handle_token, "GetUserInformation", (&identifier, &options), ) .await } } impl<'a> std::ops::Deref for AccountProxy<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } #[doc(alias = "xdp_portal_get_user_information")] #[doc(alias = "org.freedesktop.portal.Account")] #[derive(Debug, Default)] /// A [builder-pattern] type to construct [`UserInformation`]. /// /// [builder-pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html pub struct UserInformationRequest { options: UserInformationOptions, identifier: WindowIdentifier, } impl UserInformationRequest { #[must_use] /// Sets a user-visible reason for the request. pub fn reason<'a>(mut self, reason: impl Into>) -> Self { self.options.reason = reason.into().map(ToOwned::to_owned); self } #[must_use] /// Sets a window identifier. pub fn identifier(mut self, identifier: impl Into>) -> Self { self.identifier = identifier.into().unwrap_or_default(); self } /// Build the [`UserInformation`]. pub async fn send(self) -> Result, Error> { let proxy = AccountProxy::new().await?; proxy.user_information(&self.identifier, self.options).await } } ashpd-0.9.1/src/desktop/background.rs000064400000000000000000000153531046102023000156660ustar 00000000000000//! Request to run in the background or started automatically when the user //! logs in. //! //! **Note** This portal only works for sandboxed applications. //! //! Wrapper of the DBus interface: [`org.freedesktop.portal.Background`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Background.html). //! //! ### Examples //! //! ```rust,no_run //! use ashpd::desktop::background::Background; //! //! async fn run() -> ashpd::Result<()> { //! let response = Background::request() //! .reason("Automatically fetch your latest mails") //! .auto_start(true) //! .command(&["geary"]) //! .dbus_activatable(false) //! .send() //! .await? //! .response()?; //! //! println!("{}", response.auto_start()); //! println!("{}", response.run_in_background()); //! //! Ok(()) //! } //! ``` //! //! If no `command` is provided, the [`Exec`](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables) line from the [desktop //! file](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#introduction) will be used. use serde::Serialize; use zbus::zvariant::{DeserializeDict, SerializeDict, Type}; use super::{HandleToken, Request}; use crate::{proxy::Proxy, Error, WindowIdentifier}; #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct BackgroundOptions { handle_token: HandleToken, reason: Option, autostart: Option, #[zvariant(rename = "dbus-activatable")] dbus_activatable: Option, #[zvariant(rename = "commandline")] command: Option>, } #[derive(DeserializeDict, Type, Debug)] /// The response of a [`BackgroundRequest`] request. #[zvariant(signature = "dict")] pub struct Background { background: bool, autostart: bool, } impl Background { /// Creates a new builder-pattern struct instance to construct /// [`Background`]. /// /// This method returns an instance of [`BackgroundRequest`]. pub fn request() -> BackgroundRequest { BackgroundRequest::default() } /// If the application is allowed to run in the background. pub fn run_in_background(&self) -> bool { self.background } /// If the application will be auto-started. pub fn auto_start(&self) -> bool { self.autostart } } #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct SetStatusOptions { message: String, } /// The interface lets sandboxed applications request that the application /// is allowed to run in the background or started automatically when the user /// logs in. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.Background`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Background.html). #[doc(alias = "org.freedesktop.portal.Background")] pub struct BackgroundProxy<'a>(Proxy<'a>); impl<'a> BackgroundProxy<'a> { /// Create a new instance of [`BackgroundProxy`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.Background").await?; Ok(Self(proxy)) } /// Sets the status of the application running in background. /// /// # Arguments /// /// * `message` - A string that will be used as the status message of the /// application. /// /// # Required version /// /// The method requires the 2nd version implementation of the portal and /// would fail with [`Error::RequiresVersion`] otherwise. /// /// # Specifications /// /// See also [`SetStatus`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Background.html#org-freedesktop-portal-background-setstatus). pub async fn set_status(&self, message: &str) -> Result<(), Error> { self.0 .call_versioned( "SetStatus", &(SetStatusOptions { message: message.to_owned(), }), 2, ) .await } async fn request_background( &self, identifier: &WindowIdentifier, options: BackgroundOptions, ) -> Result, Error> { self.0 .request( &options.handle_token, "RequestBackground", (&identifier, &options), ) .await } } impl<'a> std::ops::Deref for BackgroundProxy<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } #[doc(alias = "xdp_portal_request_background")] /// A [builder-pattern] type to construct [`Background`]. /// /// [builder-pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html #[derive(Debug, Default)] pub struct BackgroundRequest { identifier: WindowIdentifier, options: BackgroundOptions, } impl BackgroundRequest { #[must_use] /// Sets a window identifier. pub fn identifier(mut self, identifier: impl Into>) -> Self { self.identifier = identifier.into().unwrap_or_default(); self } #[must_use] /// Sets whether to auto start the application or not. pub fn auto_start(mut self, auto_start: impl Into>) -> Self { self.options.autostart = auto_start.into(); self } #[must_use] /// Sets whether the application is dbus activatable. pub fn dbus_activatable(mut self, dbus_activatable: impl Into>) -> Self { self.options.dbus_activatable = dbus_activatable.into(); self } #[must_use] /// Specifies the command line to execute. /// If this is not specified, the [`Exec`](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables) line from the [desktop /// file](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#introduction) pub fn command, I: AsRef + Type + Serialize>( mut self, command: impl Into>, ) -> Self { self.options.command = command .into() .map(|a| a.into_iter().map(|s| s.as_ref().to_owned()).collect()); self } #[must_use] /// Sets a user-visible reason for the request. pub fn reason<'a>(mut self, reason: impl Into>) -> Self { self.options.reason = reason.into().map(ToOwned::to_owned); self } /// Build the [`Background`]. pub async fn send(self) -> Result, Error> { let proxy = BackgroundProxy::new().await?; proxy .request_background(&self.identifier, self.options) .await } } ashpd-0.9.1/src/desktop/camera.rs000064400000000000000000000213761046102023000150010ustar 00000000000000//! Check if a camera is available, request access to it and open a PipeWire //! remote stream. //! //! ### Examples //! //! ```rust,no_run //! use ashpd::desktop::camera::Camera; //! //! pub async fn run() -> ashpd::Result<()> { //! let camera = Camera::new().await?; //! if camera.is_present().await? { //! camera.request_access().await?; //! let remote_fd = camera.open_pipe_wire_remote().await?; //! // pass the remote fd to GStreamer for example //! } //! Ok(()) //! } //! ``` use std::{collections::HashMap, os::fd::OwnedFd}; #[cfg(feature = "pipewire")] use pipewire::{context::Context, main_loop::MainLoop}; use zbus::zvariant::{self, SerializeDict, Type, Value}; use super::{HandleToken, Request}; use crate::{proxy::Proxy, Error}; #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct CameraAccessOptions { handle_token: HandleToken, } /// The interface lets sandboxed applications access camera devices, such as web /// cams. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.Camera`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Camera.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.Camera")] pub struct Camera<'a>(Proxy<'a>); impl<'a> Camera<'a> { /// Create a new instance of [`Camera`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.Camera").await?; Ok(Self(proxy)) } /// Requests an access to the camera. /// /// # Specifications /// /// See also [`AccessCamera`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Camera.html#org-freedesktop-portal-camera-accesscamera). #[doc(alias = "AccessCamera")] #[doc(alias = "xdp_portal_access_camera")] pub async fn request_access(&self) -> Result, Error> { let options = CameraAccessOptions::default(); self.0 .empty_request(&options.handle_token, "AccessCamera", &options) .await } /// Open a file descriptor to the PipeWire remote where the camera nodes are /// available. /// /// # Returns /// /// File descriptor of an open PipeWire remote. /// /// # Specifications /// /// See also [`OpenPipeWireRemote`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Camera.html#org-freedesktop-portal-camera-openpipewireremote). #[doc(alias = "OpenPipeWireRemote")] #[doc(alias = "xdp_portal_open_pipewire_remote_for_camera")] pub async fn open_pipe_wire_remote(&self) -> Result { // `options` parameter doesn't seems to be used yet // see https://github.com/flatpak/xdg-desktop-portal/blob/master/src/camera.c#L178 let options: HashMap<&str, Value<'_>> = HashMap::new(); let fd = self .0 .call::("OpenPipeWireRemote", &options) .await?; Ok(fd.into()) } /// A boolean stating whether there is any cameras available. /// /// # Specifications /// /// See also [`IsCameraPresent`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Camera.html#org-freedesktop-portal-camera-iscamerapresent). #[doc(alias = "IsCameraPresent")] #[doc(alias = "xdp_portal_is_camera_present")] pub async fn is_present(&self) -> Result { self.0.property("IsCameraPresent").await } } impl<'a> std::ops::Deref for Camera<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } #[cfg(feature = "pipewire")] /// A PipeWire camera stream returned by [`pipewire_streams`]. #[derive(Debug)] pub struct Stream { node_id: u32, properties: HashMap, } #[cfg(feature = "pipewire")] impl Stream { /// The id of the PipeWire node. pub fn node_id(&self) -> u32 { self.node_id } /// The node properties. pub fn properties(&self) -> HashMap { self.properties.clone() } } #[cfg(feature = "pipewire")] fn pipewire_streams_inner( fd: OwnedFd, callback: F, done_callback: G, ) -> Result<(), pipewire::Error> { let mainloop = MainLoop::new(None)?; let context = Context::new(&mainloop)?; let core = context.connect_fd(fd, None)?; let registry = core.get_registry()?; let pending = core.sync(0).expect("sync failed"); let loop_clone = mainloop.clone(); let _listener_reg = registry .add_listener_local() .global(move |global| { if let Some(props) = &global.props { if props.get("media.role") == Some("Camera") { #[cfg(feature = "tracing")] tracing::info!("found camera: {:#?}", props); let mut properties = HashMap::new(); for (key, value) in props.iter() { properties.insert(key.to_string(), value.to_string()); } let node_id = global.id; let stream = Stream { node_id, properties, }; callback.clone()(stream); } } }) .register(); let _listener_core = core .add_listener_local() .done(move |id, seq| { if id == pipewire::core::PW_ID_CORE && seq == pending { loop_clone.quit(); done_callback.clone()(); } }) .register(); mainloop.run(); Ok(()) } /// A helper to get a list of PipeWire streams to use with the camera file /// descriptor returned by [`Camera::open_pipe_wire_remote`]. /// /// Currently, the camera portal only gives us a file descriptor. Not passing a /// node id may cause the media session controller to auto-connect the client to /// an incorrect node. /// /// The method looks for the available output streams of a `media.role` type of /// `Camera` and return a list of `Stream`s. /// /// *Note* The socket referenced by `fd` must not be used while this function is /// running. #[cfg(feature = "pipewire")] #[cfg_attr(docsrs, doc(cfg(feature = "pipewire")))] pub async fn pipewire_streams(fd: OwnedFd) -> Result, pipewire::Error> { let (sender, receiver) = futures_channel::oneshot::channel(); let (streams_sender, mut streams_receiver) = futures_channel::mpsc::unbounded(); let sender = std::sync::Arc::new(std::sync::Mutex::new(Some(sender))); let streams_sender = std::sync::Arc::new(std::sync::Mutex::new(streams_sender)); std::thread::spawn(move || { let inner_sender = sender.clone(); if let Err(err) = pipewire_streams_inner( fd, move |stream| { let inner_streams_sender = streams_sender.clone(); if let Ok(mut sender) = inner_streams_sender.lock() { let _result = sender.start_send(stream); }; }, move || { if let Ok(mut guard) = inner_sender.lock() { if let Some(inner_sender) = guard.take() { let _result = inner_sender.send(Ok(())); } } }, ) { #[cfg(feature = "tracing")] tracing::error!("Failed to get pipewire streams {:#?}", err); let mut guard = sender.lock().unwrap(); if let Some(sender) = guard.take() { let _ = sender.send(Err(err)); } } }); receiver.await.unwrap()?; let mut streams = vec![]; while let Ok(Some(stream)) = streams_receiver.try_next() { streams.push(stream); } Ok(streams) } #[cfg(not(feature = "pipewire"))] #[cfg_attr(docsrs, doc(cfg(not(feature = "pipewire"))))] /// Request access to the camera and return a file descriptor if one is /// available. pub async fn request() -> Result, Error> { let proxy = Camera::new().await?; proxy.request_access().await?; if proxy.is_present().await? { Ok(Some(proxy.open_pipe_wire_remote().await?)) } else { Ok(None) } } #[cfg(feature = "pipewire")] #[cfg_attr(docsrs, doc(cfg(feature = "pipewire")))] /// Request access to the camera and return a file descriptor and a list of the /// available streams, one per camera. pub async fn request() -> Result)>, Error> { let proxy = Camera::new().await?; proxy.request_access().await?; if proxy.is_present().await? { let fd = proxy.open_pipe_wire_remote().await?; let streams = pipewire_streams(fd.try_clone()?).await?; Ok(Some((fd, streams))) } else { Ok(None) } } ashpd-0.9.1/src/desktop/clipboard.rs000064400000000000000000000135151046102023000155040ustar 00000000000000//! Interact with the clipboard. //! //! The portal is mostly meant to be used along with //! [`RemoteDesktop`](crate::desktop::remote_desktop::RemoteDesktop) use std::collections::HashMap; use futures_util::{Stream, StreamExt}; use zbus::zvariant::{DeserializeDict, OwnedFd, OwnedObjectPath, SerializeDict, Type, Value}; use super::{remote_desktop::RemoteDesktop, Session}; use crate::{proxy::Proxy, Result}; #[derive(Debug, Type, SerializeDict)] #[zvariant(signature = "dict")] struct SetSelectionOptions<'a> { mime_types: &'a [&'a str], } #[derive(Debug, Type, DeserializeDict)] #[zvariant(signature = "dict")] /// The details of a new clipboard selection. pub struct SelectionOwnerChanged { mime_types: Option>, session_is_owner: Option, } impl SelectionOwnerChanged { /// Whether the session is the owner of the clipboard selection or not. pub fn session_is_owner(&self) -> Option { self.session_is_owner } /// A list of mime types the new clipboard has content for. pub fn mime_types(&self) -> Vec { self.mime_types.clone().unwrap_or_default() } } #[doc(alias = "org.freedesktop.portal.Clipboard")] /// Wrapper of the DBus interface: [`org.freedesktop.portal.Clipboard`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Clipboard.html). pub struct Clipboard<'a>(Proxy<'a>); impl<'a> Clipboard<'a> { /// Create a new instance of [`Clipboard`]. pub async fn new() -> Result> { Ok(Self( Proxy::new_desktop("org.freedesktop.portal.Clipboard").await?, )) } /// # Specifications /// /// See also [`RequestClipboard`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Clipboard.html#org-freedesktop-portal-clipboard-requestclipboard). #[doc(alias = "RequestClipboard")] pub async fn request(&self, session: &Session<'_, RemoteDesktop<'_>>) -> Result<()> { let options: HashMap<&str, Value<'_>> = HashMap::default(); self.0 .call_method("RequestClipboard", &(session, options)) .await?; Ok(()) } /// # Specifications /// /// See also [`SetSelection`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Clipboard.html#org-freedesktop-portal-clipboard-setselection). #[doc(alias = "SetSelection")] pub async fn set_selection( &self, session: &Session<'_, RemoteDesktop<'_>>, mime_types: &[&str], ) -> Result<()> { let options = SetSelectionOptions { mime_types }; self.0.call("SetSelection", &(session, options)).await?; Ok(()) } /// # Specifications /// /// See also [`SelectionWrite`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Clipboard.html#org-freedesktop-portal-clipboard-selectionwrite). #[doc(alias = "SelectionWrite")] pub async fn selection_write( &self, session: &Session<'_, RemoteDesktop<'_>>, serial: u32, ) -> Result { let fd = self .0 .call::("SelectionWrite", &(session, serial)) .await?; Ok(fd) } /// # Specifications /// /// See also [`SelectionWriteDone`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Clipboard.html#org-freedesktop-portal-clipboard-selectionwritedone). #[doc(alias = "SelectionWriteDone")] pub async fn selection_write_done( &self, session: &Session<'_, RemoteDesktop<'_>>, serial: u32, success: bool, ) -> Result<()> { self.0 .call("SelectionWriteDone", &(session, serial, success)) .await } /// # Specifications /// /// See also [`SelectionRead`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Clipboard.html#org-freedesktop-portal-clipboard-selectionread). #[doc(alias = "SelectionRead")] pub async fn selection_read( &self, session: &Session<'_, RemoteDesktop<'_>>, mime_type: &str, ) -> Result { let fd = self .0 .call::("SelectionRead", &(session, mime_type)) .await?; Ok(fd) } /// Notifies the session that the clipboard selection has changed. /// # Specifications /// /// See also [`SelectionOwnerChanged`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Clipboard.html#org-freedesktop-portal-clipboard-selectionownerchanged). #[doc(alias = "SelectionOwnerChanged")] pub async fn receive_selection_owner_changed( &self, ) -> Result, SelectionOwnerChanged)>> { Ok(self .0 .signal::<(OwnedObjectPath, SelectionOwnerChanged)>("SelectionOwnerChanged") .await? .filter_map(|(p, o)| async move { Session::new(p).await.map(|s| (s, o)).ok() })) } /// # Specifications /// /// See also [`SelectionTransfer`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Clipboard.html#org-freedesktop-portal-clipboard-selectiontransfer). #[doc(alias = "SelectionTransfer")] pub async fn receive_selection_transfer( &self, ) -> Result, String, u32)>> { Ok(self .0 .signal::<(OwnedObjectPath, String, u32)>("SelectionTransfer") .await? .filter_map(|(p, mime_type, serial)| async move { Session::new(p) .await .map(|session| (session, mime_type, serial)) .ok() })) } } impl<'a> std::ops::Deref for Clipboard<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } ashpd-0.9.1/src/desktop/color.rs000064400000000000000000000027131046102023000146610ustar 00000000000000use crate::zvariant::{self, DeserializeDict, Type}; #[derive(DeserializeDict, Clone, Copy, PartialEq, Type, zvariant::Value, zvariant::OwnedValue)] /// A color as a RGB tuple. /// /// **Note** the values are normalized in the [0.0, 1.0] range. #[zvariant(signature = "dict")] pub struct Color { color: (f64, f64, f64), } impl Color { pub(crate) fn new(color: (f64, f64, f64)) -> Self { Self { color } } /// Red. pub fn red(&self) -> f64 { self.color.0 } /// Green. pub fn green(&self) -> f64 { self.color.1 } /// Blue. pub fn blue(&self) -> f64 { self.color.2 } } #[cfg(feature = "gtk4")] impl From for gtk4::gdk::RGBA { fn from(color: Color) -> Self { gtk4::gdk::RGBA::builder() .red(color.red() as f32) .green(color.green() as f32) .blue(color.blue() as f32) .build() } } impl std::fmt::Debug for Color { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Color") .field("red", &self.red()) .field("green", &self.green()) .field("blue", &self.blue()) .finish() } } impl std::fmt::Display for Color { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&format!( "({}, {}, {})", self.red(), self.green(), self.blue() )) } } ashpd-0.9.1/src/desktop/device.rs000064400000000000000000000103701046102023000150000ustar 00000000000000//! Request access to specific devices such as camera, speakers or microphone. //! //! **Note** This portal doesn't work for sandboxed applications. //! //! ### Examples //! //! Access a [`Device`] //! //! ```rust,no_run //! use ashpd::desktop::device::{Device, DeviceProxy}; //! //! async fn run() -> ashpd::Result<()> { //! let proxy = DeviceProxy::new().await?; //! proxy.access_device(6879, &[Device::Speakers]).await?; //! Ok(()) //! } //! ``` use std::{fmt, str::FromStr}; use serde::{Deserialize, Serialize}; use zbus::zvariant::{SerializeDict, Type}; use super::{HandleToken, Request}; use crate::{proxy::Proxy, Error}; #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`DeviceProxy::access_device`] request. #[zvariant(signature = "dict")] struct AccessDeviceOptions { /// A string that will be used as the last element of the handle. handle_token: HandleToken, } #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdDevice"))] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Type)] #[zvariant(signature = "s")] #[serde(rename_all = "lowercase")] /// The possible device to request access to. pub enum Device { /// A microphone. Microphone, /// Speakers. Speakers, /// A Camera. Camera, } impl fmt::Display for Device { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Microphone => write!(f, "Microphone"), Self::Speakers => write!(f, "Speakers"), Self::Camera => write!(f, "Camera"), } } } impl AsRef for Device { fn as_ref(&self) -> &str { match self { Self::Microphone => "Microphone", Self::Speakers => "Speakers", Self::Camera => "Camera", } } } impl From for &'static str { fn from(d: Device) -> Self { match d { Device::Microphone => "Microphone", Device::Speakers => "Speakers", Device::Camera => "Camera", } } } impl FromStr for Device { type Err = Error; fn from_str(s: &str) -> Result { match s { "Microphone" | "microphone" => Ok(Device::Microphone), "Speakers" | "speakers" => Ok(Device::Speakers), "Camera" | "camera" => Ok(Device::Camera), _ => Err(Error::ParseError("Failed to parse device, invalid value")), } } } /// The interface lets services ask if an application should get access to /// devices such as microphones, speakers or cameras. Not a portal in the strict /// sense, since the API is not directly accessible to applications inside the /// sandbox. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.Device`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Device.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.Device")] pub struct DeviceProxy<'a>(Proxy<'a>); impl<'a> DeviceProxy<'a> { /// Create a new instance of [`DeviceProxy`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.Device").await?; Ok(Self(proxy)) } /// Asks for access to a device. /// /// # Arguments /// /// * `pid` - The pid of the application on whose behalf the request is /// made. /// * `devices` - A list of devices to request access to. /// /// *Note* Asking for multiple devices at the same time may or may not be /// supported /// /// # Specifications /// /// See also [`AccessDevice`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Device.html#org-freedesktop-portal-device-accessdevice). #[doc(alias = "AccessDevice")] pub async fn access_device(&self, pid: u32, devices: &[Device]) -> Result, Error> { let options = AccessDeviceOptions::default(); self.0 .empty_request( &options.handle_token, "AccessDevice", &(pid, devices, &options), ) .await } } impl<'a> std::ops::Deref for DeviceProxy<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } ashpd-0.9.1/src/desktop/dynamic_launcher.rs000064400000000000000000000307651046102023000170600ustar 00000000000000//! Install launchers like Web Application from your browser or Steam. //! //! # Examples //! //! ```rust,no_run //! use std::io::Read; //! use ashpd::{ //! desktop::{ //! dynamic_launcher::{DynamicLauncherProxy, PrepareInstallOptions}, //! Icon, //! }, //! WindowIdentifier, //! }; //! //! async fn run() -> ashpd::Result<()> { //! let proxy = DynamicLauncherProxy::new().await?; //! //! let filename = "/home/bilalelmoussaoui/Projects/ashpd/ashpd-demo/data/icons/com.belmoussaoui.ashpd.demo.svg"; //! let mut f = std::fs::File::open(&filename).expect("no file found"); //! let metadata = std::fs::metadata(&filename).expect("unable to read metadata"); //! let mut buffer = vec![0; metadata.len() as usize]; //! f.read(&mut buffer).expect("buffer overflow"); //! //! let icon = Icon::Bytes(buffer); //! let response = proxy //! .prepare_install( //! &WindowIdentifier::default(), //! "SomeApp", //! icon, //! PrepareInstallOptions::default() //! ) //! .await? //! .response()?; //! let token = response.token(); //! //! //! // Name and Icon will be overwritten from what we provided above //! // Exec will be overridden to call `flatpak run our-app` if the application is sandboxed //! let desktop_entry = r#" //! [Desktop Entry] //! Comment=My Web App //! Type=Application //! "#; //! proxy //! .install(&token, "some_file.desktop", desktop_entry) //! .await?; //! //! proxy.uninstall("some_file.desktop").await?; //! Ok(()) //! } //! ``` use std::collections::HashMap; use enumflags2::{bitflags, BitFlags}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use zbus::zvariant::{self, DeserializeDict, OwnedValue, SerializeDict, Type, Value}; use super::{HandleToken, Icon, Request}; use crate::{proxy::Proxy, ActivationToken, Error, WindowIdentifier}; #[bitflags] #[derive(Default, Serialize_repr, Deserialize_repr, PartialEq, Eq, Debug, Copy, Clone, Type)] #[repr(u32)] #[doc(alias = "XdpLauncherType")] /// The type of the launcher. pub enum LauncherType { #[doc(alias = "XDP_LAUNCHER_APPLICATION")] #[default] /// A launcher that represents an application Application, #[doc(alias = "XDP_LAUNCHER_WEBAPP")] /// A launcher that represents a web application WebApplication, } #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdIconType"))] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Type)] #[zvariant(signature = "s")] #[serde(rename_all = "lowercase")] /// The icon format. pub enum IconType { /// PNG. Png, /// JPEG. Jpeg, /// SVG. Svg, } #[derive(Debug, Deserialize, Type)] #[zvariant(signature = "(vsu)")] /// The icon of the launcher. pub struct LauncherIcon(zvariant::OwnedValue, IconType, u32); impl LauncherIcon { /// The actual icon. pub fn icon(&self) -> Icon { Icon::try_from(&self.0).unwrap() } /// The icon type. pub fn type_(&self) -> IconType { self.1 } /// The icon size. pub fn size(&self) -> u32 { self.2 } } #[derive(Debug, Default, SerializeDict, Type)] #[zvariant(signature = "dict")] /// Options to pass to [`DynamicLauncherProxy::prepare_install`] pub struct PrepareInstallOptions { handle_token: HandleToken, modal: Option, launcher_type: LauncherType, target: Option, editable_name: Option, editable_icon: Option, } impl PrepareInstallOptions { /// Sets whether the dialog should be a modal. pub fn modal(mut self, modal: impl Into>) -> Self { self.modal = modal.into(); self } /// Sets the launcher type. pub fn launcher_type(mut self, launcher_type: LauncherType) -> Self { self.launcher_type = launcher_type; self } /// The URL for a [`LauncherType::WebApplication`] otherwise it is not /// needed. pub fn target<'a>(mut self, target: impl Into>) -> Self { self.target = target.into().map(ToOwned::to_owned); self } /// Sets whether the name should be editable. pub fn editable_name(mut self, editable_name: impl Into>) -> Self { self.editable_name = editable_name.into(); self } /// Sets whether the icon should be editable. pub fn editable_icon(mut self, editable_icon: impl Into>) -> Self { self.editable_icon = editable_icon.into(); self } } #[derive(DeserializeDict, Type)] #[zvariant(signature = "dict")] /// A response of [`DynamicLauncherProxy::prepare_install`] pub struct PrepareInstallResponse { name: String, icon: OwnedValue, token: String, } impl PrepareInstallResponse { /// The user defined name or a predefined one pub fn name(&self) -> &str { &self.name } /// A token to pass to [`DynamicLauncherProxy::install`] pub fn token(&self) -> &str { &self.token } /// The user selected icon or a predefined one pub fn icon(&self) -> Icon { let inner = self.icon.downcast_ref::().unwrap(); Icon::try_from(inner).unwrap() } } impl std::fmt::Debug for PrepareInstallResponse { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PrepareInstallResponse") .field("name", &self.name()) .field("icon", &self.icon()) .field("token", &self.token()) .finish() } } #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] /// Options to pass to [`DynamicLauncherProxy::launch`] pub struct LaunchOptions { activation_token: Option, } impl LaunchOptions { /// Sets the token that can be used to activate the chosen application. #[must_use] pub fn activation_token( mut self, activation_token: impl Into>, ) -> Self { self.activation_token = activation_token.into(); self } } #[derive(Debug)] /// Wrong type of [`crate::desktop::Icon`] was used. pub struct UnexpectedIconError; impl std::error::Error for UnexpectedIconError {} impl std::fmt::Display for UnexpectedIconError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("Unexpected icon type. Only Icon::Bytes is supported") } } /// The interface lets sandboxed applications install launchers like Web /// Application from your browser or Steam. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.DynamicLauncher`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.DynamicLauncher")] pub struct DynamicLauncherProxy<'a>(Proxy<'a>); impl<'a> DynamicLauncherProxy<'a> { /// Create a new instance of [`DynamicLauncherProxy`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.DynamicLauncher").await?; Ok(Self(proxy)) } /// *Note* Only `Icon::Bytes` is accepted. /// /// # Specifications /// /// See also [`PrepareInstall`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html#org-freedesktop-portal-dynamiclauncher-prepareinstall). #[doc(alias = "PrepareInstall")] #[doc(alias = "xdp_portal_dynamic_launcher_prepare_install")] #[doc(alias = "xdp_portal_dynamic_launcher_prepare_install_finish")] pub async fn prepare_install( &self, parent_window: &WindowIdentifier, name: &str, icon: Icon, options: PrepareInstallOptions, ) -> Result, Error> { if !icon.is_bytes() { return Err(UnexpectedIconError {}.into()); } self.0 .request( &options.handle_token, "PrepareInstall", &(parent_window, name, icon.as_value(), &options), ) .await } /// *Note* Only `Icon::Bytes` is accepted. /// /// # Specifications /// /// See also [`RequestInstallToken`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html#org-freedesktop-portal-dynamiclauncher-requestinstalltoken). #[doc(alias = "RequestInstallToken")] #[doc(alias = "xdp_portal_dynamic_launcher_request_install_token")] pub async fn request_install_token(&self, name: &str, icon: Icon) -> Result { if !icon.is_bytes() { return Err(UnexpectedIconError {}.into()); } // No supported options for now let options: HashMap<&str, zvariant::Value<'_>> = HashMap::new(); self.0 .call::("RequestInstallToken", &(name, icon.as_value(), options)) .await } /// # Specifications /// /// See also [`Install`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html#org-freedesktop-portal-dynamiclauncher-install). #[doc(alias = "Install")] #[doc(alias = "xdp_portal_dynamic_launcher_install")] pub async fn install( &self, token: &str, desktop_file_id: &str, desktop_entry: &str, ) -> Result<(), Error> { // No supported options for now let options: HashMap<&str, zvariant::Value<'_>> = HashMap::new(); self.0 .call::<()>("Install", &(token, desktop_file_id, desktop_entry, options)) .await } /// # Specifications /// /// See also [`Uninstall`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html#org-freedesktop-portal-dynamiclauncher-uninstall). #[doc(alias = "Uninstall")] #[doc(alias = "xdp_portal_dynamic_launcher_uninstall")] pub async fn uninstall(&self, desktop_file_id: &str) -> Result<(), Error> { // No supported options for now let options: HashMap<&str, zvariant::Value<'_>> = HashMap::new(); self.0 .call::<()>("Uninstall", &(desktop_file_id, options)) .await } /// # Specifications /// /// See also [`GetDesktopEntry`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html#org-freedesktop-portal-dynamiclauncher-getdesktopentry). #[doc(alias = "GetDesktopEntry")] #[doc(alias = "xdp_portal_dynamic_launcher_get_desktop_entry")] pub async fn desktop_entry(&self, desktop_file_id: &str) -> Result { self.0.call("GetDesktopEntry", &(desktop_file_id)).await } /// # Specifications /// /// See also [`GetIcon`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html#org-freedesktop-portal-dynamiclauncher-geticon). #[doc(alias = "GetIcon")] #[doc(alias = "xdp_portal_dynamic_launcher_get_icon")] pub async fn icon(&self, desktop_file_id: &str) -> Result { self.0.call("GetIcon", &(desktop_file_id)).await } /// # Specifications /// /// See also [`Launch`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html#org-freedesktop-portal-dynamiclauncher-launch). #[doc(alias = "Launch")] #[doc(alias = "xdp_portal_dynamic_launcher_launch")] pub async fn launch(&self, desktop_file_id: &str, options: LaunchOptions) -> Result<(), Error> { self.0.call("Launch", &(desktop_file_id, &options)).await } /// # Specifications /// /// See also [`SupportedLauncherTypes`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html#org-freedesktop-portal-dynamiclauncher-supportedlaunchertypes). #[doc(alias = "SupportedLauncherTypes")] pub async fn supported_launcher_types(&self) -> Result, Error> { self.0 .property::>("SupportedLauncherTypes") .await } } impl<'a> std::ops::Deref for DynamicLauncherProxy<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } #[cfg(test)] mod test { use super::*; #[test] fn test_icon_signature() { let signature = LauncherIcon::signature(); assert_eq!(signature.as_str(), "(vsu)"); let icon = vec![IconType::Png]; assert_eq!(serde_json::to_string(&icon).unwrap(), "[\"png\"]"); } } ashpd-0.9.1/src/desktop/email.rs000064400000000000000000000141151046102023000146310ustar 00000000000000//! Compose an email. //! //! Wrapper of the DBus interface: [`org.freedesktop.portal.Email`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Email.html). //! //! # Examples //! //! Compose an email //! //! ```rust,no_run //! use std::{fs::File, os::fd::OwnedFd}; //! //! use ashpd::desktop::email::EmailRequest; //! //! async fn run() -> ashpd::Result<()> { //! let file = File::open("/home/bilelmoussaoui/Downloads/adwaita-night.jpg").unwrap(); //! EmailRequest::default() //! .address("test@gmail.com") //! .subject("email subject") //! .body("the pre-filled email body") //! .attach(OwnedFd::from(file)) //! .send() //! .await; //! Ok(()) //! } //! ``` use std::os::fd::OwnedFd; use serde::Serialize; use zbus::zvariant::{self, SerializeDict, Type}; use super::{HandleToken, Request}; use crate::{proxy::Proxy, ActivationToken, Error, WindowIdentifier}; #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct EmailOptions { handle_token: HandleToken, address: Option, addresses: Option>, cc: Option>, bcc: Option>, subject: Option, body: Option, attachment_fds: Option>, activation_token: Option, } #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.Email")] struct EmailProxy<'a>(Proxy<'a>); impl<'a> EmailProxy<'a> { /// Create a new instance of [`EmailProxy`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.Email").await?; Ok(Self(proxy)) } /// Presents a window that lets the user compose an email. /// /// **Note** the default email client for the host will need to support /// `mailto:` URIs following RFC 2368. /// /// # Arguments /// /// * `identifier` - Identifier for the application window. /// * `options` - An [`EmailOptions`]. /// /// # Specifications /// /// See also [`ComposeEmail`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Email.html#org-freedesktop-portal-email-composeemail). #[doc(alias = "ComposeEmail")] pub async fn compose( &self, identifier: &WindowIdentifier, options: EmailOptions, ) -> Result, Error> { self.0 .empty_request( &options.handle_token, "ComposeEmail", &(&identifier, &options), ) .await } } impl<'a> std::ops::Deref for EmailProxy<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } #[derive(Debug, Default)] #[doc(alias = "xdp_portal_compose_email")] /// A [builder-pattern] type to compose an email. /// /// [builder-pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html pub struct EmailRequest { identifier: WindowIdentifier, options: EmailOptions, } impl EmailRequest { /// Sets a window identifier. #[must_use] pub fn identifier(mut self, identifier: impl Into>) -> Self { self.identifier = identifier.into().unwrap_or_default(); self } /// Sets the email address to send the email to. #[must_use] pub fn address<'a>(mut self, address: impl Into>) -> Self { self.options.address = address.into().map(ToOwned::to_owned); self } /// Sets a list of email addresses to send the email to. #[must_use] pub fn addresses, I: AsRef + Type + Serialize>( mut self, addresses: impl Into>, ) -> Self { self.options.addresses = addresses .into() .map(|a| a.into_iter().map(|s| s.as_ref().to_owned()).collect()); self } /// Sets a list of email addresses to BCC. #[must_use] pub fn bcc, I: AsRef + Type + Serialize>( mut self, bcc: impl Into>, ) -> Self { self.options.bcc = bcc .into() .map(|a| a.into_iter().map(|s| s.as_ref().to_owned()).collect()); self } /// Sets a list of email addresses to CC. #[must_use] pub fn cc, I: AsRef + Type + Serialize>( mut self, cc: impl Into>, ) -> Self { self.options.cc = cc .into() .map(|a| a.into_iter().map(|s| s.as_ref().to_owned()).collect()); self } /// Sets the email subject. #[must_use] pub fn subject<'a>(mut self, subject: impl Into>) -> Self { self.options.subject = subject.into().map(ToOwned::to_owned); self } /// Sets the email body. #[must_use] pub fn body<'a>(mut self, body: impl Into>) -> Self { self.options.body = body.into().map(ToOwned::to_owned); self } /// Attaches a file to the email. #[must_use] pub fn attach(mut self, attachment: OwnedFd) -> Self { self.add_attachment(attachment); self } // TODO Added in version 4 of the interface. /// Sets the token that can be used to activate the chosen application. #[must_use] pub fn activation_token( mut self, activation_token: impl Into>, ) -> Self { self.options.activation_token = activation_token.into(); self } /// A different variant of [`Self::attach`]. pub fn add_attachment(&mut self, attachment: OwnedFd) { let attachment = zvariant::OwnedFd::from(attachment); match self.options.attachment_fds { Some(ref mut attachments) => attachments.push(attachment), None => { self.options.attachment_fds.replace(vec![attachment]); } }; } /// Send the request. pub async fn send(self) -> Result, Error> { let proxy = EmailProxy::new().await?; proxy.compose(&self.identifier, self.options).await } } ashpd-0.9.1/src/desktop/file_chooser.rs000064400000000000000000000460131046102023000162050ustar 00000000000000//! The interface lets sandboxed applications ask the user for access to files //! outside the sandbox. The portal backend will present the user with a file //! chooser dialog. //! //! Wrapper of the DBus interface: [`org.freedesktop.portal.FileChooser`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileChooser.html). //! //! ### Examples //! //! #### Opening a file //! //! ```rust,no_run //! use ashpd::desktop::file_chooser::{Choice, FileFilter, SelectedFiles}; //! //! async fn run() -> ashpd::Result<()> { //! let files = SelectedFiles::open_file() //! .title("open a file to read") //! .accept_label("read") //! .modal(true) //! .multiple(true) //! .choice( //! Choice::new("encoding", "Encoding", "latin15") //! .insert("utf8", "Unicode (UTF-8)") //! .insert("latin15", "Western"), //! ) //! // A trick to have a checkbox //! .choice(Choice::boolean("re-encode", "Re-encode", false)) //! .filter(FileFilter::new("SVG Image").mimetype("image/svg+xml")) //! .send() //! .await? //! .response()?; //! //! println!("{:#?}", files); //! //! Ok(()) //! } //! ``` //! //! #### Ask to save a file //! //! ```rust,no_run //! use ashpd::desktop::file_chooser::{FileFilter, SelectedFiles}; //! //! async fn run() -> ashpd::Result<()> { //! let files = SelectedFiles::save_file() //! .title("open a file to write") //! .accept_label("write") //! .current_name("image.jpg") //! .modal(true) //! .filter(FileFilter::new("JPEG Image").glob("*.jpg")) //! .send() //! .await? //! .response()?; //! //! println!("{:#?}", files); //! //! Ok(()) //! } //! ``` //! //! #### Ask to save multiple files //! //! ```rust,no_run //! use ashpd::desktop::file_chooser::SelectedFiles; //! //! async fn run() -> ashpd::Result<()> { //! let files = SelectedFiles::save_files() //! .title("open files to write") //! .accept_label("write files") //! .modal(true) //! .current_folder("/home/bilelmoussaoui/Pictures")? //! .files(&["test.jpg", "awesome.png"])? //! .send() //! .await? //! .response()?; //! //! println!("{:#?}", files); //! //! Ok(()) //! } //! ``` use std::path::Path; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use zbus::zvariant::{DeserializeDict, SerializeDict, Type}; use super::{HandleToken, Request}; use crate::{proxy::Proxy, Error, FilePath, WindowIdentifier}; #[derive(Clone, Serialize, Deserialize, Type, Debug, PartialEq)] /// A file filter, to limit the available file choices to a mimetype or a glob /// pattern. pub struct FileFilter(String, Vec<(FilterType, String)>); #[derive(Clone, Serialize_repr, Deserialize_repr, Debug, Type, PartialEq)] #[repr(u32)] enum FilterType { GlobPattern = 0, MimeType = 1, } impl FilterType { /// Whether it is a mime type filter. fn is_mimetype(&self) -> bool { matches!(self, FilterType::MimeType) } /// Whether it is a glob pattern type filter. fn is_pattern(&self) -> bool { matches!(self, FilterType::GlobPattern) } } impl FileFilter { /// Create a new file filter /// /// # Arguments /// /// * `label` - user-visible name of the file filter. pub fn new(label: &str) -> Self { Self(label.to_owned(), vec![]) } /// Adds a mime type to the file filter. #[must_use] pub fn mimetype(mut self, mimetype: &str) -> Self { self.1.push((FilterType::MimeType, mimetype.to_owned())); self } /// Adds a glob pattern to the file filter. #[must_use] pub fn glob(mut self, pattern: &str) -> Self { self.1.push((FilterType::GlobPattern, pattern.to_owned())); self } } impl FileFilter { /// The label of the filter. pub fn label(&self) -> &str { &self.0 } /// List of mimetypes filters. pub fn mimetype_filters(&self) -> Vec<&str> { self.1 .iter() .filter_map(|(type_, string)| type_.is_mimetype().then_some(string.as_str())) .collect() } /// List of glob patterns filters. pub fn pattern_filters(&self) -> Vec<&str> { self.1 .iter() .filter_map(|(type_, string)| type_.is_pattern().then_some(string.as_str())) .collect() } } #[derive(Clone, Serialize, Deserialize, Type, Debug)] /// Presents the user with a choice to select from or as a checkbox. pub struct Choice(String, String, Vec<(String, String)>, String); impl Choice { /// Creates a checkbox choice. /// /// # Arguments /// /// * `id` - A unique identifier of the choice. /// * `label` - user-visible name of the choice. /// * `state` - the initial state value. pub fn boolean(id: &str, label: &str, state: bool) -> Self { Self::new(id, label, &state.to_string()) } /// Creates a new choice. /// /// # Arguments /// /// * `id` - A unique identifier of the choice. /// * `label` - user-visible name of the choice. /// * `initial_selection` - the initially selected value. pub fn new(id: &str, label: &str, initial_selection: &str) -> Self { Self( id.to_owned(), label.to_owned(), vec![], initial_selection.to_owned(), ) } /// Adds a (key, value) as a choice. #[must_use] pub fn insert(mut self, key: &str, value: &str) -> Self { self.2.push((key.to_owned(), value.to_owned())); self } /// The choice's unique id pub fn id(&self) -> &str { &self.0 } /// The user visible label of the choice. pub fn label(&self) -> &str { &self.1 } /// Pairs of choices. pub fn pairs(&self) -> Vec<(&str, &str)> { self.2 .iter() .map(|(x, y)| (x.as_str(), y.as_str())) .collect::>() } /// The initially selected value. pub fn initial_selection(&self) -> &str { &self.3 } } #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct OpenFileOptions { handle_token: HandleToken, accept_label: Option, modal: Option, multiple: Option, directory: Option, filters: Vec, current_filter: Option, choices: Option>, current_folder: Option, } #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct SaveFileOptions { handle_token: HandleToken, accept_label: Option, modal: Option, current_name: Option, current_folder: Option, current_file: Option, filters: Vec, current_filter: Option, choices: Option>, } #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct SaveFilesOptions { handle_token: HandleToken, accept_label: Option, modal: Option, choices: Option>, current_folder: Option, files: Option>, } #[derive(Debug, Type, DeserializeDict)] /// A response of [`OpenFileRequest`], [`SaveFileRequest`] or /// [`SaveFilesRequest`]. #[zvariant(signature = "dict")] pub struct SelectedFiles { uris: Vec, choices: Option>, } impl SelectedFiles { /// Start an open file request. pub fn open_file() -> OpenFileRequest { OpenFileRequest::default() } /// Start a save file request. pub fn save_file() -> SaveFileRequest { SaveFileRequest::default() } /// Start a save files request. pub fn save_files() -> SaveFilesRequest { SaveFilesRequest::default() } /// The selected files uris. pub fn uris(&self) -> &[url::Url] { self.uris.as_slice() } /// The selected value of each choice as a tuple of (key, value) pub fn choices(&self) -> &[(String, String)] { self.choices.as_deref().unwrap_or_default() } } #[doc(alias = "org.freedesktop.portal.FileChooser")] struct FileChooserProxy<'a>(Proxy<'a>); impl<'a> FileChooserProxy<'a> { /// Create a new instance of [`FileChooserProxy`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.FileChooser").await?; Ok(Self(proxy)) } pub async fn open_file( &self, identifier: &WindowIdentifier, title: &str, options: OpenFileOptions, ) -> Result, Error> { self.0 .request( &options.handle_token, "OpenFile", &(&identifier, title, &options), ) .await } pub async fn save_file( &self, identifier: &WindowIdentifier, title: &str, options: SaveFileOptions, ) -> Result, Error> { self.0 .request( &options.handle_token, "SaveFile", &(&identifier, title, &options), ) .await } pub async fn save_files( &self, identifier: &WindowIdentifier, title: &str, options: SaveFilesOptions, ) -> Result, Error> { self.0 .request( &options.handle_token, "SaveFiles", &(&identifier, title, &options), ) .await } } impl<'a> std::ops::Deref for FileChooserProxy<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } #[derive(Debug, Default)] #[doc(alias = "xdp_portal_open_file")] /// A [builder-pattern] type to open a file. /// /// [builder-pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html pub struct OpenFileRequest { identifier: WindowIdentifier, title: String, options: OpenFileOptions, } impl OpenFileRequest { #[must_use] /// Sets a window identifier. pub fn identifier(mut self, identifier: impl Into>) -> Self { self.identifier = identifier.into().unwrap_or_default(); self } /// Sets a title for the file chooser dialog. #[must_use] pub fn title<'a>(mut self, title: impl Into>) -> Self { self.title = title.into().map(ToOwned::to_owned).unwrap_or_default(); self } /// Sets a user-visible string to the "accept" button. #[must_use] pub fn accept_label<'a>(mut self, accept_label: impl Into>) -> Self { self.options.accept_label = accept_label.into().map(ToOwned::to_owned); self } /// Sets whether the dialog should be a modal. #[must_use] pub fn modal(mut self, modal: impl Into>) -> Self { self.options.modal = modal.into(); self } /// Sets whether to allow multiple files selection. #[must_use] pub fn multiple(mut self, multiple: impl Into>) -> Self { self.options.multiple = multiple.into(); self } /// Sets whether to select directories or not. #[must_use] pub fn directory(mut self, directory: impl Into>) -> Self { self.options.directory = directory.into(); self } /// Adds a files filter. #[must_use] pub fn filter(mut self, filter: FileFilter) -> Self { self.options.filters.push(filter); self } #[must_use] /// Adds a list of files filters. pub fn filters(mut self, filters: impl IntoIterator) -> Self { self.options.filters = filters.into_iter().collect(); self } /// Specifies the default filter. #[must_use] pub fn current_filter(mut self, current_filter: impl Into>) -> Self { self.options.current_filter = current_filter.into(); self } /// Adds a choice. #[must_use] pub fn choice(mut self, choice: Choice) -> Self { self.options .choices .get_or_insert_with(Vec::new) .push(choice); self } #[must_use] /// Adds a list of choices. pub fn choices(mut self, choices: impl IntoIterator) -> Self { self.options.choices = Some(choices.into_iter().collect()); self } /// Specifies the current folder path. pub fn current_folder>( mut self, current_folder: impl Into>, ) -> Result { self.options.current_folder = current_folder .into() .map(|c| FilePath::new(c)) .transpose()?; Ok(self) } /// Send the request. pub async fn send(self) -> Result, Error> { let proxy = FileChooserProxy::new().await?; proxy .open_file(&self.identifier, &self.title, self.options) .await } } #[derive(Debug, Default)] #[doc(alias = "xdp_portal_save_files")] /// A [builder-pattern] type to save multiple files. /// /// [builder-pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html pub struct SaveFilesRequest { identifier: WindowIdentifier, title: String, options: SaveFilesOptions, } impl SaveFilesRequest { #[must_use] /// Sets a window identifier. pub fn identifier(mut self, identifier: impl Into>) -> Self { self.identifier = identifier.into().unwrap_or_default(); self } /// Sets a title for the file chooser dialog. #[must_use] pub fn title<'a>(mut self, title: impl Into>) -> Self { self.title = title.into().map(ToOwned::to_owned).unwrap_or_default(); self } /// Sets a user-visible string to the "accept" button. #[must_use] pub fn accept_label<'a>(mut self, accept_label: impl Into>) -> Self { self.options.accept_label = accept_label.into().map(ToOwned::to_owned); self } /// Sets whether the dialog should be a modal. #[must_use] pub fn modal(mut self, modal: impl Into>) -> Self { self.options.modal = modal.into(); self } /// Adds a choice. #[must_use] pub fn choice(mut self, choice: Choice) -> Self { self.options .choices .get_or_insert_with(Vec::new) .push(choice); self } #[must_use] /// Adds a list of choices. pub fn choices(mut self, choices: impl IntoIterator) -> Self { self.options.choices = Some(choices.into_iter().collect()); self } /// Specifies the current folder path. pub fn current_folder>( mut self, current_folder: impl Into>, ) -> Result { self.options.current_folder = current_folder .into() .map(|c| FilePath::new(c)) .transpose()?; Ok(self) } /// Sets a list of files to save. pub fn files>>( mut self, files: impl Into>, ) -> Result { self.options.files = files .into() .map(|files| files.into_iter().map(|s| FilePath::new(s)).collect()) .transpose()?; Ok(self) } /// Send the request. pub async fn send(self) -> Result, Error> { let proxy = FileChooserProxy::new().await?; proxy .save_files(&self.identifier, &self.title, self.options) .await } } #[derive(Debug, Default)] #[doc(alias = "xdp_portal_save_file")] /// A [builder-pattern] type to save a file. /// /// [builder-pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html pub struct SaveFileRequest { identifier: WindowIdentifier, title: String, options: SaveFileOptions, } impl SaveFileRequest { #[must_use] /// Sets a window identifier. pub fn identifier(mut self, identifier: impl Into>) -> Self { self.identifier = identifier.into().unwrap_or_default(); self } /// Sets a title for the file chooser dialog. #[must_use] pub fn title<'a>(mut self, title: impl Into>) -> Self { self.title = title.into().map(ToOwned::to_owned).unwrap_or_default(); self } /// Sets a user-visible string to the "accept" button. #[must_use] pub fn accept_label<'a>(mut self, accept_label: impl Into>) -> Self { self.options.accept_label = accept_label.into().map(ToOwned::to_owned); self } /// Sets whether the dialog should be a modal. #[must_use] pub fn modal(mut self, modal: impl Into>) -> Self { self.options.modal = modal.into(); self } /// Sets the current file name. #[must_use] pub fn current_name<'a>(mut self, current_name: impl Into>) -> Self { self.options.current_name = current_name.into().map(ToOwned::to_owned); self } /// Sets the current folder. pub fn current_folder>( mut self, current_folder: impl Into>, ) -> Result { self.options.current_folder = current_folder .into() .map(|c| FilePath::new(c)) .transpose()?; Ok(self) } /// Sets the absolute path of the file. pub fn current_file>( mut self, current_file: impl Into>, ) -> Result { self.options.current_file = current_file.into().map(|c| FilePath::new(c)).transpose()?; Ok(self) } /// Adds a files filter. #[must_use] pub fn filter(mut self, filter: FileFilter) -> Self { self.options.filters.push(filter); self } #[must_use] /// Adds a list of files filters. pub fn filters(mut self, filters: impl IntoIterator) -> Self { self.options.filters = filters.into_iter().collect(); self } /// Sets the default filter. #[must_use] pub fn current_filter(mut self, current_filter: impl Into>) -> Self { self.options.current_filter = current_filter.into(); self } /// Adds a choice. #[must_use] pub fn choice(mut self, choice: Choice) -> Self { self.options .choices .get_or_insert_with(Vec::new) .push(choice); self } #[must_use] /// Adds a list of choices. pub fn choices(mut self, choices: impl IntoIterator) -> Self { self.options.choices = Some(choices.into_iter().collect()); self } /// Send the request. pub async fn send(self) -> Result, Error> { let proxy = FileChooserProxy::new().await?; proxy .save_file(&self.identifier, &self.title, self.options) .await } } ashpd-0.9.1/src/desktop/game_mode.rs000064400000000000000000000271771046102023000154730ustar 00000000000000//! # Examples //! //! ```rust,no_run //! use ashpd::desktop::game_mode::GameMode; //! //! async fn run() -> ashpd::Result<()> { //! let proxy = GameMode::new().await?; //! //! println!("{:#?}", proxy.register(246612).await?); //! println!("{:#?}", proxy.query_status(246612).await?); //! println!("{:#?}", proxy.unregister(246612).await?); //! println!("{:#?}", proxy.query_status(246612).await?); //! //! Ok(()) //! } //! ``` use std::{fmt::Debug, os::fd::BorrowedFd}; use serde_repr::Deserialize_repr; use zbus::zvariant::{Fd, Type}; use crate::{error::PortalError, proxy::Proxy, Error}; #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdGameModeStatus"))] #[derive(Deserialize_repr, PartialEq, Eq, Debug, Clone, Copy, Type)] #[repr(i32)] /// The status of the game mode. pub enum Status { /// GameMode is inactive. Inactive = 0, /// GameMode is active. Active = 1, /// GameMode is active and `pid` is registered. Registered = 2, /// The query failed inside GameMode. Rejected = -1, } #[derive(Deserialize_repr, PartialEq, Eq, Debug, Type)] #[repr(i32)] /// The status of a (un-)register game mode request. enum RegisterStatus { /// If the game was successfully (un-)registered. Success = 0, /// If the request was rejected by GameMode. Rejected = -1, } /// The interface lets sandboxed applications access GameMode from within the /// sandbox. /// /// It is analogous to the `com.feralinteractive.GameMode` interface and will /// proxy request there, but with additional permission checking and pid /// mapping. The latter is necessary in the case that sandbox has pid namespace /// isolation enabled. See the man page for pid_namespaces(7) for more details, /// but briefly, it means that the sandbox has its own process id namespace /// which is separated from the one on the host. Thus there will be two separate /// process ids (pids) within two different namespaces that both identify same /// process. One id from the pid namespace inside the sandbox and one id from /// the host pid namespace. Since GameMode expects pids from the host pid /// namespace but programs inside the sandbox can only know pids from the /// sandbox namespace, process ids need to be translated from the portal to the /// host namespace. The portal will do that transparently for all calls where /// this is necessary. /// /// Note: GameMode will monitor active clients, i.e. games and other programs /// that have successfully called [`GameMode::register`]. In the event /// that a client terminates without a call to the /// [`GameMode::unregister`] method, GameMode will automatically /// un-register the client. This might happen with a (small) delay. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.GameMode`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GameMode.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.GameMode")] pub struct GameMode<'a>(Proxy<'a>); impl<'a> GameMode<'a> { /// Create a new instance of [`GameMode`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.GameMode").await?; Ok(Self(proxy)) } /// Query the GameMode status for a process. /// If the caller is running inside a sandbox with pid namespace isolation, /// the pid will be translated to the respective host pid. /// /// # Arguments /// /// * `pid` - Process id to query the GameMode status of. /// /// # Specifications /// /// See also [`QueryStatus`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GameMode.html#org-freedesktop-portal-gamemode-querystatus). #[doc(alias = "QueryStatus")] pub async fn query_status(&self, pid: u32) -> Result { self.0.call("QueryStatus", &(pid)).await } /// Query the GameMode status for a process. /// /// # Arguments /// /// * `target` - Pid file descriptor to query the GameMode status of. /// * `requester` - Pid file descriptor of the process requesting the /// information. /// /// # Specifications /// /// See also [`QueryStatusByPIDFd`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GameMode.html#org-freedesktop-portal-gamemode-querystatusbypidfd). #[doc(alias = "QueryStatusByPIDFd")] pub async fn query_status_by_pidfd( &self, target: &BorrowedFd<'_>, requester: &BorrowedFd<'_>, ) -> Result { self.0 .call( "QueryStatusByPIDFd", &(Fd::from(target), Fd::from(requester)), ) .await } /// Query the GameMode status for a process. /// /// # Arguments /// /// * `target` - Process id to query the GameMode status of. /// * `requester` - Process id of the process requesting the information. /// /// # Specifications /// /// See also [`QueryStatusByPid`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GameMode.html#org-freedesktop-portal-gamemode-querystatusbypid). #[doc(alias = "QueryStatusByPid")] pub async fn query_status_by_pid(&self, target: u32, requester: u32) -> Result { self.0.call("QueryStatusByPid", &(target, requester)).await } /// Register a game with GameMode and thus request GameMode to be activated. /// If the caller is running inside a sandbox with pid namespace isolation, /// the pid will be translated to the respective host pid. See the general /// introduction for details. If the GameMode has already been requested /// for pid before, this call will fail. /// /// # Arguments /// /// * `pid` - Process id of the game to register. /// /// # Specifications /// /// See also [`RegisterGame`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GameMode.html#org-freedesktop-portal-gamemode-registergame). #[doc(alias = "RegisterGame")] pub async fn register(&self, pid: u32) -> Result<(), Error> { let status = self.0.call("RegisterGame", &(pid)).await?; match status { RegisterStatus::Success => Ok(()), RegisterStatus::Rejected => Err(Error::Portal(PortalError::Failed(format!( "Failed to register game for `{pid}`" )))), } } /// Register a game with GameMode. /// /// # Arguments /// /// * `target` - Process file descriptor of the game to register. /// * `requester` - Process file descriptor of the process requesting the /// registration. /// /// # Specifications /// /// See also [`RegisterGameByPIDFd`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GameMode.html#org-freedesktop-portal-gamemode-registergamebypidfd). #[doc(alias = "RegisterGameByPIDFd")] pub async fn register_by_pidfd( &self, target: &BorrowedFd<'_>, requester: &BorrowedFd<'_>, ) -> Result<(), Error> { let status = self .0 .call( "RegisterGameByPIDFd", &(Fd::from(target), Fd::from(requester)), ) .await?; match status { RegisterStatus::Success => Ok(()), RegisterStatus::Rejected => Err(Error::Portal(PortalError::Failed( "Failed to register by pidfd".to_string(), ))), } } /// Register a game with GameMode. /// /// # Arguments /// /// * `target` - Process id of the game to register. /// * `requester` - Process id of the process requesting the registration. /// /// # Specifications /// /// See also [`RegisterGameByPid`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GameMode.html#org-freedesktop-portal-gamemode-registergamebypid). #[doc(alias = "RegisterGameByPid")] pub async fn register_by_pid(&self, target: u32, requester: u32) -> Result<(), Error> { let status = self .0 .call("RegisterGameByPid", &(target, requester)) .await?; match status { RegisterStatus::Success => Ok(()), RegisterStatus::Rejected => Err(Error::Portal(PortalError::Failed(format!( "Failed to register by pid for target=`{target}` requester=`{requester}`" )))), } } /// Un-register a game from GameMode. /// if the call is successful and there are no other games or clients /// registered, GameMode will be deactivated. If the caller is running /// inside a sandbox with pid namespace isolation, the pid will be /// translated to the respective host pid. /// /// # Arguments /// /// * `pid` - Process id of the game to un-register. /// /// # Specifications /// /// See also [`UnregisterGame`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GameMode.html#org-freedesktop-portal-gamemode-unregistergame). #[doc(alias = "UnregisterGame")] pub async fn unregister(&self, pid: u32) -> Result<(), Error> { let status = self.0.call("UnregisterGame", &(pid)).await?; match status { RegisterStatus::Success => Ok(()), RegisterStatus::Rejected => Err(Error::Portal(PortalError::Failed(format!( "Failed to unregister for `{pid}`" )))), } } /// Un-register a game from GameMode. /// /// # Arguments /// /// * `target` - Pid file descriptor of the game to un-register. /// * `requester` - Pid file descriptor of the process requesting the /// un-registration. /// /// # Specifications /// /// See also [`UnregisterGameByPIDFd`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GameMode.html#org-freedesktop-portal-gamemode-unregistergamebypidfd). #[doc(alias = "UnregisterGameByPIDFd")] pub async fn unregister_by_pidfd( &self, target: &BorrowedFd<'_>, requester: &BorrowedFd<'_>, ) -> Result<(), Error> { let status = self .0 .call( "UnregisterGameByPIDFd", &(Fd::from(target), Fd::from(requester)), ) .await?; match status { RegisterStatus::Success => Ok(()), RegisterStatus::Rejected => Err(Error::Portal(PortalError::Failed( "Failed to unregister by pidfd`".to_string(), ))), } } /// Un-register a game from GameMode. /// /// # Arguments /// /// * `target` - Process id of the game to un-register. /// * `requester` - Process id of the process requesting the /// un-registration. /// /// # Specifications /// /// See also [`UnregisterGameByPid`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GameMode.html#org-freedesktop-portal-gamemode-unregistergamebypid). #[doc(alias = "UnregisterGameByPid")] pub async fn unregister_by_pid(&self, target: u32, requester: u32) -> Result<(), Error> { let status = self .0 .call("UnregisterGameByPid", &(target, requester)) .await?; match status { RegisterStatus::Success => Ok(()), RegisterStatus::Rejected => Err(Error::Portal(PortalError::Failed(format!( "Failed to unregister by pid for target=`{target}` requester=`{requester}`" )))), } } } impl<'a> std::ops::Deref for GameMode<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } ashpd-0.9.1/src/desktop/global_shortcuts.rs000064400000000000000000000245641046102023000171310ustar 00000000000000//! Register global shortcuts use std::{collections::HashMap, fmt::Debug, time::Duration}; use futures_util::{Stream, TryFutureExt}; use serde::{Deserialize, Serialize}; use zbus::zvariant::{ DeserializeDict, ObjectPath, OwnedObjectPath, OwnedValue, SerializeDict, Type, }; use super::{session::SessionPortal, HandleToken, Request, Session}; use crate::{desktop::session::CreateSessionResponse, proxy::Proxy, Error, WindowIdentifier}; #[derive(Clone, SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct NewShortcutInfo { /// User-readable text describing what the shortcut does. description: String, /// The preferred shortcut trigger, defined as described by the "shortcuts" /// XDG specification. Optional. preferred_trigger: Option, } /// Shortcut descriptor used to bind new shortcuts in /// [`GlobalShortcuts::bind_shortcuts`] #[derive(Clone, Serialize, Type, Debug)] pub struct NewShortcut(String, NewShortcutInfo); impl NewShortcut { /// Construct new shortcut pub fn new(id: impl Into, description: impl Into) -> Self { Self( id.into(), NewShortcutInfo { description: description.into(), preferred_trigger: None, }, ) } /// Sets the preferred shortcut trigger, defined as described by the /// "shortcuts" XDG specification. #[must_use] pub fn preferred_trigger<'a>(mut self, preferred_trigger: impl Into>) -> Self { self.1.preferred_trigger = preferred_trigger.into().map(ToOwned::to_owned); self } } #[derive(Clone, DeserializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct ShortcutInfo { /// User-readable text describing what the shortcut does. description: String, /// User-readable text describing how to trigger the shortcut for the client /// to render. trigger_description: String, } /// Struct that contains information about existing binded shortcut. /// /// If you need to create a new shortcuts, take a look at [`NewShortcut`] /// instead. #[derive(Clone, Deserialize, Type, Debug)] pub struct Shortcut(String, ShortcutInfo); impl Shortcut { /// Shortcut id pub fn id(&self) -> &str { &self.0 } /// User-readable text describing what the shortcut does. pub fn description(&self) -> &str { &self.1.description } /// User-readable text describing how to trigger the shortcut for the client /// to render. pub fn trigger_description(&self) -> &str { &self.1.trigger_description } } /// Specified options for a [`GlobalShortcuts::create_session`] request. #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct CreateSessionOptions { /// A string that will be used as the last element of the handle. handle_token: HandleToken, /// A string that will be used as the last element of the session handle. session_handle_token: HandleToken, } /// Specified options for a [`GlobalShortcuts::bind_shortcuts`] request. #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct BindShortcutsOptions { /// A string that will be used as the last element of the handle. handle_token: HandleToken, } /// A response to a [`GlobalShortcuts::bind_shortcuts`] request. #[derive(DeserializeDict, Type, Debug)] #[zvariant(signature = "dict")] pub struct BindShortcuts { shortcuts: Vec, } impl BindShortcuts { /// A list of shortcuts. pub fn shortcuts(&self) -> &[Shortcut] { &self.shortcuts } } /// Specified options for a [`GlobalShortcuts::list_shortcuts`] request. #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct ListShortcutsOptions { /// A string that will be used as the last element of the handle. handle_token: HandleToken, } /// A response to a [`GlobalShortcuts::list_shortcuts`] request. #[derive(DeserializeDict, Type, Debug)] #[zvariant(signature = "dict")] pub struct ListShortcuts { /// A list of shortcuts. shortcuts: Vec, } impl ListShortcuts { /// A list of shortcuts. pub fn shortcuts(&self) -> &[Shortcut] { &self.shortcuts } } /// Notifies about a shortcut becoming active. #[derive(Debug, Deserialize, Type)] pub struct Activated(OwnedObjectPath, String, u64, HashMap); impl Activated { /// Session that requested the shortcut. pub fn session_handle(&self) -> ObjectPath<'_> { self.0.as_ref() } /// The application-provided ID for the shortcut. pub fn shortcut_id(&self) -> &str { &self.1 } /// The timestamp, as seconds and microseconds since the Unix epoch. pub fn timestamp(&self) -> Duration { Duration::from_millis(self.2) } /// Optional information pub fn options(&self) -> &HashMap { &self.3 } } /// Notifies that a shortcut is not active anymore. #[derive(Debug, Deserialize, Type)] pub struct Deactivated(OwnedObjectPath, String, u64, HashMap); impl Deactivated { /// Session that requested the shortcut. pub fn session_handle(&self) -> ObjectPath<'_> { self.0.as_ref() } /// The application-provided ID for the shortcut. pub fn shortcut_id(&self) -> &str { &self.1 } /// The timestamp, as seconds and microseconds since the Unix epoch. pub fn timestamp(&self) -> Duration { Duration::from_millis(self.2) } /// Optional information pub fn options(&self) -> &HashMap { &self.3 } } /// Indicates that the information associated with some of the shortcuts has /// changed. #[derive(Debug, Deserialize, Type)] pub struct ShortcutsChanged(OwnedObjectPath, Vec); impl ShortcutsChanged { /// Session that requested the shortcut. pub fn session_handle(&self) -> ObjectPath<'_> { self.0.as_ref() } /// Shortcuts that have been registered. pub fn shortcuts(&self) -> &[Shortcut] { &self.1 } } /// Wrapper of the DBus interface: [`org.freedesktop.portal.GlobalShortcuts`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.GlobalShortcuts")] pub struct GlobalShortcuts<'a>(Proxy<'a>); impl<'a> GlobalShortcuts<'a> { /// Create a new instance of [`GlobalShortcuts`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.GlobalShortcuts").await?; Ok(Self(proxy)) } /// Create a global shortcuts session. /// /// # Specifications /// /// See also [`CreateSession`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-createsession). #[doc(alias = "CreateSession")] pub async fn create_session(&self) -> Result, Error> { let options = CreateSessionOptions::default(); let (request, proxy) = futures_util::try_join!( self.0 .request::(&options.handle_token, "CreateSession", &options) .into_future(), Session::from_unique_name(&options.session_handle_token).into_future(), )?; assert_eq!(proxy.path(), &request.response()?.session_handle.as_ref()); Ok(proxy) } /// Bind the shortcuts. /// /// # Specifications /// /// See also [`BindShortcuts`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-bindshortcuts). #[doc(alias = "BindShortcuts")] pub async fn bind_shortcuts( &self, session: &Session<'_, Self>, shortcuts: &[NewShortcut], parent_window: &WindowIdentifier, ) -> Result, Error> { let options = BindShortcutsOptions::default(); self.0 .request( &options.handle_token, "BindShortcuts", &(session, shortcuts, parent_window, &options), ) .await } /// Lists all shortcuts. /// /// # Specifications /// /// See also [`ListShortcuts`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-listshortcuts). #[doc(alias = "ListShortcuts")] pub async fn list_shortcuts( &self, session: &Session<'_, Self>, ) -> Result, Error> { let options = ListShortcutsOptions::default(); self.0 .request(&options.handle_token, "ListShortcuts", &(session, &options)) .await } /// Signal emitted when shortcut becomes active. /// /// # Specifications /// /// See also [`Activated`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-activated). #[doc(alias = "Activated")] pub async fn receive_activated(&self) -> Result, Error> { self.0.signal("Activated").await } /// Signal emitted when shortcut is not active anymore. /// /// # Specifications /// /// See also [`Deactivated`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-deactivated). #[doc(alias = "Deactivated")] pub async fn receive_deactivated(&self) -> Result, Error> { self.0.signal("Deactivated").await } /// Signal emitted when information associated with some of the shortcuts /// has changed. /// /// # Specifications /// /// See also [`ShortcutsChanged`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-shortcutschanged). #[doc(alias = "ShortcutsChanged")] pub async fn receive_shortcuts_changed( &self, ) -> Result, Error> { self.0.signal("ShortcutsChanged").await } } impl<'a> std::ops::Deref for GlobalShortcuts<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } impl SessionPortal for GlobalShortcuts<'_> {} ashpd-0.9.1/src/desktop/handle_token.rs000064400000000000000000000063401046102023000161760ustar 00000000000000use std::{ convert::TryFrom, fmt::{self, Debug, Display}, }; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use serde::{Deserialize, Serialize}; use zbus::{names::OwnedMemberName, zvariant::Type}; /// A handle token is a DBus Object Path element, specified in the /// [`Request`](crate::desktop::Request) or /// [`Session`](crate::desktop::Session) object path following this format /// `/org/freedesktop/portal/desktop/request/SENDER/TOKEN` where sender is the /// caller's unique name and token is the [`HandleToken`]. /// /// A valid object path element must only contain the ASCII characters /// `[A-Z][a-z][0-9]_` #[derive(Serialize, Type)] pub struct HandleToken(OwnedMemberName); impl Display for HandleToken { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) } } impl Debug for HandleToken { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("HandleToken") .field(&self.0.as_str()) .finish() } } impl Default for HandleToken { fn default() -> Self { let mut rng = thread_rng(); let token: String = (&mut rng) .sample_iter(Alphanumeric) .take(10) .map(char::from) .collect(); format!("ashpd_{token}").parse().unwrap() } } #[derive(Debug)] pub struct HandleInvalidCharacter(char); impl std::fmt::Display for HandleInvalidCharacter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!("Invalid Character {}", self.0)) } } impl std::error::Error for HandleInvalidCharacter {} impl std::str::FromStr for HandleToken { type Err = HandleInvalidCharacter; fn from_str(value: &str) -> Result { for char in value.chars() { if !char.is_ascii_alphanumeric() && char != '_' { return Err(HandleInvalidCharacter(char)); } } Ok(Self(OwnedMemberName::try_from(value).unwrap())) } } impl TryFrom for HandleToken { type Error = HandleInvalidCharacter; fn try_from(value: String) -> Result { value.parse::() } } impl TryFrom<&str> for HandleToken { type Error = HandleInvalidCharacter; fn try_from(value: &str) -> Result { value.parse::() } } impl<'de> Deserialize<'de> for HandleToken { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let token = String::deserialize(deserializer)?; token .parse::() .map_err(|err| serde::de::Error::custom(err.to_string())) } } #[cfg(test)] mod test { use std::str::FromStr; use super::HandleToken; #[test] fn handle_token() { assert!(HandleToken::from_str("token").is_ok()); let token = HandleToken::from_str("token2").unwrap(); assert_eq!(token.to_string(), "token2".to_string()); assert!(HandleToken::from_str("/test").is_err()); assert!(HandleToken::from_str("تجربة").is_err()); assert!(HandleToken::from_str("test_token").is_ok()); HandleToken::default(); // ensure we don't panic } } ashpd-0.9.1/src/desktop/icon.rs000064400000000000000000000165351046102023000145020ustar 00000000000000use serde::{ de, ser::{Serialize, SerializeTuple}, Deserialize, }; use zbus::zvariant::{self, OwnedValue, Type, Value}; use crate::Error; #[derive(Debug, PartialEq, Eq, Type)] #[zvariant(signature = "(sv)")] /// A representation of an icon. /// /// Used by both the Notification & Dynamic launcher portals. pub enum Icon { /// An icon URI. Uri(url::Url), /// A list of icon names. Names(Vec), /// Icon bytes. Bytes(Vec), } impl Icon { /// Create an icon from a list of names. pub fn with_names(names: impl IntoIterator) -> Self where N: ToString, { Self::Names(names.into_iter().map(|name| name.to_string()).collect()) } pub(crate) fn is_bytes(&self) -> bool { matches!(self, Self::Bytes(_)) } pub(crate) fn inner_bytes(&self) -> Value { match self { Self::Bytes(bytes) => { let mut array = zvariant::Array::new(u8::signature()); for byte in bytes.iter() { // Safe to unwrap because we are sure it is of the correct type array.append(Value::from(*byte)).unwrap(); } Value::from(array) } _ => panic!("Only bytes icons can be converted to a bytes variant"), } } pub(crate) fn as_value(&self) -> Value { let tuple = match self { Self::Uri(uri) => ("file", Value::from(uri.as_str())), Self::Names(names) => { let mut array = zvariant::Array::new(String::signature()); for name in names.iter() { // Safe to unwrap because we are sure it is of the correct type array.append(Value::from(name)).unwrap(); } ("themed", Value::from(array)) } Self::Bytes(_) => ("bytes", self.inner_bytes()), }; Value::new(tuple) } } impl Serialize for Icon { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { let mut tuple = serializer.serialize_tuple(2)?; match self { Self::Uri(uri) => { tuple.serialize_element("file")?; tuple.serialize_element(&Value::from(uri.as_str()))?; } Self::Names(names) => { tuple.serialize_element("themed")?; let mut array = zvariant::Array::new(String::signature()); for name in names.iter() { // Safe to unwrap because we are sure it is of the correct type array.append(Value::from(name)).unwrap(); } tuple.serialize_element(&Value::from(array))?; } Self::Bytes(_) => { tuple.serialize_element("bytes")?; tuple.serialize_element(&self.inner_bytes())?; } } tuple.end() } } impl<'de> Deserialize<'de> for Icon { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let (type_, data) = <(String, OwnedValue)>::deserialize(deserializer)?; match type_.as_str() { "file" => { let uri_str = data.downcast_ref::().unwrap(); let uri = url::Url::parse(uri_str.as_str()) .map_err(|_| de::Error::custom("Couldn't deserialize Icon of type 'file'"))?; Ok(Self::Uri(uri)) } "bytes" => { let array = data.downcast_ref::().unwrap(); let mut bytes = Vec::with_capacity(array.len()); for byte in array.inner() { bytes.push(byte.downcast_ref::().unwrap()); } Ok(Self::Bytes(bytes)) } "themed" => { let array = data.downcast_ref::().unwrap(); let mut names = Vec::with_capacity(array.len()); for value in array.inner() { let name = value.downcast_ref::().unwrap(); names.push(name.as_str().to_owned()); } Ok(Self::Names(names)) } _ => Err(de::Error::custom("Invalid Icon type")), } } } impl TryFrom<&OwnedValue> for Icon { type Error = crate::Error; fn try_from(value: &OwnedValue) -> Result { let structure = value.downcast_ref::().unwrap(); let fields = structure.fields(); let type_ = fields[0].downcast_ref::().unwrap(); match type_.as_str() { "file" => { let uri_str = fields[1] .downcast_ref::() .unwrap() .to_owned(); let uri = url::Url::parse(uri_str.as_str()) .map_err(|_| crate::Error::ParseError("Failed to parse uri"))?; Ok(Self::Uri(uri)) } "bytes" => { let array = fields[1].downcast_ref::().unwrap(); let mut bytes = Vec::with_capacity(array.len()); for byte in array.inner() { bytes.push(byte.downcast_ref::().unwrap()); } Ok(Self::Bytes(bytes)) } "themed" => { let array = fields[1].downcast_ref::().unwrap(); let mut names = Vec::with_capacity(array.len()); for value in array.inner() { let name = value.downcast_ref::().unwrap(); names.push(name.as_str().to_owned()); } Ok(Self::Names(names)) } _ => Err(Error::ParseError("Invalid Icon type")), } } } impl TryFrom for Icon { type Error = crate::Error; fn try_from(value: OwnedValue) -> Result { Self::try_from(&value) } } impl TryFrom> for Icon { type Error = crate::Error; fn try_from(value: Value<'_>) -> Result { Self::try_from(&value) } } impl TryFrom<&Value<'_>> for Icon { type Error = crate::Error; fn try_from(value: &Value<'_>) -> Result { Self::try_from(value.try_to_owned()?) } } #[cfg(test)] mod test { use zbus::zvariant::{serialized::Context, to_bytes, Endian}; use super::*; #[test] fn check_icon_signature() { assert_eq!(Icon::signature(), "(sv)"); } #[test] fn serialize_deserialize() { let ctxt = Context::new_dbus(Endian::Little, 0); let icon = Icon::with_names(["dialog-symbolic"]); let encoded = to_bytes(ctxt, &icon).unwrap(); let decoded: Icon = encoded.deserialize().unwrap().0; assert_eq!(decoded, icon); let icon = Icon::Uri(url::Url::parse("file://some/icon.png").unwrap()); let encoded = to_bytes(ctxt, &icon).unwrap(); let decoded: Icon = encoded.deserialize().unwrap().0; assert_eq!(decoded, icon); let icon = Icon::Bytes(vec![1, 0, 1, 0]); let encoded = to_bytes(ctxt, &icon).unwrap(); let decoded: Icon = encoded.deserialize().unwrap().0; assert_eq!(decoded, icon); } } ashpd-0.9.1/src/desktop/inhibit.rs000064400000000000000000000212501046102023000151660ustar 00000000000000//! # Examples //! //! How to inhibit logout/user switch //! //! ```rust,no_run //! use std::{thread, time}; //! //! use ashpd::{ //! desktop::inhibit::{InhibitFlags, InhibitProxy, SessionState}, //! WindowIdentifier, //! }; //! use futures_util::StreamExt; //! //! async fn run() -> ashpd::Result<()> { //! let proxy = InhibitProxy::new().await?; //! let identifier = WindowIdentifier::default(); //! //! let session = proxy.create_monitor(&identifier).await?; //! //! let state = proxy.receive_state_changed().await?.next().await.unwrap(); //! match state.session_state() { //! SessionState::Running => (), //! SessionState::QueryEnd => { //! proxy //! .inhibit( //! &identifier, //! InhibitFlags::Logout | InhibitFlags::UserSwitch, //! "please save the opened project first", //! ) //! .await?; //! thread::sleep(time::Duration::from_secs(1)); //! proxy.query_end_response(&session).await?; //! } //! SessionState::Ending => { //! println!("ending the session"); //! } //! } //! Ok(()) //! } //! ``` use enumflags2::{bitflags, BitFlags}; use futures_util::{Stream, TryFutureExt}; use serde::Deserialize; use serde_repr::{Deserialize_repr, Serialize_repr}; use zbus::zvariant::{DeserializeDict, ObjectPath, OwnedObjectPath, SerializeDict, Type}; use super::{session::SessionPortal, HandleToken, Request, Session}; use crate::{desktop::session::CreateSessionResponse, proxy::Proxy, Error, WindowIdentifier}; #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`InhibitProxy::create_monitor`] request. #[zvariant(signature = "dict")] struct CreateMonitorOptions { /// A string that will be used as the last element of the handle. handle_token: HandleToken, /// A string that will be used as the last element of the session handle. session_handle_token: HandleToken, } #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`InhibitProxy::inhibit`] request. #[zvariant(signature = "dict")] struct InhibitOptions { /// A string that will be used as the last element of the handle. handle_token: HandleToken, /// User-visible reason for the inhibition. reason: Option, } #[bitflags] #[derive(Serialize_repr, PartialEq, Eq, Debug, Clone, Copy, Type)] #[repr(u32)] #[doc(alias = "XdpInhibitFlags")] /// The actions to inhibit that can end the user's session pub enum InhibitFlags { #[doc(alias = "XDP_INHIBIT_FLAG_LOGOUT")] /// Logout. Logout, #[doc(alias = "XDP_INHIBIT_FLAG_USER_SWITCH")] /// User switch. UserSwitch, #[doc(alias = "XDP_INHIBIT_FLAG_SUSPEND")] /// Suspend. Suspend, #[doc(alias = "XDP_INHIBIT_FLAG_IDLE")] /// Idle. Idle, } #[derive(Debug, DeserializeDict, Type)] #[zvariant(signature = "dict")] struct State { #[zvariant(rename = "screensaver-active")] screensaver_active: bool, #[zvariant(rename = "session-state")] session_state: SessionState, } #[derive(Debug, Deserialize, Type)] /// A response received when the `state_changed` signal is received. pub struct InhibitState(OwnedObjectPath, State); impl InhibitState { /// The session triggered the state change pub fn session_handle(&self) -> ObjectPath<'_> { self.0.as_ref() } /// Whether screensaver is active or not. pub fn screensaver_active(&self) -> bool { self.1.screensaver_active } /// The session state. pub fn session_state(&self) -> SessionState { self.1.session_state } } #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdSessionState"))] #[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Debug, Clone, Copy, Type)] #[doc(alias = "XdpLoginSessionState")] #[repr(u32)] /// The current state of the user's session. pub enum SessionState { #[doc(alias = "XDP_LOGIN_SESSION_RUNNING")] /// Running. Running = 1, #[doc(alias = "XDP_LOGIN_SESSION_QUERY_END")] /// The user asked to end the session e.g logout. QueryEnd = 2, #[doc(alias = "XDP_LOGIN_SESSION_ENDING")] /// The session is ending. Ending = 3, } /// The interface lets sandboxed applications inhibit the user session from /// ending, suspending, idling or getting switched away. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.Inhibit`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Inhibit.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.Inhibit")] pub struct InhibitProxy<'a>(Proxy<'a>); impl<'a> InhibitProxy<'a> { /// Create a new instance of [`InhibitProxy`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.Inhibit").await?; Ok(Self(proxy)) } /// Creates a monitoring session. /// While this session is active, the caller will receive `state_changed` /// signals with updates on the session state. /// /// # Arguments /// /// * `identifier` - The application window identifier. /// /// # Specifications /// /// See also [`CreateMonitor`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Inhibit.html#org-freedesktop-portal-inhibit-createmonitor). #[doc(alias = "CreateMonitor")] #[doc(alias = "xdp_portal_session_monitor_start")] pub async fn create_monitor( &self, identifier: &WindowIdentifier, ) -> Result, Error> { let options = CreateMonitorOptions::default(); let body = &(&identifier, &options); let (monitor, proxy) = futures_util::try_join!( self.0 .request::(&options.handle_token, "CreateMonitor", body) .into_future(), Session::from_unique_name(&options.session_handle_token).into_future(), )?; assert_eq!(proxy.path(), &monitor.response()?.session_handle.as_ref()); Ok(proxy) } /// Inhibits a session status changes. /// /// # Arguments /// /// * `identifier` - The application window identifier. /// * `flags` - The flags determine what changes are inhibited. /// * `reason` - User-visible reason for the inhibition. /// /// # Specifications /// /// See also [`Inhibit`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Inhibit.html#org-freedesktop-portal-inhibit-inhibit). #[doc(alias = "Inhibit")] #[doc(alias = "xdp_portal_session_inhibit")] pub async fn inhibit( &self, identifier: &WindowIdentifier, flags: BitFlags, reason: &str, ) -> Result, Error> { let options = InhibitOptions { reason: Some(reason.to_owned()), handle_token: Default::default(), }; self.0 .empty_request( &options.handle_token, "Inhibit", &(&identifier, flags, &options), ) .await } /// Signal emitted when the session state changes. /// /// # Specifications /// /// See also [`StateChanged`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Inhibit.html#org-freedesktop-portal-inhibit-statechanged). #[doc(alias = "StateChanged")] #[doc(alias = "XdpPortal::session-state-changed")] pub async fn receive_state_changed(&self) -> Result, Error> { self.0.signal("StateChanged").await } /// Acknowledges that the caller received the "state_changed" signal. /// This method should be called within one second after receiving a /// [`receive_state_changed()`][`InhibitProxy::receive_state_changed`] /// signal with the [`SessionState::QueryEnd`] state. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_monitor()`][`InhibitProxy::create_monitor`]. /// /// # Specifications /// /// See also [`QueryEndResponse`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Inhibit.html#org-freedesktop-portal-inhibit-queryendresponse). #[doc(alias = "QueryEndResponse")] #[doc(alias = "xdp_portal_session_monitor_query_end_response")] pub async fn query_end_response(&self, session: &Session<'_, Self>) -> Result<(), Error> { self.0.call("QueryEndResponse", &(session)).await } } impl<'a> std::ops::Deref for InhibitProxy<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } impl SessionPortal for InhibitProxy<'_> {} ashpd-0.9.1/src/desktop/input_capture.rs000064400000000000000000000601731046102023000164310ustar 00000000000000//! # Examples //! //! ## A Note of Warning Regarding the GNOME Portal Implementation //! //! `xdg-desktop-portal-gnome` in version 46.0 has a //! [bug](https://gitlab.gnome.org/GNOME/xdg-desktop-portal-gnome/-/issues/126) //! that prevents reenabling a disabled session. //! //! Since changing barrier locations requires a session to be disabled, //! it is currently (as of GNOME 46) not possible to change barriers //! after a session has been enabled! //! //! (the [official documentation](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-setpointerbarriers) //! states that a //! [`InputCapture::set_pointer_barriers()`][set_pointer_barriers] //! request suspends the capture session but in reality the GNOME //! desktop portal enforces a //! [`InputCapture::disable()`][disable] //! request //! in order to use //! [`InputCapture::set_pointer_barriers()`][set_pointer_barriers] //! ) //! //! [set_pointer_barriers]: crate::desktop::input_capture::InputCapture::set_pointer_barriers //! [disable]: crate::desktop::input_capture::InputCapture::disable //! //! ## Retrieving an Ei File Descriptor //! //! The input capture portal is used to negotiate the input capture //! triggers and enable input capturing. //! //! Actual input capture events are then communicated over a unix //! stream using the [libei protocol](https://gitlab.freedesktop.org/libinput/libei). //! //! The lifetime of an ei file descriptor is bound by a capture session. //! //! ```rust,no_run //! use std::os::fd::AsRawFd; //! //! use ashpd::desktop::input_capture::{Capabilities, InputCapture}; //! //! async fn run() -> ashpd::Result<()> { //! let input_capture = InputCapture::new().await?; //! let (session, capabilities) = input_capture //! .create_session( //! &ashpd::WindowIdentifier::default(), //! Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen, //! ) //! .await?; //! eprintln!("capabilities: {capabilities}"); //! //! let eifd = input_capture.connect_to_eis(&session).await?; //! eprintln!("eifd: {}", eifd.as_raw_fd()); //! Ok(()) //! } //! ``` //! //! //! ## Selecting Pointer Barriers. //! //! Input capture is triggered through pointer barriers that are provided //! by the client. //! //! The provided barriers need to be positioned at the edges of outputs //! (monitors) and can be denied by the compositor for various reasons, such as //! wrong placement. //! //! For debugging why a barrier placement failed, the logs of the //! active portal implementation can be useful, e.g.: //! //! ```sh //! journalctl --user -xeu xdg-desktop-portal-gnome.service //! ``` //! //! The following example sets up barriers according to `pos` //! (either `Left`, `Right`, `Top` or `Bottom`). //! //! Note that barriers positioned between two monitors will be denied //! and returned in the `failed_barrier_ids` vector. //! //! ```rust,no_run //! use ashpd::desktop::input_capture::{Barrier, Capabilities, InputCapture}; //! //! #[allow(unused)] //! enum Position { //! Left, //! Right, //! Top, //! Bottom, //! } //! //! async fn run() -> ashpd::Result<()> { //! let input_capture = InputCapture::new().await?; //! let (session, _capabilities) = input_capture //! .create_session( //! &ashpd::WindowIdentifier::default(), //! Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen, //! ) //! .await?; //! //! let pos = Position::Left; //! let zones = input_capture.zones(&session).await?.response()?; //! eprintln!("zones: {zones:?}"); //! let barriers = zones //! .regions() //! .iter() //! .enumerate() //! .map(|(n, r)| { //! let id = n as u32; //! let (x, y) = (r.x_offset(), r.y_offset()); //! let (width, height) = (r.width() as i32, r.height() as i32); //! let barrier_pos = match pos { //! Position::Left => (x, y, x, y + height - 1), // start pos, end pos, inclusive //! Position::Right => (x + width, y, x + width, y + height - 1), //! Position::Top => (x, y, x + width - 1, y), //! Position::Bottom => (x, y + height, x + width - 1, y + height), //! }; //! Barrier::new(id, barrier_pos) //! }) //! .collect::>(); //! //! eprintln!("requested barriers: {barriers:?}"); //! //! let request = input_capture //! .set_pointer_barriers(&session, &barriers, zones.zone_set()) //! .await?; //! let response = request.response()?; //! let failed_barrier_ids = response.failed_barriers(); //! //! eprintln!("failed barrier ids: {:?}", failed_barrier_ids); //! //! Ok(()) //! } //! ``` //! //! ## Enabling Input Capture and Retrieving Captured Input Events. //! //! The following full example uses the [reis crate](https://docs.rs/reis/0.2.0/reis/) //! for libei communication. //! //! Input Capture can be released using ESC. //! //! ```rust,no_run //! use std::{collections::HashMap, os::unix::net::UnixStream, sync::OnceLock, time::Duration}; //! //! use ashpd::desktop::input_capture::{Barrier, Capabilities, InputCapture}; //! use futures_util::StreamExt; //! use reis::{ //! ei::{self, keyboard::KeyState}, //! event::{DeviceCapability, EiEvent, KeyboardKey}, //! tokio::{EiConvertEventStream, EiEventStream}, //! }; //! //! #[allow(unused)] //! enum Position { //! Left, //! Right, //! Top, //! Bottom, //! } //! //! static INTERFACES: OnceLock> = OnceLock::new(); //! //! async fn run() -> ashpd::Result<()> { //! let input_capture = InputCapture::new().await?; //! //! let (session, _cap) = input_capture //! .create_session( //! &ashpd::WindowIdentifier::default(), //! Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen, //! ) //! .await?; //! //! // connect to eis server //! let fd = input_capture.connect_to_eis(&session).await?; //! //! // create unix stream from fd //! let stream = UnixStream::from(fd); //! stream.set_nonblocking(true)?; //! //! // create ei context //! let context = ei::Context::new(stream)?; //! context.flush().unwrap(); //! //! let mut event_stream = EiEventStream::new(context.clone())?; //! let interfaces = INTERFACES.get_or_init(|| { //! HashMap::from([ //! ("ei_connection", 1), //! ("ei_callback", 1), //! ("ei_pingpong", 1), //! ("ei_seat", 1), //! ("ei_device", 2), //! ("ei_pointer", 1), //! ("ei_pointer_absolute", 1), //! ("ei_scroll", 1), //! ("ei_button", 1), //! ("ei_keyboard", 1), //! ("ei_touchscreen", 1), //! ]) //! }); //! let response = reis::tokio::ei_handshake( //! &mut event_stream, //! "ashpd-mre", //! ei::handshake::ContextType::Receiver, //! interfaces, //! ) //! .await //! .expect("ei handshake failed"); //! //! let mut event_stream = EiConvertEventStream::new(event_stream, response.serial); //! //! let pos = Position::Left; //! let zones = input_capture.zones(&session).await?.response()?; //! eprintln!("zones: {zones:?}"); //! let barriers = zones //! .regions() //! .iter() //! .enumerate() //! .map(|(n, r)| { //! let id = n as u32; //! let (x, y) = (r.x_offset(), r.y_offset()); //! let (width, height) = (r.width() as i32, r.height() as i32); //! let barrier_pos = match pos { //! Position::Left => (x, y, x, y + height - 1), // start pos, end pos, inclusive //! Position::Right => (x + width, y, x + width, y + height - 1), //! Position::Top => (x, y, x + width - 1, y), //! Position::Bottom => (x, y + height, x + width - 1, y + height), //! }; //! Barrier::new(id, barrier_pos) //! }) //! .collect::>(); //! //! eprintln!("requested barriers: {barriers:?}"); //! //! let request = input_capture //! .set_pointer_barriers(&session, &barriers, zones.zone_set()) //! .await?; //! let response = request.response()?; //! let failed_barrier_ids = response.failed_barriers(); //! //! eprintln!("failed barrier ids: {:?}", failed_barrier_ids); //! //! input_capture.enable(&session).await?; //! //! let mut activate_stream = input_capture.receive_activated().await?; //! //! loop { //! let activated = activate_stream.next().await.unwrap(); //! //! eprintln!("activated: {activated:?}"); //! loop { //! let ei_event = event_stream.next().await.unwrap().unwrap(); //! eprintln!("ei event: {ei_event:?}"); //! if let EiEvent::SeatAdded(seat_event) = &ei_event { //! seat_event.seat.bind_capabilities(&[ //! DeviceCapability::Pointer, //! DeviceCapability::PointerAbsolute, //! DeviceCapability::Keyboard, //! DeviceCapability::Touch, //! DeviceCapability::Scroll, //! DeviceCapability::Button, //! ]); //! context.flush().unwrap(); //! } //! if let EiEvent::DeviceAdded(_) = ei_event { //! // new device added -> restart capture //! break; //! }; //! if let EiEvent::KeyboardKey(KeyboardKey { key, state, .. }) = ei_event { //! if key == 1 && state == KeyState::Press { //! // esc pressed //! break; //! } //! } //! } //! //! eprintln!("releasing input capture"); //! let (x, y) = activated.cursor_position().unwrap(); //! let (x, y) = (x as f64, y as f64); //! let cursor_pos = match pos { //! Position::Left => (x + 1., y), //! Position::Right => (x - 1., y), //! Position::Top => (x, y - 1.), //! Position::Bottom => (x, y + 1.), //! }; //! input_capture //! .release(&session, activated.activation_id(), Some(cursor_pos)) //! .await?; //! } //! } //! ``` use std::{collections::HashMap, os::fd::OwnedFd}; use enumflags2::{bitflags, BitFlags}; use futures_util::{Stream, TryFutureExt}; use serde::Deserialize; use serde_repr::{Deserialize_repr, Serialize_repr}; use zbus::zvariant::{ self, DeserializeDict, ObjectPath, OwnedObjectPath, OwnedValue, SerializeDict, Type, Value, }; use super::{session::SessionPortal, HandleToken, Request, Session}; use crate::{proxy::Proxy, Error, WindowIdentifier}; #[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Debug, Copy, Clone, Type)] #[bitflags] #[repr(u32)] /// Supported capabilities pub enum Capabilities { /// Keyboard Keyboard, /// Pointer Pointer, /// Touchscreen Touchscreen, } #[derive(Debug, SerializeDict, Type)] #[zvariant(signature = "dict")] struct CreateSessionOptions { handle_token: HandleToken, session_handle_token: HandleToken, capabilities: BitFlags, } #[derive(Debug, DeserializeDict, Type)] #[zvariant(signature = "dict")] struct CreateSessionResponse { session_handle: OwnedObjectPath, capabilities: BitFlags, } #[derive(Default, Debug, SerializeDict, Type)] #[zvariant(signature = "dict")] struct GetZonesOptions { handle_token: HandleToken, } #[derive(Default, Debug, SerializeDict, Type)] #[zvariant(signature = "dict")] struct SetPointerBarriersOptions { handle_token: HandleToken, } #[derive(Default, Debug, SerializeDict, Type)] #[zvariant(signature = "dict")] struct EnableOptions {} #[derive(Default, Debug, SerializeDict, Type)] #[zvariant(signature = "dict")] struct DisableOptions {} #[derive(Default, Debug, SerializeDict, Type)] #[zvariant(signature = "dict")] struct ReleaseOptions { activation_id: Option, cursor_position: Option<(f64, f64)>, } /// Indicates that an input capturing session was disabled. #[derive(Debug, Deserialize, Type)] #[zvariant(signature = "(oa{sv})")] pub struct Disabled(OwnedObjectPath, HashMap); impl Disabled { /// Session that was disabled. pub fn session_handle(&self) -> ObjectPath<'_> { self.0.as_ref() } /// Optional information pub fn options(&self) -> &HashMap { &self.1 } } #[derive(Debug, DeserializeDict, Type)] #[zvariant(signature = "dict")] struct DeactivatedOptions { activation_id: Option, } /// Indicates that an input capturing session was deactivated. #[derive(Debug, Deserialize, Type)] #[zvariant(signature = "(oa{sv})")] pub struct Deactivated(OwnedObjectPath, DeactivatedOptions); impl Deactivated { /// Session that was deactivated. pub fn session_handle(&self) -> ObjectPath<'_> { self.0.as_ref() } /// The same activation_id number as in the corresponding "Activated" /// signal. pub fn activation_id(&self) -> Option { self.1.activation_id } } #[derive(Debug, DeserializeDict, Type)] #[zvariant(signature = "dict")] struct ActivatedOptions { activation_id: Option, cursor_position: Option<(f32, f32)>, barrier_id: Option, } /// Indicates that an input capturing session was activated. #[derive(Debug, Deserialize, Type)] #[zvariant(signature = "(oa{sv})")] pub struct Activated(OwnedObjectPath, ActivatedOptions); impl Activated { /// Session that was activated. pub fn session_handle(&self) -> ObjectPath<'_> { self.0.as_ref() } /// A number that can be used to synchronize with the transport-layer. pub fn activation_id(&self) -> Option { self.1.activation_id } /// The current cursor position in the same coordinate space as the zones. pub fn cursor_position(&self) -> Option<(f32, f32)> { self.1.cursor_position } /// The barrier id of the barrier that triggered pub fn barrier_id(&self) -> Option { self.1.barrier_id } } #[derive(Debug, DeserializeDict, Type)] #[zvariant(signature = "dict")] struct ZonesChangedOptions { zone_set: Option, } /// Indicates that zones available to this session changed. #[derive(Debug, Deserialize, Type)] #[zvariant(signature = "(oa{sv})")] pub struct ZonesChanged(OwnedObjectPath, ZonesChangedOptions); impl ZonesChanged { /// Session that was deactivated. pub fn session_handle(&self) -> ObjectPath<'_> { self.0.as_ref() } /// The zone_set ID of the invalidated zone. pub fn zone_set(&self) -> Option { self.1.zone_set } } /// A region of a [`Zones`]. #[derive(Debug, Clone, Copy, Deserialize, Type)] #[zvariant(signature = "(uuii)")] pub struct Region(u32, u32, i32, i32); impl Region { /// The width. pub fn width(self) -> u32 { self.0 } /// The height pub fn height(self) -> u32 { self.1 } /// The x offset. pub fn x_offset(self) -> i32 { self.2 } /// The y offset. pub fn y_offset(self) -> i32 { self.3 } } /// A response of [`InputCapture::zones`]. #[derive(Debug, Type, DeserializeDict)] #[zvariant(signature = "dict")] pub struct Zones { zones: Vec, zone_set: u32, } impl Zones { /// A list of regions. pub fn regions(&self) -> &[Region] { &self.zones } /// A unique ID to be used in [`InputCapture::set_pointer_barriers`]. pub fn zone_set(&self) -> u32 { self.zone_set } } /// A barrier ID. pub type BarrierID = u32; #[derive(Debug, SerializeDict, Type)] #[zvariant(signature = "dict")] /// Input Barrier. pub struct Barrier { barrier_id: BarrierID, position: (i32, i32, i32, i32), } impl Barrier { /// Create a new barrier. pub fn new(barrier_id: BarrierID, position: (i32, i32, i32, i32)) -> Self { Self { barrier_id, position, } } } /// A response to [`InputCapture::set_pointer_barriers`] #[derive(Debug, DeserializeDict, Type)] #[zvariant(signature = "dict")] pub struct SetPointerBarriersResponse { failed_barriers: Vec, } impl SetPointerBarriersResponse { /// List of pointer barriers that have been denied pub fn failed_barriers(&self) -> &[BarrierID] { &self.failed_barriers } } /// Wrapper of the DBus interface: [`org.freedesktop.portal.InputCapture`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html). #[doc(alias = "org.freedesktop.portal.InputCapture")] pub struct InputCapture<'a>(Proxy<'a>); impl<'a> InputCapture<'a> { /// Create a new instance of [`InputCapture`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.InputCapture").await?; Ok(Self(proxy)) } /// Create an input capture session. /// /// # Specifications /// /// See also [`CreateSession`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-createsession). pub async fn create_session( &self, parent_window: &WindowIdentifier, capabilities: BitFlags, ) -> Result<(Session<'_, Self>, BitFlags), Error> { let options = CreateSessionOptions { handle_token: Default::default(), session_handle_token: Default::default(), capabilities, }; let (request, proxy) = futures_util::try_join!( self.0 .request::( &options.handle_token, "CreateSession", (parent_window, &options) ) .into_future(), Session::from_unique_name(&options.session_handle_token).into_future(), )?; let response = request.response()?; assert_eq!(proxy.path(), &response.session_handle.as_ref()); Ok((proxy, response.capabilities)) } /// A set of currently available input zones for this session. /// /// # Specifications /// /// See also [`GetZones`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-getzones). #[doc(alias = "GetZones")] pub async fn zones(&self, session: &Session<'_, Self>) -> Result, Error> { let options = GetZonesOptions::default(); self.0 .request(&options.handle_token, "GetZones", (session, &options)) .await } /// Set up zero or more pointer barriers. /// /// # Specifications /// /// See also [`SetPointerBarriers`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-setpointerbarriers). #[doc(alias = "SetPointerBarriers")] pub async fn set_pointer_barriers( &self, session: &Session<'_, Self>, barriers: &[Barrier], zone_set: u32, ) -> Result, Error> { let options = SetPointerBarriersOptions::default(); self.0 .request( &options.handle_token, "SetPointerBarriers", &(session, &options, barriers, zone_set), ) .await } /// Enable input capturing. /// /// # Specifications /// /// See also [`Enable`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-enable). pub async fn enable(&self, session: &Session<'_, Self>) -> Result<(), Error> { let options = EnableOptions::default(); self.0.call("Enable", &(session, &options)).await } /// Disable input capturing. /// /// # Specifications /// /// See also [`Disable`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-disable). pub async fn disable(&self, session: &Session<'_, Self>) -> Result<(), Error> { let options = DisableOptions::default(); self.0.call("Disable", &(session, &options)).await } /// Release any ongoing input capture. /// /// # Specifications /// /// See also [`Release`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-release). pub async fn release( &self, session: &Session<'_, Self>, activation_id: Option, cursor_position: Option<(f64, f64)>, ) -> Result<(), Error> { let options = ReleaseOptions { activation_id, cursor_position, }; self.0.call("Release", &(session, &options)).await } /// Connect to EIS. /// /// # Specifications /// /// See also [`ConnectToEIS`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-connecttoeis). #[doc(alias = "ConnectToEIS")] pub async fn connect_to_eis(&self, session: &Session<'_, Self>) -> Result { // `ConnectToEIS` doesn't take any options for now let options: HashMap<&str, Value<'_>> = HashMap::new(); let fd = self .0 .call::("ConnectToEIS", &(session, options)) .await?; Ok(fd.into()) } /// Signal emitted when the application will no longer receive captured /// events. /// /// # Specifications /// /// See also [`Disabled`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-disabled). #[doc(alias = "Disabled")] pub async fn receive_disabled(&self) -> Result, Error> { self.0.signal("Disabled").await } /// Signal emitted when input capture starts and /// input events are about to be sent to the application. /// /// # Specifications /// /// See also [`Activated`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-activated). #[doc(alias = "Activated")] pub async fn receive_activated(&self) -> Result, Error> { self.0.signal("Activated").await } /// Signal emitted when input capture stopped and input events /// are no longer sent to the application. /// /// # Specifications /// /// See also [`Deactivated`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-deactivated). #[doc(alias = "Deactivated")] pub async fn receive_deactivated(&self) -> Result, Error> { self.0.signal("Deactivated").await } /// Signal emitted when the set of zones available to this session change. /// /// # Specifications /// /// See also [`ZonesChanged`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-zoneschanged). #[doc(alias = "ZonesChanged")] pub async fn receive_zones_changed(&self) -> Result, Error> { self.0.signal("ZonesChanged").await } /// Supported capabilities. /// /// # Specifications /// /// See also [`SupportedCapabilities`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-supportedcapabilities). #[doc(alias = "SupportedCapabilities")] pub async fn supported_capabilities(&self) -> Result, Error> { self.0.property("SupportedCapabilities").await } } impl<'a> std::ops::Deref for InputCapture<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } impl SessionPortal for InputCapture<'_> {} ashpd-0.9.1/src/desktop/location.rs000064400000000000000000000223651046102023000153600ustar 00000000000000//! # Examples //! //! ```rust,no_run //! use ashpd::{ //! desktop::location::{Accuracy, LocationProxy}, //! WindowIdentifier, //! }; //! use futures_util::{FutureExt, StreamExt}; //! //! async fn run() -> ashpd::Result<()> { //! let proxy = LocationProxy::new().await?; //! let identifier = WindowIdentifier::default(); //! let session = proxy //! .create_session(None, None, Some(Accuracy::Street)) //! .await?; //! let mut stream = proxy.receive_location_updated().await?; //! let (_, location) = futures_util::join!( //! proxy //! .start(&session, &identifier) //! .map(|e| e.expect("Couldn't start session")), //! stream.next().map(|e| e.expect("Stream is exhausted")) //! ); //! println!("{}", location.accuracy()); //! println!("{}", location.longitude()); //! println!("{}", location.latitude()); //! session.close().await?; //! Ok(()) //! } //! ``` use std::fmt::Debug; use futures_util::{Stream, TryFutureExt}; use serde::Deserialize; use serde_repr::Serialize_repr; use zbus::zvariant::{DeserializeDict, ObjectPath, OwnedObjectPath, SerializeDict, Type}; use super::{session::SessionPortal, HandleToken, Request, Session}; use crate::{proxy::Proxy, Error, WindowIdentifier}; #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdLocationAccuracy"))] #[derive(Serialize_repr, PartialEq, Eq, Clone, Copy, Debug, Type)] #[doc(alias = "XdpLocationAccuracy")] #[repr(u32)] /// The accuracy of the location. pub enum Accuracy { #[doc(alias = "XDP_LOCATION_ACCURACY_NONE")] /// None. None = 0, #[doc(alias = "XDP_LOCATION_ACCURACY_COUNTRY")] /// Country. Country = 1, #[doc(alias = "XDP_LOCATION_ACCURACY_CITY")] /// City. City = 2, #[doc(alias = "XDP_LOCATION_ACCURACY_NEIGHBORHOOD")] /// Neighborhood. Neighborhood = 3, #[doc(alias = "XDP_LOCATION_ACCURACY_STREET")] /// Street. Street = 4, #[doc(alias = "XDP_LOCATION_ACCURACY_EXACT")] /// The exact location. Exact = 5, } #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`LocationProxy::create_session`] request. #[zvariant(signature = "dict")] struct CreateSessionOptions { /// A string that will be used as the last element of the session handle. session_handle_token: HandleToken, /// Distance threshold in meters. Default is 0. #[zvariant(rename = "distance-threshold")] distance_threshold: Option, /// Time threshold in seconds. Default is 0. #[zvariant(rename = "time-threshold")] time_threshold: Option, /// Requested accuracy. Default is `Accuracy::Exact`. accuracy: Option, } #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`LocationProxy::start`] request. #[zvariant(signature = "dict")] struct SessionStartOptions { /// A string that will be used as the last element of the handle. handle_token: HandleToken, } #[derive(Deserialize, Type)] /// The response received on a `location_updated` signal. pub struct Location(OwnedObjectPath, LocationInner); impl Location { /// The associated session. pub fn session_handle(&self) -> ObjectPath<'_> { self.0.as_ref() } /// The accuracy, in meters. pub fn accuracy(&self) -> f64 { self.1.accuracy } /// The altitude, in meters. pub fn altitude(&self) -> Option { if self.1.altitude == -f64::MAX { None } else { Some(self.1.altitude) } } /// The speed, in meters per second. pub fn speed(&self) -> Option { if self.1.speed == -1f64 { None } else { Some(self.1.speed) } } /// The heading, in degrees, going clockwise. North 0, East 90, South 180, /// West 270. pub fn heading(&self) -> Option { if self.1.heading == -1f64 { None } else { Some(self.1.heading) } } /// The location description. pub fn description(&self) -> Option<&str> { if self.1.description.is_empty() { None } else { Some(&self.1.description) } } /// The latitude, in degrees. pub fn latitude(&self) -> f64 { self.1.latitude } /// The longitude, in degrees. pub fn longitude(&self) -> f64 { self.1.longitude } /// The timestamp when the location was retrieved. pub fn timestamp(&self) -> std::time::Duration { std::time::Duration::from_secs(self.1.timestamp.0) } } impl Debug for Location { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Location") .field("accuracy", &self.accuracy()) .field("altitude", &self.altitude()) .field("speed", &self.speed()) .field("heading", &self.heading()) .field("description", &self.description()) .field("latitude", &self.latitude()) .field("longitude", &self.longitude()) .field("timestamp", &self.timestamp()) .finish() } } #[derive(Debug, SerializeDict, DeserializeDict, Type)] #[zvariant(signature = "dict")] struct LocationInner { #[zvariant(rename = "Accuracy")] accuracy: f64, #[zvariant(rename = "Altitude")] altitude: f64, #[zvariant(rename = "Speed")] speed: f64, #[zvariant(rename = "Heading")] heading: f64, #[zvariant(rename = "Description")] description: String, #[zvariant(rename = "Latitude")] latitude: f64, #[zvariant(rename = "Longitude")] longitude: f64, #[zvariant(rename = "Timestamp")] timestamp: (u64, u64), } /// The interface lets sandboxed applications query basic information about the /// location. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.Location`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Location.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.Location")] pub struct LocationProxy<'a>(Proxy<'a>); impl<'a> LocationProxy<'a> { /// Create a new instance of [`LocationProxy`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.Location").await?; Ok(Self(proxy)) } /// Signal emitted when the user location is updated. /// /// # Specifications /// /// See also [`LocationUpdated`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Location.html#org-freedesktop-portal-location-locationupdated). #[doc(alias = "LocationUpdated")] #[doc(alias = "XdpPortal::location-updated")] pub async fn receive_location_updated(&self) -> Result, Error> { self.0.signal("LocationUpdated").await } /// Create a location session. /// /// # Arguments /// /// * `distance_threshold` - Sets the distance threshold in meters, default /// to `0`. /// * `time_threshold` - Sets the time threshold in seconds, default to `0`. /// * `accuracy` - Sets the location accuracy, default to /// [`Accuracy::Exact`]. /// /// # Specifications /// /// See also [`CreateSession`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Location.html#org-freedesktop-portal-location-createsession). #[doc(alias = "CreateSession")] pub async fn create_session( &self, distance_threshold: Option, time_threshold: Option, accuracy: Option, ) -> Result, Error> { let options = CreateSessionOptions { distance_threshold, time_threshold, accuracy, ..Default::default() }; let (path, proxy) = futures_util::try_join!( self.0 .call::("CreateSession", &(options)) .into_future(), Session::from_unique_name(&options.session_handle_token).into_future(), )?; assert_eq!(proxy.path(), &path.into_inner()); Ok(proxy) } /// Start the location session. /// An application can only attempt start a session once. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`LocationProxy::create_session`]. /// * `identifier` - Identifier for the application window. /// /// # Specifications /// /// See also [`Start`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Location.html#org-freedesktop-portal-location-start). #[doc(alias = "Start")] #[doc(alias = "xdp_portal_location_monitor_start")] pub async fn start( &self, session: &Session<'_, Self>, identifier: &WindowIdentifier, ) -> Result, Error> { let options = SessionStartOptions::default(); self.0 .empty_request( &options.handle_token, "Start", &(session, &identifier, &options), ) .await } } impl SessionPortal for LocationProxy<'_> {} impl<'a> std::ops::Deref for LocationProxy<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } ashpd-0.9.1/src/desktop/memory_monitor.rs000064400000000000000000000037171046102023000166270ustar 00000000000000//! # Examples //! //! ```rust,no_run //! use ashpd::desktop::memory_monitor::MemoryMonitor; //! use futures_util::StreamExt; //! //! async fn run() -> ashpd::Result<()> { //! let proxy = MemoryMonitor::new().await?; //! let level = proxy //! .receive_low_memory_warning() //! .await? //! .next() //! .await //! .expect("Stream exhausted"); //! println!("{}", level); //! Ok(()) //! } //! ``` use futures_util::Stream; use crate::{proxy::Proxy, Error}; /// The interface provides information about low system memory to sandboxed /// applications. /// /// It is not a portal in the strict sense, since it does not involve user /// interaction. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.MemoryMonitor`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.MemoryMonitor.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.MemoryMonitor")] pub struct MemoryMonitor<'a>(Proxy<'a>); impl<'a> MemoryMonitor<'a> { /// Create a new instance of [`MemoryMonitor`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.MemoryMonitor").await?; Ok(Self(proxy)) } /// Signal emitted when a particular low memory situation happens /// with 0 being the lowest level of memory availability warning, and 255 /// being the highest. /// /// # Specifications /// /// See also [`LowMemoryWarning`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.MemoryMonitor.html#org-freedesktop-portal-memorymonitor-lowmemorywarning). #[doc(alias = "LowMemoryWarning")] pub async fn receive_low_memory_warning(&self) -> Result, Error> { self.0.signal("LowMemoryWarning").await } } impl<'a> std::ops::Deref for MemoryMonitor<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } ashpd-0.9.1/src/desktop/mod.rs000064400000000000000000000043651046102023000143270ustar 00000000000000mod handle_token; pub(crate) mod request; mod session; pub(crate) use self::handle_token::HandleToken; pub use self::{ request::{Request, Response, ResponseError}, session::Session, }; mod color; pub use color::Color; mod icon; pub use icon::Icon; pub mod account; pub mod background; pub mod camera; pub mod clipboard; #[deprecated = "The portal does not serve any purpose as nothing really can make use of it as is."] pub mod device; pub mod dynamic_launcher; pub mod email; /// Open/save file(s) chooser. pub mod file_chooser; /// Enable/disable/query the status of Game Mode. pub mod game_mode; /// Register global shortcuts pub mod global_shortcuts; /// Inhibit the session from being restarted or the user from logging out. pub mod inhibit; /// Capture input events from physical or logical devices. pub mod input_capture; /// Query the user's GPS location. pub mod location; /// Monitor memory level. pub mod memory_monitor; /// Check the status of the network on a user's machine. pub mod network_monitor; /// Send/withdraw notifications. pub mod notification; pub mod open_uri; /// Power profile monitoring. pub mod power_profile_monitor; /// Print a document. pub mod print; /// Proxy information. pub mod proxy_resolver; pub mod realtime; /// Start a remote desktop session and interact with it. pub mod remote_desktop; pub mod screencast; pub mod screenshot; /// Retrieve a per-application secret used to encrypt confidential data inside /// the sandbox. pub mod secret; /// Read & listen to system settings changes. pub mod settings; pub mod trash; pub mod wallpaper; #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdPersistMode"))] #[derive( Default, serde_repr::Serialize_repr, PartialEq, Eq, Debug, Copy, Clone, zbus::zvariant::Type, )] #[doc(alias = "XdpPersistMode")] #[repr(u32)] /// Persistence mode for a screencast or remote desktop session. pub enum PersistMode { #[doc(alias = "XDP_PERSIST_MODE_NONE")] #[default] /// Do not persist. DoNot = 0, #[doc(alias = "XDP_PERSIST_MODE_TRANSIENT")] /// Persist while the application is running. Application = 1, #[doc(alias = "XDP_PERSIST_MODE_PERSISTENT")] /// Persist until explicitly revoked. ExplicitlyRevoked = 2, } ashpd-0.9.1/src/desktop/network_monitor.rs000064400000000000000000000172351046102023000170100ustar 00000000000000//! **Note** This portal doesn't work for sandboxed applications. //! # Examples //! //! ```rust,no_run //! 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(()) //! } //! ``` use std::fmt; use futures_util::Stream; use serde_repr::Deserialize_repr; use zbus::zvariant::{DeserializeDict, Type}; use crate::{proxy::Proxy, Error}; #[derive(DeserializeDict, Type, Debug)] /// The network status, composed of the availability, metered & connectivity #[zvariant(signature = "dict")] pub struct NetworkStatus { /// Whether the network is considered available. available: bool, /// Whether the network is considered metered. metered: bool, /// More detailed information about the host's network connectivity connectivity: Connectivity, } impl NetworkStatus { /// Returns whether the network is considered available. pub fn is_available(&self) -> bool { self.available } /// Returns whether the network is considered metered. pub fn is_metered(&self) -> bool { self.metered } /// Returns more detailed information about the host's network connectivity. pub fn connectivity(&self) -> Connectivity { self.connectivity } } #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdConnectivity"))] #[derive(Deserialize_repr, PartialEq, Eq, Debug, Clone, Copy, Type)] #[repr(u32)] /// Host's network activity pub enum Connectivity { /// The host is not configured with a route to the internet. Local = 1, /// The host is connected to a network, but can't reach the full internet. Limited = 2, /// The host is behind a captive portal and cannot reach the full internet. CaptivePortal = 3, /// The host connected to a network, and can reach the full internet. FullNetwork = 4, } impl fmt::Display for Connectivity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let connectivity = match self { Self::Local => "local", Self::Limited => "limited", Self::CaptivePortal => "captive portal", Self::FullNetwork => "full network", }; f.write_str(connectivity) } } /// The interface provides network status information to sandboxed applications. /// /// It is not a portal in the strict sense, since it does not involve user /// interaction. Applications are expected to use this interface indirectly, /// via a library API such as the GLib [`gio::NetworkMonitor`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/gio/struct.NetworkMonitor.html) interface. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.NetworkMonitor`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.NetworkMonitor.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.NetworkMonitor")] pub struct NetworkMonitor<'a>(Proxy<'a>); impl<'a> NetworkMonitor<'a> { /// Create a new instance of [`NetworkMonitor`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.NetworkMonitor").await?; Ok(Self(proxy)) } /// Returns whether the given hostname is believed to be reachable. /// /// # Arguments /// /// * `hostname` - The hostname to reach. /// * `port` - The port to reach. /// /// # Required version /// /// The method requires the 3nd version implementation of the portal and /// would fail with [`Error::RequiresVersion`] otherwise. /// /// # Specifications /// /// See also [`CanReach`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.NetworkMonitor.html#org-freedesktop-portal-networkmonitor-canreach). #[doc(alias = "CanReach")] pub async fn can_reach(&self, hostname: &str, port: u32) -> Result { self.0 .call_versioned("CanReach", &(hostname, port), 3) .await } /// Returns whether the network is considered available. /// That is, whether the system as a default route for at least one of IPv4 /// or IPv6. /// /// # Required version /// /// The method requires the 2nd version implementation of the portal and /// would fail with [`Error::RequiresVersion`] otherwise. /// /// # Specifications /// /// See also [`GetAvailable`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.NetworkMonitor.html#org-freedesktop-portal-networkmonitor-getavailable). #[doc(alias = "GetAvailable")] #[doc(alias = "get_available")] pub async fn is_available(&self) -> Result { self.0.call_versioned("GetAvailable", &(), 2).await } /// Returns more detailed information about the host's network connectivity. /// /// # Required version /// /// The method requires the 2nd version implementation of the portal and /// would fail with [`Error::RequiresVersion`] otherwise. /// /// # Specifications /// /// See also [`GetConnectivity`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.NetworkMonitor.html#org-freedesktop-portal-networkmonitor-getconnectivity). #[doc(alias = "GetConnectivity")] #[doc(alias = "get_connectivity")] pub async fn connectivity(&self) -> Result { self.0.call_versioned("GetConnectivity", &(), 2).await } /// Returns whether the network is considered metered. /// That is, whether the system as traffic flowing through the default /// connection that is subject to limitations by service providers. /// /// # Required version /// /// The method requires the 2nd version implementation of the portal and /// would fail with [`Error::RequiresVersion`] otherwise. /// /// # Specifications /// /// See also [`GetMetered`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.NetworkMonitor.html#org-freedesktop-portal-networkmonitor-getmetered). #[doc(alias = "GetMetered")] #[doc(alias = "get_metered")] pub async fn is_metered(&self) -> Result { self.0.call_versioned("GetMetered", &(), 2).await } /// Returns the three values all at once. /// /// # Required version /// /// The method requires the 3nd version implementation of the portal and /// would fail with [`Error::RequiresVersion`] otherwise. /// /// # Specifications /// /// See also [`GetStatus`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.NetworkMonitor.html#org-freedesktop-portal-networkmonitor-getstatus). #[doc(alias = "GetStatus")] #[doc(alias = "get_status")] pub async fn status(&self) -> Result { self.0.call_versioned("GetStatus", &(), 3).await } /// Emitted when the network configuration changes. /// /// # Specifications /// /// See also [`changed`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.NetworkMonitor.html#org-freedesktop-portal-networkmonitor-changed). pub async fn receive_changed(&self) -> Result, Error> { self.0.signal("changed").await } } impl<'a> std::ops::Deref for NetworkMonitor<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } ashpd-0.9.1/src/desktop/notification.rs000064400000000000000000000264221046102023000162340ustar 00000000000000//! # Examples //! //! ```rust,no_run //! use std::{thread, time}; //! //! use ashpd::desktop::{ //! notification::{Action, Button, Notification, NotificationProxy, Priority}, //! Icon, //! }; //! use futures_util::StreamExt; //! use zbus::zvariant::Value; //! //! async fn run() -> ashpd::Result<()> { //! let proxy = NotificationProxy::new().await?; //! //! let notification_id = "org.gnome.design.Contrast"; //! proxy //! .add_notification( //! notification_id, //! Notification::new("Contrast") //! .default_action("open") //! .default_action_target(100) //! .body("color copied to clipboard") //! .priority(Priority::High) //! .icon(Icon::with_names(&["dialog-question-symbolic"])) //! .button(Button::new("Copy", "copy").target(32)) //! .button(Button::new("Delete", "delete").target(40)), //! ) //! .await?; //! //! let action = proxy //! .receive_action_invoked() //! .await? //! .next() //! .await //! .expect("Stream exhausted"); //! match action.name() { //! "copy" => (), // Copy something to clipboard //! "delete" => (), // Delete the file //! _ => (), //! }; //! println!("{:#?}", action.id()); //! println!( //! "{:#?}", //! action.parameter().get(0).unwrap().downcast_ref::() //! ); //! //! proxy.remove_notification(notification_id).await?; //! Ok(()) //! } //! ``` use std::{fmt, str::FromStr}; use futures_util::Stream; use serde::{self, Deserialize, Serialize}; use zbus::zvariant::{OwnedValue, SerializeDict, Type, Value}; use super::Icon; use crate::{proxy::Proxy, Error}; #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdPriority"))] #[derive(Debug, Copy, Clone, Serialize, PartialEq, Eq, Type)] #[zvariant(signature = "s")] #[serde(rename_all = "lowercase")] /// The notification priority pub enum Priority { /// Low. Low, /// Normal. Normal, /// High. High, /// Urgent. Urgent, } impl fmt::Display for Priority { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Low => write!(f, "Low"), Self::Normal => write!(f, "Normal"), Self::High => write!(f, "High"), Self::Urgent => write!(f, "Urgent"), } } } impl AsRef for Priority { fn as_ref(&self) -> &str { match self { Self::Low => "Low", Self::Normal => "Normal", Self::High => "High", Self::Urgent => "Urgent", } } } impl From for &'static str { fn from(d: Priority) -> Self { match d { Priority::Low => "Low", Priority::Normal => "Normal", Priority::High => "High", Priority::Urgent => "Urgent", } } } impl FromStr for Priority { type Err = Error; fn from_str(s: &str) -> Result { match s { "Low" | "low" => Ok(Priority::Low), "Normal" | "normal" => Ok(Priority::Normal), "High" | "high" => Ok(Priority::High), "Urgent" | "urgent" => Ok(Priority::Urgent), _ => Err(Error::ParseError("Failed to parse priority, invalid value")), } } } #[derive(SerializeDict, Type, Debug)] /// A notification #[zvariant(signature = "dict")] pub struct Notification { /// User-visible string to display as the title. title: String, /// User-visible string to display as the body. body: Option, /// Serialized icon (e.g using gio::Icon::serialize). icon: Option, /// The priority for the notification. priority: Option, /// Name of an action that is exported by the application. /// This action will be activated when the user clicks on the notification. #[zvariant(rename = "default-action")] default_action: Option, /// Target parameter to send along when activating the default action. #[zvariant(rename = "default-action-target")] default_action_target: Option, /// Array of buttons to add to the notification. buttons: Option>, } impl Notification { /// Create a new notification. /// /// # Arguments /// /// * `title` - the notification title. pub fn new(title: &str) -> Self { Self { title: title.to_owned(), body: None, priority: None, icon: None, default_action: None, default_action_target: None, buttons: None, } } /// Sets the notification body. #[must_use] pub fn body<'a>(mut self, body: impl Into>) -> Self { self.body = body.into().map(ToOwned::to_owned); self } /// Sets an icon to the notification. #[must_use] pub fn icon(mut self, icon: impl Into>) -> Self { self.icon = icon.into(); self } /// Sets the notification priority. #[must_use] pub fn priority(mut self, priority: impl Into>) -> Self { self.priority = priority.into(); self } /// Sets the default action when the user clicks on the notification. #[must_use] pub fn default_action<'a>(mut self, default_action: impl Into>) -> Self { self.default_action = default_action.into().map(ToOwned::to_owned); self } /// Sets a value to be sent in the `action_invoked` signal. #[must_use] pub fn default_action_target<'a, T: Into>>( mut self, default_action_target: impl Into>, ) -> Self { self.default_action_target = default_action_target .into() .map(|t| t.into().try_to_owned().unwrap()); self } /// Adds a new button to the notification. #[must_use] pub fn button(mut self, button: Button) -> Self { match self.buttons { Some(ref mut buttons) => buttons.push(button), None => { self.buttons.replace(vec![button]); } }; self } } #[derive(SerializeDict, Type, Debug)] /// A notification button #[zvariant(signature = "dict")] pub struct Button { /// User-visible label for the button. Mandatory. label: String, /// Name of an action that is exported by the application. The action will /// be activated when the user clicks on the button. action: String, /// Target parameter to send along when activating the action. target: Option, } impl Button { /// Create a new notification button. /// /// # Arguments /// /// * `label` - the user visible label of the button. /// * `action` - the action name to be invoked when the user clicks on the /// button. pub fn new(label: &str, action: &str) -> Self { Self { label: label.to_owned(), action: action.to_owned(), target: None, } } /// The value to send with the action name when the button is clicked. #[must_use] pub fn target<'a, T: Into>>(mut self, target: impl Into>) -> Self { self.target = target.into().map(|t| t.into().try_to_owned().unwrap()); self } } #[derive(Debug, Deserialize, Type)] /// An invoked action. pub struct Action(String, String, Vec); impl Action { /// Notification ID. pub fn id(&self) -> &str { &self.0 } /// Action name. pub fn name(&self) -> &str { &self.1 } /// The parameters passed to the action. pub fn parameter(&self) -> &Vec { &self.2 } } /// The interface lets sandboxed applications send and withdraw notifications. /// /// It is not possible for the application to learn if the notification was /// actually presented to the user. Not a portal in the strict sense, since /// there is no user interaction. /// /// **Note** in contrast to most other portal requests, notifications are /// expected to outlast the running application. If a user clicks on a /// notification after the application has exited, it will get activated again. /// /// Notifications can specify actions that can be activated by the user. /// Actions whose name starts with 'app.' are assumed to be exported and will be /// activated via the ActivateAction() method in the org.freedesktop.Application /// interface. Other actions are activated by sending the /// `#org.freedeskop.portal.Notification::ActionInvoked` signal to the /// application. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.Notification`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Notification.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.Notification")] pub struct NotificationProxy<'a>(Proxy<'a>); impl<'a> NotificationProxy<'a> { /// Create a new instance of [`NotificationProxy`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.Notification").await?; Ok(Self(proxy)) } /// Signal emitted when a particular action is invoked. /// /// # Specifications /// /// See also [`ActionInvoked`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Notification.html#org-freedesktop-portal-notification-actioninvoked). #[doc(alias = "ActionInvoked")] #[doc(alias = "XdpPortal::notification-action-invoked")] pub async fn receive_action_invoked(&self) -> Result, Error> { self.0.signal("ActionInvoked").await } /// Sends a notification. /// /// The ID can be used to later withdraw the notification. /// If the application reuses the same ID without withdrawing, the /// notification is replaced by the new one. /// /// # Arguments /// /// * `id` - Application-provided ID for this notification. /// * `notification` - The notification. /// /// # Specifications /// /// See also [`AddNotification`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Notification.html#org-freedesktop-portal-notification-addnotification). #[doc(alias = "AddNotification")] #[doc(alias = "xdp_portal_add_notification")] pub async fn add_notification( &self, id: &str, notification: Notification, ) -> Result<(), Error> { self.0.call("AddNotification", &(id, notification)).await } /// Withdraws a notification. /// /// # Arguments /// /// * `id` - Application-provided ID for this notification. /// /// # Specifications /// /// See also [`RemoveNotification`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Notification.html#org-freedesktop-portal-notification-removenotification). #[doc(alias = "RemoveNotification")] #[doc(alias = "xdp_portal_remove_notification")] pub async fn remove_notification(&self, id: &str) -> Result<(), Error> { self.0.call("RemoveNotification", &(id)).await } } impl<'a> std::ops::Deref for NotificationProxy<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } ashpd-0.9.1/src/desktop/open_uri.rs000064400000000000000000000147031046102023000153650ustar 00000000000000//! Open a URI or a directory. //! //! Wrapper of the DBus interface: [`org.freedesktop.portal.OpenURI`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html). //! //! # Examples //! //! ## Open a file //! //! ```rust,no_run //! use std::{fs::File, os::fd::AsFd}; //! //! use ashpd::desktop::open_uri::OpenFileRequest; //! //! async fn run() -> ashpd::Result<()> { //! let file = File::open("/home/bilelmoussaoui/adwaita-day.jpg").unwrap(); //! OpenFileRequest::default() //! .ask(true) //! .send_file(&file.as_fd()) //! .await?; //! Ok(()) //! } //! ``` //! //! ## Open a file from a URI //! //! //! ```rust,no_run //! use ashpd::desktop::open_uri::OpenFileRequest; //! //! async fn run() -> ashpd::Result<()> { //! let uri = //! url::Url::parse("file:///home/bilelmoussaoui/Downloads/adwaita-night.jpg").unwrap(); //! OpenFileRequest::default().ask(true).send_uri(&uri).await?; //! Ok(()) //! } //! ``` //! //! ## Open a directory //! //! ```rust,no_run //! use std::{fs::File, os::fd::AsFd}; //! //! use ashpd::desktop::open_uri::OpenDirectoryRequest; //! //! async fn run() -> ashpd::Result<()> { //! let directory = File::open("/home/bilelmoussaoui/Downloads").unwrap(); //! OpenDirectoryRequest::default() //! .send(&directory.as_fd()) //! .await?; //! Ok(()) //! } //! ``` use std::os::fd::BorrowedFd; use url::Url; use zbus::zvariant::{Fd, SerializeDict, Type}; use super::{HandleToken, Request}; use crate::{proxy::Proxy, ActivationToken, Error, WindowIdentifier}; #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct OpenDirOptions { handle_token: HandleToken, activation_token: Option, } #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct OpenFileOptions { handle_token: HandleToken, writeable: Option, ask: Option, activation_token: Option, } #[derive(Debug)] struct OpenURIProxy<'a>(Proxy<'a>); impl<'a> OpenURIProxy<'a> { pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.OpenURI").await?; Ok(Self(proxy)) } pub async fn open_directory( &self, identifier: &WindowIdentifier, directory: &BorrowedFd<'_>, options: OpenDirOptions, ) -> Result, Error> { self.0 .empty_request( &options.handle_token, "OpenDirectory", &(&identifier, Fd::from(directory), &options), ) .await } pub async fn open_file( &self, identifier: &WindowIdentifier, file: &BorrowedFd<'_>, options: OpenFileOptions, ) -> Result, Error> { self.0 .empty_request( &options.handle_token, "OpenFile", &(&identifier, Fd::from(file), &options), ) .await } pub async fn open_uri( &self, identifier: &WindowIdentifier, uri: &url::Url, options: OpenFileOptions, ) -> Result, Error> { self.0 .empty_request( &options.handle_token, "OpenURI", &(&identifier, uri, &options), ) .await } } impl<'a> std::ops::Deref for OpenURIProxy<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } #[derive(Debug, Default)] #[doc(alias = "org.freedesktop.portal.OpenURI")] #[doc(alias = "xdp_portal_open_uri")] /// A [builder-pattern] type to open a file. /// /// [builder-pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html pub struct OpenFileRequest { identifier: WindowIdentifier, options: OpenFileOptions, } impl OpenFileRequest { #[must_use] /// Sets a window identifier. pub fn identifier(mut self, identifier: impl Into>) -> Self { self.identifier = identifier.into().unwrap_or_default(); self } #[must_use] /// Whether the file should be writeable or not. pub fn writeable(mut self, writeable: impl Into>) -> Self { self.options.writeable = writeable.into(); self } #[must_use] /// Whether to always ask the user which application to use or not. pub fn ask(mut self, ask: impl Into>) -> Self { self.options.ask = ask.into(); self } /// Sets the token that can be used to activate the chosen application. #[must_use] pub fn activation_token( mut self, activation_token: impl Into>, ) -> Self { self.options.activation_token = activation_token.into(); self } /// Send the request for a file. pub async fn send_file(self, file: &BorrowedFd<'_>) -> Result, Error> { let proxy = OpenURIProxy::new().await?; proxy.open_file(&self.identifier, file, self.options).await } /// Send the request for a URI. pub async fn send_uri(self, uri: &Url) -> Result, Error> { let proxy = OpenURIProxy::new().await?; proxy.open_uri(&self.identifier, uri, self.options).await } } #[derive(Debug, Default)] #[doc(alias = "xdp_portal_open_directory")] #[doc(alias = "org.freedesktop.portal.OpenURI")] /// A [builder-pattern] type to open a directory. /// /// [builder-pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html pub struct OpenDirectoryRequest { identifier: WindowIdentifier, options: OpenDirOptions, } impl OpenDirectoryRequest { #[must_use] /// Sets a window identifier. pub fn identifier(mut self, identifier: impl Into>) -> Self { self.identifier = identifier.into().unwrap_or_default(); self } /// Sets the token that can be used to activate the chosen application. #[must_use] pub fn activation_token( mut self, activation_token: impl Into>, ) -> Self { self.options.activation_token = activation_token.into(); self } /// Send the request. pub async fn send(self, directory: &BorrowedFd<'_>) -> Result, Error> { let proxy = OpenURIProxy::new().await?; proxy .open_directory(&self.identifier, directory, self.options) .await } } ashpd-0.9.1/src/desktop/power_profile_monitor.rs000064400000000000000000000032401046102023000201620ustar 00000000000000use crate::{proxy::Proxy, Error}; /// The interface provides information about the user-selected system-wide power /// profile, to sandboxed applications. /// /// It is not a portal in the strict sense, /// since it does not involve user interaction. /// /// Applications are expected to use this interface indirectly, via a library /// API such as the GLib [`gio::PowerProfileMonitor`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/gio/struct.PowerProfileMonitor.html) interface. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.PowerProfileMonitor`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.PowerProfileMonitor.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.PowerProfileMonitor")] pub struct PowerProfileMonitor<'a>(Proxy<'a>); impl<'a> PowerProfileMonitor<'a> { /// Create a new instance of [`PowerProfileMonitor`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.PowerProfileMonitor").await?; Ok(Self(proxy)) } /// Whether the power saver is enabled. /// /// # Specifications /// /// See also [`power-saver-enabled`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.PowerProfileMonitor.html#org-freedesktop-portal-powerprofilemonitor-power-saver-enabled) #[doc(alias = "power-saver-enabled")] pub async fn is_enabled(&self) -> Result { self.0.property("power-saver-enabled").await } } impl<'a> std::ops::Deref for PowerProfileMonitor<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } ashpd-0.9.1/src/desktop/print.rs000064400000000000000000000561561046102023000147110ustar 00000000000000//! # Examples //! //! Print a file //! //! ```rust,no_run //! use std::{fs::File, os::fd::AsFd}; //! //! use ashpd::{desktop::print::PrintProxy, WindowIdentifier}; //! //! async fn run() -> ashpd::Result<()> { //! let proxy = PrintProxy::new().await?; //! let identifier = WindowIdentifier::default(); //! //! let file = //! File::open("/home/bilelmoussaoui/gitlog.pdf").expect("file to print was not found"); //! let pre_print = proxy //! .prepare_print( //! &identifier, //! "prepare print", //! Default::default(), //! Default::default(), //! None, //! true, //! ) //! .await? //! .response()?; //! proxy //! .print( //! &identifier, //! "test", //! &file.as_fd(), //! Some(pre_print.token), //! true, //! ) //! .await?; //! //! Ok(()) //! } //! ``` use std::{fmt, os::fd::BorrowedFd, str::FromStr}; use serde::{Deserialize, Serialize}; use zbus::zvariant::{DeserializeDict, Fd, SerializeDict, Type}; use super::{HandleToken, Request}; use crate::{proxy::Proxy, Error, WindowIdentifier}; #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdOrientation"))] #[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq, Type)] #[zvariant(signature = "s")] #[serde(rename_all = "snake_case")] /// The page orientation. pub enum Orientation { /// Landscape. Landscape, /// Portrait. Portrait, /// Reverse landscape. ReverseLandscape, /// Reverse portrait. ReversePortrait, } impl fmt::Display for Orientation { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Landscape => write!(f, "Landscape"), Self::Portrait => write!(f, "Portrait"), Self::ReverseLandscape => write!(f, "Reverse Landscape"), Self::ReversePortrait => write!(f, "Reverse Portrait"), } } } impl AsRef for Orientation { fn as_ref(&self) -> &str { match self { Self::Landscape => "Landscape", Self::Portrait => "Portrait", Self::ReverseLandscape => "Reverse Landscape", Self::ReversePortrait => "Reverse Portrait", } } } impl From for &'static str { fn from(o: Orientation) -> Self { match o { Orientation::Landscape => "Landscape", Orientation::Portrait => "Portrait", Orientation::ReverseLandscape => "Reverse Landscape", Orientation::ReversePortrait => "Reverse Portrait", } } } impl FromStr for Orientation { type Err = Error; fn from_str(s: &str) -> Result { match s { "Landscape" | "landscape" => Ok(Orientation::Landscape), "Portrait" | "portrait" => Ok(Orientation::Portrait), "ReverseLandscape" | "reverse_landscape" => Ok(Orientation::ReverseLandscape), "ReversePortrait" | "reverse_portrait" => Ok(Orientation::ReversePortrait), _ => Err(Error::ParseError( "Failed to parse orientation, invalid value", )), } } } #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdQuality"))] #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Type)] #[zvariant(signature = "s")] #[serde(rename_all = "lowercase")] /// The print quality. pub enum Quality { /// Draft quality. Draft, /// Low quality. Low, /// Normal quality. Normal, /// High quality. High, } impl fmt::Display for Quality { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Draft => write!(f, "Draft"), Self::Low => write!(f, "Low"), Self::Normal => write!(f, "Normal"), Self::High => write!(f, "High"), } } } impl AsRef for Quality { fn as_ref(&self) -> &str { match self { Self::Draft => "Draft", Self::Low => "Low", Self::Normal => "Normal", Self::High => "High", } } } impl From for &'static str { fn from(q: Quality) -> Self { match q { Quality::Draft => "Draft", Quality::Low => "Low", Quality::Normal => "Normal", Quality::High => "High", } } } impl FromStr for Quality { type Err = Error; fn from_str(s: &str) -> Result { match s { "Draft" | "draft" => Ok(Quality::Draft), "Low" | "low" => Ok(Quality::Low), "Normal" | "normal" => Ok(Quality::Normal), "High" | "high" => Ok(Quality::High), _ => Err(Error::ParseError("Failed to parse quality, invalid value")), } } } #[derive(SerializeDict, DeserializeDict, Type, Debug, Default)] /// Print settings to set in the print dialog. #[zvariant(signature = "dict")] pub struct Settings { /// One of landscape, portrait, reverse_landscape or reverse_portrait. pub orientation: Option, /// A paper name according to [PWG 5101.1-2002](ftp://ftp.pwg.org/pub/pwg/candidates/cs-pwgmsn10-20020226-5101.1.pdf) #[zvariant(rename = "paper-format")] pub paper_format: Option, /// Paper width, in millimeters. #[zvariant(rename = "paper-width")] pub paper_width: Option, /// Paper height, in millimeters. #[zvariant(rename = "paper-height")] pub paper_height: Option, /// The number of copies to print. #[zvariant(rename = "n-copies")] pub n_copies: Option, /// The default paper source. #[zvariant(rename = "default-source")] pub default_source: Option, /// Print quality. pub quality: Option, /// The resolution, sets both resolution-x & resolution-y pub resolution: Option, /// Whether to use color. #[zvariant(rename = "use-color")] pub use_color: Option, /// Duplex printing mode, one of simplex, horizontal or vertical. pub duplex: Option, /// Whether to collate copies. pub collate: Option, /// Whether to reverse the order of printed pages. pub reverse: Option, /// A media type according to [PWG 5101.1-2002](ftp://ftp.pwg.org/pub/pwg/candidates/cs-pwgmsn10-20020226-5101.1.pdf) #[zvariant(rename = "media-type")] pub media_type: Option, /// The dithering to use, one of fine, none, coarse, lineart, grayscale or /// error-diffusion. pub dither: Option, /// The scale in percent pub scale: Option, /// What pages to print, one of all, selection, current or ranges. #[zvariant(rename = "print-pages")] pub print_pages: Option, /// A list of page ranges, formatted like this: 0-2,4,9-11. #[zvariant(rename = "page-ranges")] pub page_ranges: Option, /// What pages to print, one of all, even or odd. #[zvariant(rename = "page-set")] pub page_set: Option, /// The finishings. pub finishings: Option, /// The number of pages per sheet. #[zvariant(rename = "number-up")] pub number_up: Option, /// One of lrtb, lrbt, rltb, rlbt, tblr, tbrl, btlr, btrl. #[zvariant(rename = "number-up-layout")] pub number_up_layout: Option, #[zvariant(rename = "output-bin")] /// The output bin. pub output_bin: Option, /// The horizontal resolution in dpi. #[zvariant(rename = "resolution-x")] pub resolution_x: Option, /// The vertical resolution in dpi. #[zvariant(rename = "resolution-y")] pub resolution_y: Option, /// The resolution in lpi (lines per inch). #[zvariant(rename = "printer-lpi")] pub print_lpi: Option, /// Basename to use for print-to-file. #[zvariant(rename = "output-basename")] pub output_basename: Option, /// Format to use for print-to-file, one of PDF, PS, SVG #[zvariant(rename = "output-file-format")] pub output_file_format: Option, /// The uri used for print-to file. #[zvariant(rename = "output-uri")] pub output_uri: Option, } impl Settings { /// Sets the orientation. #[must_use] pub fn orientation(mut self, orientation: impl Into>) -> Self { self.orientation = orientation.into(); self } /// Sets the paper name. #[must_use] pub fn paper_format<'a>(mut self, paper_format: impl Into>) -> Self { self.paper_format = paper_format.into().map(ToOwned::to_owned); self } /// Sets the paper width. #[must_use] pub fn paper_width<'a>(mut self, paper_width: impl Into>) -> Self { self.paper_width = paper_width.into().map(ToOwned::to_owned); self } /// Sets the paper height. #[must_use] pub fn paper_height<'a>(mut self, paper_height: impl Into>) -> Self { self.paper_height = paper_height.into().map(ToOwned::to_owned); self } /// Sets the number of copies to print. #[must_use] pub fn n_copies<'a>(mut self, n_copies: impl Into>) -> Self { self.n_copies = n_copies.into().map(ToOwned::to_owned); self } /// Sets the default paper source. #[must_use] pub fn default_source<'a>(mut self, default_source: impl Into>) -> Self { self.default_source = default_source.into().map(ToOwned::to_owned); self } /// Sets the print quality. #[must_use] pub fn quality(mut self, quality: impl Into>) -> Self { self.quality = quality.into(); self } /// Sets the resolution, both resolution-x & resolution-y. #[must_use] pub fn resolution<'a>(mut self, resolution: impl Into>) -> Self { self.resolution = resolution.into().map(ToOwned::to_owned); self } /// Sets whether to use color. #[must_use] pub fn use_color(mut self, use_color: impl Into>) -> Self { self.use_color = use_color.into(); self } /// Sets the duplex printing mode. #[must_use] pub fn duplex<'a>(mut self, duplex: impl Into>) -> Self { self.duplex = duplex.into().map(ToOwned::to_owned); self } /// Whether to collate copies. #[must_use] pub fn collate<'a>(mut self, collate: impl Into>) -> Self { self.collate = collate.into().map(ToOwned::to_owned); self } /// Sets whether to reverse the order of the printed pages. #[must_use] pub fn reverse<'a>(mut self, reverse: impl Into>) -> Self { self.reverse = reverse.into().map(ToOwned::to_owned); self } /// Sets the media type. #[must_use] pub fn media_type<'a>(mut self, media_type: impl Into>) -> Self { self.media_type = media_type.into().map(ToOwned::to_owned); self } /// Sets the dithering to use. #[must_use] pub fn dither<'a>(mut self, dither: impl Into>) -> Self { self.dither = dither.into().map(ToOwned::to_owned); self } /// Sets the page scale in percent. #[must_use] pub fn scale<'a>(mut self, scale: impl Into>) -> Self { self.scale = scale.into().map(ToOwned::to_owned); self } /// Sets what pages to print, one of all, selection, current or ranges. #[must_use] pub fn print_pages<'a>(mut self, print_pages: impl Into>) -> Self { self.print_pages = print_pages.into().map(ToOwned::to_owned); self } /// Sets a list of page ranges, formatted like this: 0-2,4,9-11. #[must_use] pub fn page_ranges<'a>(mut self, page_ranges: impl Into>) -> Self { self.page_ranges = page_ranges.into().map(ToOwned::to_owned); self } /// Sets what pages to print, one of all, even or odd. #[must_use] pub fn page_set<'a>(mut self, page_set: impl Into>) -> Self { self.page_set = page_set.into().map(ToOwned::to_owned); self } /// Sets the finishings. #[must_use] pub fn finishings<'a>(mut self, finishings: impl Into>) -> Self { self.finishings = finishings.into().map(ToOwned::to_owned); self } /// Sets the number of pages per sheet. #[must_use] pub fn number_up<'a>(mut self, number_up: impl Into>) -> Self { self.number_up = number_up.into().map(ToOwned::to_owned); self } /// Sets the number up layout, one of lrtb, lrbt, rltb, rlbt, tblr, tbrl, /// btlr, btrl. #[must_use] pub fn number_up_layout<'a>(mut self, number_up_layout: impl Into>) -> Self { self.number_up_layout = number_up_layout.into().map(ToOwned::to_owned); self } /// Sets the output bin #[must_use] pub fn output_bin<'a>(mut self, output_bin: impl Into>) -> Self { self.output_bin = output_bin.into().map(ToOwned::to_owned); self } /// Sets the horizontal resolution in dpi. #[must_use] pub fn resolution_x<'a>(mut self, resolution_x: impl Into>) -> Self { self.resolution_x = resolution_x.into().map(ToOwned::to_owned); self } /// Sets the vertical resolution in dpi. #[must_use] pub fn resolution_y<'a>(mut self, resolution_y: impl Into>) -> Self { self.resolution_y = resolution_y.into().map(ToOwned::to_owned); self } /// Sets the resolution in lines per inch. #[must_use] pub fn print_lpi<'a>(mut self, print_lpi: impl Into>) -> Self { self.print_lpi = print_lpi.into().map(ToOwned::to_owned); self } /// Sets the print-to-file base name. #[must_use] pub fn output_basename<'a>(mut self, output_basename: impl Into>) -> Self { self.output_basename = output_basename.into().map(ToOwned::to_owned); self } /// Sets the print-to-file format, one of PS, PDF, SVG. #[must_use] pub fn output_file_format<'a>( mut self, output_file_format: impl Into>, ) -> Self { self.output_file_format = output_file_format.into().map(ToOwned::to_owned); self } /// Sets the print-to-file output uri. #[must_use] pub fn output_uri<'a>(mut self, output_uri: impl Into>) -> Self { self.output_uri = output_uri.into().map(ToOwned::to_owned); self } } #[derive(SerializeDict, DeserializeDict, Type, Debug, Default)] /// Setup the printed pages. #[zvariant(signature = "dict")] pub struct PageSetup { /// the PPD name. It's the name to select a given driver. #[zvariant(rename = "PPDName")] pub ppdname: Option, /// The name of the page setup. pub name: Option, /// The user-visible name of the page setup. pub display_name: Option, /// Paper width in millimeters. pub width: Option, /// Paper height in millimeters. pub height: Option, /// Top margin in millimeters. pub margin_top: Option, /// Bottom margin in millimeters. pub margin_bottom: Option, /// Right margin in millimeters. pub margin_right: Option, /// Left margin in millimeters. pub margin_left: Option, /// The page orientation. pub orientation: Option, } impl PageSetup { /// Sets the ppdname. #[must_use] pub fn ppdname<'a>(mut self, ppdname: impl Into>) -> Self { self.ppdname = ppdname.into().map(ToOwned::to_owned); self } /// Sets the name of the page setup. #[must_use] pub fn name<'a>(mut self, name: impl Into>) -> Self { self.name = name.into().map(ToOwned::to_owned); self } /// Sets the user visible name of the page setup. #[must_use] pub fn display_name<'a>(mut self, display_name: impl Into>) -> Self { self.display_name = display_name.into().map(ToOwned::to_owned); self } /// Sets the orientation. #[must_use] pub fn orientation(mut self, orientation: impl Into>) -> Self { self.orientation = orientation.into(); self } /// Sets the page width. #[must_use] pub fn width(mut self, width: impl Into>) -> Self { self.width = width.into(); self } /// Sets the page height. #[must_use] pub fn height(mut self, height: impl Into>) -> Self { self.height = height.into(); self } /// Sets the page top margin. #[must_use] pub fn margin_top(mut self, margin_top: impl Into>) -> Self { self.margin_top = margin_top.into(); self } /// Sets the page bottom margin. #[must_use] pub fn margin_bottom(mut self, margin_bottom: impl Into>) -> Self { self.margin_bottom = margin_bottom.into(); self } /// Sets the page right margin. #[must_use] pub fn margin_right(mut self, margin_right: impl Into>) -> Self { self.margin_right = margin_right.into(); self } /// Sets the page margin left. #[must_use] pub fn margin_left(mut self, margin_left: impl Into>) -> Self { self.margin_left = margin_left.into(); self } } #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`PrintProxy::prepare_print`] request. #[zvariant(signature = "dict")] struct PreparePrintOptions { /// A string that will be used as the last element of the handle. handle_token: HandleToken, /// Whether to make the dialog modal. modal: Option, /// Label for the accept button. Mnemonic underlines are allowed. accept_label: Option, } impl PreparePrintOptions { /// Sets whether the dialog should be a modal. #[must_use] pub fn modal(mut self, modal: impl Into>) -> Self { self.modal = modal.into(); self } /// Label for the accept button. Mnemonic underlines are allowed. #[must_use] pub fn accept_label<'a>(mut self, accept_label: impl Into>) -> Self { self.accept_label = accept_label.into().map(ToOwned::to_owned); self } } #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`PrintProxy::print`] request. #[zvariant(signature = "dict")] struct PrintOptions { /// A string that will be used as the last element of the handle. handle_token: HandleToken, /// Whether to make the dialog modal. modal: Option, /// Token that was returned by a previous [`PrintProxy::prepare_print`] /// call. token: Option, } impl PrintOptions { /// A token retrieved from [`PrintProxy::prepare_print`]. pub fn token(mut self, token: impl Into>) -> Self { self.token = token.into(); self } /// Sets whether the dialog should be a modal. pub fn modal(mut self, modal: impl Into>) -> Self { self.modal = modal.into(); self } } #[derive(DeserializeDict, Type, Debug)] /// A response to a [`PrintProxy::prepare_print`] request. #[zvariant(signature = "dict")] pub struct PreparePrint { /// The printing settings. pub settings: Settings, #[zvariant(rename = "page-setup")] /// The printed pages setup. pub page_setup: PageSetup, /// A token to pass to the print request. pub token: u32, } /// The interface lets sandboxed applications print. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.Print`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Print.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.Print")] pub struct PrintProxy<'a>(Proxy<'a>); impl<'a> PrintProxy<'a> { /// Create a new instance of [`PrintProxy`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.Print").await?; Ok(Self(proxy)) } // TODO accept_label: Added in version 2 of the interface. /// Presents a print dialog to the user and returns print settings and page /// setup. /// /// # Arguments /// /// * `identifier` - Identifier for the application window. /// * `title` - Title for the print dialog. /// * `settings` - [`Settings`]. /// * `page_setup` - [`PageSetup`]. /// * `modal` - Whether the dialog should be a modal. /// * `accept_label` - Label for the accept button. Mnemonic underlines are /// allowed. /// /// # Specifications /// /// See also [`PreparePrint`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Print.html#org-freedesktop-portal-print-prepareprint). #[doc(alias = "PreparePrint")] #[doc(alias = "xdp_portal_prepare_print")] pub async fn prepare_print( &self, identifier: &WindowIdentifier, title: &str, settings: Settings, page_setup: PageSetup, accept_label: impl Into>, modal: bool, ) -> Result, Error> { let options = PreparePrintOptions::default() .modal(modal) .accept_label(accept_label); self.0 .request( &options.handle_token, "PreparePrint", &(&identifier, title, settings, page_setup, &options), ) .await } /// Asks to print a file. /// The file must be passed in the form of a file descriptor open for /// reading. This ensures that sandboxed applications only print files /// that they have access to. /// /// # Arguments /// /// * `identifier` - The application window identifier. /// * `title` - The title for the print dialog. /// * `fd` - File descriptor for reading the content to print. /// * `token` - A token returned by a call to /// [`prepare_print()`][`PrintProxy::prepare_print`]. /// * `modal` - Whether the dialog should be a modal. /// /// # Specifications /// /// See also [`Print`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Print.html#org-freedesktop-portal-print-print). #[doc(alias = "Print")] #[doc(alias = "xdp_portal_print_file")] pub async fn print( &self, identifier: &WindowIdentifier, title: &str, fd: &BorrowedFd<'_>, token: Option, modal: bool, ) -> Result, Error> { let options = PrintOptions::default() .token(token.unwrap_or(0)) .modal(modal); self.0 .empty_request( &options.handle_token, "Print", &(&identifier, title, Fd::from(fd), &options), ) .await } } impl<'a> std::ops::Deref for PrintProxy<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } ashpd-0.9.1/src/desktop/proxy_resolver.rs000064400000000000000000000042451046102023000166470ustar 00000000000000//! **Note** This portal doesn't work for sandboxed applications. //! # Examples //! //! ```rust,no_run //! use ashpd::desktop::proxy_resolver::ProxyResolver; //! //! async fn run() -> ashpd::Result<()> { //! let proxy = ProxyResolver::new().await?; //! let url = url::Url::parse("www.google.com").unwrap(); //! //! println!("{:#?}", proxy.lookup(&url).await?); //! //! Ok(()) //! } //! ``` use crate::{proxy::Proxy, Error}; /// The interface provides network proxy information to sandboxed applications. /// /// It is not a portal in the strict sense, since it does not involve user /// interaction. Applications are expected to use this interface indirectly, /// via a library API such as the GLib [`gio::ProxyResolver`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/gio/struct.ProxyResolver.html) interface. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.ProxyResolver`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ProxyResolver.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.ProxyResolver")] pub struct ProxyResolver<'a>(Proxy<'a>); impl<'a> ProxyResolver<'a> { /// Create a new instance of [`ProxyResolver`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.ProxyResolver").await?; Ok(Self(proxy)) } /// Looks up which proxy to use to connect to `uri`. /// /// # Returns /// /// A list of proxy uris of the form `protocol://[user[:password]host:port` /// The protocol can be `http`, `rtsp`, `socks` or another proxying /// protocol. `direct://` is used when no proxy is needed. /// /// # Specifications /// /// See also [`Lookup`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ProxyResolver.html#org-freedesktop-portal-proxyresolver-lookup). #[doc(alias = "Lookup")] pub async fn lookup(&self, uri: &url::Url) -> Result, Error> { self.0.call("Lookup", &(uri)).await } } impl<'a> std::ops::Deref for ProxyResolver<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } ashpd-0.9.1/src/desktop/realtime.rs000064400000000000000000000043571046102023000153530ustar 00000000000000//! Set threads to realtime. //! //! Wrapper of the DBus interface: [`org.freedesktop.portal.Realtime`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Realtime.html). use crate::{proxy::Proxy, Error}; /// Interface for setting a thread to realtime from within the sandbox. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.Realtime`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Realtime.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.Realtime")] pub struct Realtime<'a>(Proxy<'a>); impl<'a> Realtime<'a> { /// Create a new instance of [`Realtime`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.Realtime").await?; Ok(Self(proxy)) } #[doc(alias = "MakeThreadRealtimeWithPID")] #[allow(missing_docs)] pub async fn max_thread_realtime_with_pid( &self, process: u64, thread: u64, priority: u32, ) -> Result<(), Error> { self.0 .call("MakeThreadRealtimeWithPID", &(&process, &thread, &priority)) .await } #[doc(alias = "MakeThreadHighPriorityWithPID")] #[allow(missing_docs)] pub async fn max_thread_high_priority_with_pid( &self, process: u64, thread: u64, priority: i32, ) -> Result<(), Error> { self.0 .call( "MakeThreadHighPriorityWithPID", &(&process, &thread, &priority), ) .await } #[doc(alias = "MaxRealtimePriority")] #[allow(missing_docs)] pub async fn max_realtime_priority(&self) -> Result { self.0.property("MaxRealtimePriority").await } #[doc(alias = "MinNiceLevel")] #[allow(missing_docs)] pub async fn min_nice_level(&self) -> Result { self.0.property("MinNiceLevel").await } #[doc(alias = "RTTimeUSecMax")] #[allow(missing_docs)] pub async fn rt_time_usec_max(&self) -> Result { self.0.property("RTTimeUSecMax").await } } impl<'a> std::ops::Deref for Realtime<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } ashpd-0.9.1/src/desktop/remote_desktop.rs000064400000000000000000000624071046102023000165750ustar 00000000000000//! # Examples //! //! ```rust,no_run //! use ashpd::{ //! desktop::{ //! remote_desktop::{DeviceType, KeyState, RemoteDesktop}, //! PersistMode, //! }, //! WindowIdentifier, //! }; //! //! async fn run() -> ashpd::Result<()> { //! let proxy = RemoteDesktop::new().await?; //! let session = proxy.create_session().await?; //! proxy //! .select_devices( //! &session, //! DeviceType::Keyboard | DeviceType::Pointer, //! None, //! PersistMode::DoNot, //! ) //! .await?; //! //! let response = proxy //! .start(&session, &WindowIdentifier::default()) //! .await? //! .response()?; //! println!("{:#?}", response.devices()); //! //! // 13 for Enter key code //! proxy //! .notify_keyboard_keycode(&session, 13, KeyState::Pressed) //! .await?; //! //! Ok(()) //! } //! ``` //! //! You can also use the Remote Desktop portal with the ScreenCast one. In order //! to do so, you need to call //! [`Screencast::select_sources()`][select_sources] //! on the session created with //! [`RemoteDesktop::create_session()`][create_session] //! //! ```rust,no_run //! use ashpd::{ //! desktop::{ //! remote_desktop::{DeviceType, KeyState, RemoteDesktop}, //! screencast::{CursorMode, Screencast, SourceType}, //! PersistMode, //! }, //! WindowIdentifier, //! }; //! //! async fn run() -> ashpd::Result<()> { //! let remote_desktop = RemoteDesktop::new().await?; //! let screencast = Screencast::new().await?; //! let identifier = WindowIdentifier::default(); //! let session = remote_desktop.create_session().await?; //! //! remote_desktop //! .select_devices( //! &session, //! DeviceType::Keyboard | DeviceType::Pointer, //! None, //! PersistMode::DoNot, //! ) //! .await?; //! screencast //! .select_sources( //! &session, //! CursorMode::Metadata, //! SourceType::Monitor | SourceType::Window, //! true, //! None, //! PersistMode::DoNot, //! ) //! .await?; //! //! let response = remote_desktop //! .start(&session, &identifier) //! .await? //! .response()?; //! println!("{:#?}", response.devices()); //! println!("{:#?}", response.streams()); //! //! // 13 for Enter key code //! remote_desktop //! .notify_keyboard_keycode(&session, 13, KeyState::Pressed) //! .await?; //! //! Ok(()) //! } //! ``` //! [select_sources]: crate::desktop::screencast::Screencast::select_sources //! [create_session]: crate::desktop::remote_desktop::RemoteDesktop::create_session use std::{collections::HashMap, os::fd::OwnedFd}; use enumflags2::{bitflags, BitFlags}; use futures_util::TryFutureExt; use serde_repr::{Deserialize_repr, Serialize_repr}; use zbus::zvariant::{self, DeserializeDict, SerializeDict, Type, Value}; use super::{ screencast::Stream, session::SessionPortal, HandleToken, PersistMode, Request, Session, }; use crate::{desktop::session::CreateSessionResponse, proxy::Proxy, Error, WindowIdentifier}; #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdKeyState"))] #[derive(Serialize_repr, Deserialize_repr, Copy, Clone, PartialEq, Eq, Debug, Type)] #[doc(alias = "XdpKeyState")] /// The keyboard key state. #[repr(u32)] pub enum KeyState { #[doc(alias = "XDP_KEY_PRESSED")] /// The key is pressed. Pressed = 1, #[doc(alias = "XDP_KEY_RELEASED")] /// The key is released.. Released = 0, } #[bitflags] #[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Debug, Clone, Copy, Type)] #[repr(u32)] #[doc(alias = "XdpDeviceType")] /// A bit flag for the available devices. pub enum DeviceType { #[doc(alias = "XDP_DEVICE_KEYBOARD")] /// A keyboard. Keyboard, #[doc(alias = "XDP_DEVICE_POINTER")] /// A mouse pointer. Pointer, #[doc(alias = "XDP_DEVICE_TOUCHSCREEN")] /// A touchscreen Touchscreen, } #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdAxis"))] #[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Debug, Clone, Copy, Type)] #[doc(alias = "XdpDiscreteAxis")] #[repr(u32)] /// The available axis. pub enum Axis { #[doc(alias = "XDP_AXIS_VERTICAL_SCROLL")] /// Vertical axis. Vertical = 0, #[doc(alias = "XDP_AXIS_HORIZONTAL_SCROLL")] /// Horizontal axis. Horizontal = 1, } #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`RemoteDesktop::create_session`] request. #[zvariant(signature = "dict")] struct CreateRemoteOptions { /// A string that will be used as the last element of the handle. handle_token: HandleToken, /// A string that will be used as the last element of the session handle. session_handle_token: HandleToken, } #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`RemoteDesktop::select_devices`] request. #[zvariant(signature = "dict")] struct SelectDevicesOptions { /// A string that will be used as the last element of the handle. handle_token: HandleToken, /// The device types to request remote controlling of. Default is all. types: Option>, restore_token: Option, persist_mode: Option, } impl SelectDevicesOptions { /// Sets the device types to request remote controlling of. pub fn types(mut self, types: impl Into>>) -> Self { self.types = types.into(); self } pub fn persist_mode(mut self, persist_mode: impl Into>) -> Self { self.persist_mode = persist_mode.into(); self } pub fn restore_token<'a>(mut self, token: impl Into>) -> Self { self.restore_token = token.into().map(ToOwned::to_owned); self } } #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`RemoteDesktop::start`] request. #[zvariant(signature = "dict")] struct StartRemoteOptions { /// A string that will be used as the last element of the handle. handle_token: HandleToken, } #[derive(DeserializeDict, Type, Debug, Default)] /// A response to a [`RemoteDesktop::select_devices`] request. #[zvariant(signature = "dict")] pub struct SelectedDevices { devices: BitFlags, streams: Option>, restore_token: Option, } impl SelectedDevices { /// The selected devices. pub fn devices(&self) -> BitFlags { self.devices } /// The selected streams if a ScreenCast portal is used on the same session pub fn streams(&self) -> Option<&[Stream]> { self.streams.as_deref() } /// The session restore token. pub fn restore_token(&self) -> Option<&str> { self.restore_token.as_deref() } } /// The interface lets sandboxed applications create remote desktop sessions. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.RemoteDesktop`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.RemoteDesktop")] pub struct RemoteDesktop<'a>(Proxy<'a>); impl<'a> RemoteDesktop<'a> { /// Create a new instance of [`RemoteDesktop`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.RemoteDesktop").await?; Ok(Self(proxy)) } /// Create a remote desktop session. /// A remote desktop session is used to allow remote controlling a desktop /// session. It can also be used together with a screen cast session. /// /// # Specifications /// /// See also [`CreateSession`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html#org-freedesktop-portal-remotedesktop-createsession). #[doc(alias = "CreateSession")] #[doc(alias = "xdp_portal_create_remote_desktop_session")] pub async fn create_session(&self) -> Result, Error> { let options = CreateRemoteOptions::default(); let (request, proxy) = futures_util::try_join!( self.0 .request::(&options.handle_token, "CreateSession", &options) .into_future(), Session::from_unique_name(&options.session_handle_token).into_future() )?; assert_eq!(proxy.path(), &request.response()?.session_handle.as_ref()); Ok(proxy) } /// Select input devices to remote control. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`RemoteDesktop::create_session`]. /// * `types` - The device types to request remote controlling of. /// /// # Specifications /// /// See also [`SelectDevices`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html#org-freedesktop-portal-remotedesktop-selectdevices). #[doc(alias = "SelectDevices")] pub async fn select_devices( &self, session: &Session<'_, Self>, types: BitFlags, restore_token: Option<&str>, persist_mode: PersistMode, ) -> Result, Error> { let options = SelectDevicesOptions::default() .types(types) .persist_mode(persist_mode) .restore_token(restore_token); self.0 .empty_request(&options.handle_token, "SelectDevices", &(session, &options)) .await } /// Start the remote desktop session. /// /// This will typically result in the portal presenting a dialog letting /// the user select what to share, including devices and optionally screen /// content if screen cast sources was selected. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`RemoteDesktop::create_session`]. /// * `identifier` - The application window identifier. /// /// # Specifications /// /// See also [`Start`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html#org-freedesktop-portal-remotedesktop-start). #[doc(alias = "Start")] pub async fn start( &self, session: &Session<'_, Self>, identifier: &WindowIdentifier, ) -> Result, Error> { let options = StartRemoteOptions::default(); self.0 .request( &options.handle_token, "Start", &(session, &identifier, &options), ) .await } /// Notify keyboard code. /// /// **Note** only works if [`DeviceType::Keyboard`] access was provided /// after starting the session. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`RemoteDesktop::create_session`]. /// * `keycode` - Keyboard code that was pressed or released. /// * `state` - The new state of the keyboard code. /// /// # Specifications /// /// See also [`NotifyKeyboardKeycode`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html#org-freedesktop-portal-remotedesktop-notifykeyboardkeycode). #[doc(alias = "NotifyKeyboardKeycode")] pub async fn notify_keyboard_keycode( &self, session: &Session<'_, Self>, keycode: i32, state: KeyState, ) -> Result<(), Error> { // The `notify` methods don't take any options for now // see https://github.com/flatpak/xdg-desktop-portal/blob/master/src/remote-desktop.c#L723 let options: HashMap<&str, Value<'_>> = HashMap::new(); self.0 .call("NotifyKeyboardKeycode", &(session, options, keycode, state)) .await } /// Notify keyboard symbol. /// /// **Note** only works if [`DeviceType::Keyboard`] access was provided /// after starting the session. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`RemoteDesktop::create_session`]. /// * `keysym` - Keyboard symbol that was pressed or released. /// * `state` - The new state of the keyboard code. /// /// # Specifications /// /// See also [`NotifyKeyboardKeysym`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html#org-freedesktop-portal-remotedesktop-notifykeyboardkeysym). #[doc(alias = "NotifyKeyboardKeysym")] pub async fn notify_keyboard_keysym( &self, session: &Session<'_, Self>, keysym: i32, state: KeyState, ) -> Result<(), Error> { // The `notify` methods don't take any options for now // see https://github.com/flatpak/xdg-desktop-portal/blob/master/src/remote-desktop.c#L723 let options: HashMap<&str, Value<'_>> = HashMap::new(); self.0 .call("NotifyKeyboardKeysym", &(session, options, keysym, state)) .await } /// Notify about a new touch up event. /// /// **Note** only works if [`DeviceType::Touchscreen`] access was provided /// after starting the session. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`RemoteDesktop::create_session`]. /// * `slot` - Touch slot where touch point appeared. /// /// # Specifications /// /// See also [`NotifyTouchUp`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html#org-freedesktop-portal-remotedesktop-notifytouchup). #[doc(alias = "NotifyTouchUp")] pub async fn notify_touch_up( &self, session: &Session<'_, Self>, slot: u32, ) -> Result<(), Error> { // The `notify` methods don't take any options for now // see https://github.com/flatpak/xdg-desktop-portal/blob/master/src/remote-desktop.c#L723 let options: HashMap<&str, Value<'_>> = HashMap::new(); self.0 .call("NotifyTouchUp", &(session, options, slot)) .await } /// Notify about a new touch down event. /// The (x, y) position represents the new touch point position in the /// streams logical coordinate space. /// /// **Note** only works if [`DeviceType::Touchscreen`] access was provided /// after starting the session. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`RemoteDesktop::create_session`]. /// * `stream` - The PipeWire stream node the coordinate is relative to. /// * `slot` - Touch slot where touch point appeared. /// * `x` - Touch down x coordinate. /// * `y` - Touch down y coordinate. /// /// # Specifications /// /// See also [`NotifyTouchDown`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html#org-freedesktop-portal-remotedesktop-notifytouchdown). #[doc(alias = "NotifyTouchDown")] pub async fn notify_touch_down( &self, session: &Session<'_, Self>, stream: u32, slot: u32, x: f64, y: f64, ) -> Result<(), Error> { // The `notify` methods don't take any options for now // see https://github.com/flatpak/xdg-desktop-portal/blob/master/src/remote-desktop.c#L723 let options: HashMap<&str, Value<'_>> = HashMap::new(); self.0 .call("NotifyTouchDown", &(session, options, stream, slot, x, y)) .await } /// Notify about a new touch motion event. /// The (x, y) position represents where the touch point position in the /// streams logical coordinate space moved. /// /// **Note** only works if [`DeviceType::Touchscreen`] access was provided /// after starting the session. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`RemoteDesktop::create_session`]. /// * `stream` - The PipeWire stream node the coordinate is relative to. /// * `slot` - Touch slot where touch point appeared. /// * `x` - Touch motion x coordinate. /// * `y` - Touch motion y coordinate. /// /// # Specifications /// /// See also [`NotifyTouchMotion`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html#org-freedesktop-portal-remotedesktop-notifytouchmotion). #[doc(alias = "NotifyTouchMotion")] pub async fn notify_touch_motion( &self, session: &Session<'_, Self>, stream: u32, slot: u32, x: f64, y: f64, ) -> Result<(), Error> { // The `notify` methods don't take any options for now // see https://github.com/flatpak/xdg-desktop-portal/blob/master/src/remote-desktop.c#L723 let options: HashMap<&str, Value<'_>> = HashMap::new(); self.0 .call("NotifyTouchMotion", &(session, options, stream, slot, x, y)) .await } /// Notify about a new absolute pointer motion event. /// The (x, y) position represents the new pointer position in the streams /// logical coordinate space. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`RemoteDesktop::create_session`]. /// * `stream` - The PipeWire stream node the coordinate is relative to. /// * `x` - Pointer motion x coordinate. /// * `y` - Pointer motion y coordinate. /// /// # Specifications /// /// See also [`NotifyPointerMotionAbsolute`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html#org-freedesktop-portal-remotedesktop-notifypointermotionabsolute). #[doc(alias = "NotifyPointerMotionAbsolute")] pub async fn notify_pointer_motion_absolute( &self, session: &Session<'_, Self>, stream: u32, x: f64, y: f64, ) -> Result<(), Error> { // The `notify` methods don't take any options for now // see https://github.com/flatpak/xdg-desktop-portal/blob/master/src/remote-desktop.c#L723 let options: HashMap<&str, Value<'_>> = HashMap::new(); self.0 .call( "NotifyPointerMotionAbsolute", &(session, options, stream, x, y), ) .await } /// Notify about a new relative pointer motion event. /// The (dx, dy) vector represents the new pointer position in the streams /// logical coordinate space. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`RemoteDesktop::create_session`]. /// * `dx` - Relative movement on the x axis. /// * `dy` - Relative movement on the y axis. /// /// # Specifications /// /// See also [`NotifyPointerMotion`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html#org-freedesktop-portal-remotedesktop-notifypointermotion). #[doc(alias = "NotifyPointerMotion")] pub async fn notify_pointer_motion( &self, session: &Session<'_, Self>, dx: f64, dy: f64, ) -> Result<(), Error> { // The `notify` methods don't take any options for now // see https://github.com/flatpak/xdg-desktop-portal/blob/master/src/remote-desktop.c#L723 let options: HashMap<&str, Value<'_>> = HashMap::new(); self.0 .call("NotifyPointerMotion", &(session, options, dx, dy)) .await } /// Notify pointer button. /// The pointer button is encoded according to Linux Evdev button codes. /// /// /// **Note** only works if [`DeviceType::Pointer`] access was provided after /// starting the session. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`RemoteDesktop::create_session`]. /// * `button` - The pointer button was pressed or released. /// * `state` - The new state of the keyboard code. /// /// # Specifications /// /// See also [`NotifyPointerButton`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html#org-freedesktop-portal-remotedesktop-notifypointerbutton). #[doc(alias = "NotifyPointerButton")] pub async fn notify_pointer_button( &self, session: &Session<'_, Self>, button: i32, state: KeyState, ) -> Result<(), Error> { // The `notify` methods don't take any options for now // see https://github.com/flatpak/xdg-desktop-portal/blob/master/src/remote-desktop.c#L723 let options: HashMap<&str, Value<'_>> = HashMap::new(); self.0 .call("NotifyPointerButton", &(session, options, button, state)) .await } /// Notify pointer axis discrete. /// /// **Note** only works if [`DeviceType::Pointer`] access was provided after /// starting the session. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`RemoteDesktop::create_session`]. /// * `axis` - The axis that was scrolled. /// /// # Specifications /// /// See also [`NotifyPointerAxisDiscrete`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html#org-freedesktop-portal-remotedesktop-notifypointeraxisdiscrete). #[doc(alias = "NotifyPointerAxisDiscrete")] pub async fn notify_pointer_axis_discrete( &self, session: &Session<'_, Self>, axis: Axis, steps: i32, ) -> Result<(), Error> { // The `notify` methods don't take any options for now // see https://github.com/flatpak/xdg-desktop-portal/blob/master/src/remote-desktop.c#L723 let options: HashMap<&str, Value<'_>> = HashMap::new(); self.0 .call( "NotifyPointerAxisDiscrete", &(session, options, axis, steps), ) .await } /// Notify pointer axis. /// The axis movement from a "smooth scroll" device, such as a touchpad. /// When applicable, the size of the motion delta should be equivalent to /// the motion vector of a pointer motion done using the same advice. /// /// /// **Note** only works if [`DeviceType::Pointer`] access was provided after /// starting the session. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`RemoteDesktop::create_session`]. /// * `dx` - Relative axis movement on the x axis. /// * `dy` - Relative axis movement on the y axis. /// * `finish` - Whether it is the last axis event. /// /// # Specifications /// /// See also [`NotifyPointerAxis`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html#org-freedesktop-portal-remotedesktop-notifypointeraxis). #[doc(alias = "NotifyPointerAxis")] pub async fn notify_pointer_axis( &self, session: &Session<'_, Self>, dx: f64, dy: f64, finish: bool, ) -> Result<(), Error> { // see https://github.com/flatpak/xdg-desktop-portal/blob/master/src/remote-desktop.c#L911 let mut options: HashMap<&str, Value<'_>> = HashMap::new(); options.insert("finish", Value::Bool(finish)); self.0 .call("NotifyPointerAxis", &(session, options, dx, dy)) .await } /// Connect to EIS. /// /// **Note** only succeeds if called after [`RemoteDesktop::start`]. /// /// Requires RemoteDesktop version 2. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`RemoteDesktop::create_session`]. /// /// # Required version /// /// The method requires the 2nd version implementation of the portal and /// would fail with [`Error::RequiresVersion`] otherwise. /// /// # Specifications /// /// See also [`ConnectToEIS`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html#org-freedesktop-portal-remotedesktop-connecttoeis). #[doc(alias = "ConnectToEIS")] pub async fn connect_to_eis(&self, session: &Session<'_, Self>) -> Result { // `ConnectToEIS` doesn't take any options for now // see https://github.com/flatpak/xdg-desktop-portal/blob/master/src/remote-desktop.c#L1464 let options: HashMap<&str, Value<'_>> = HashMap::new(); let fd = self .0 .call_versioned::("ConnectToEIS", &(session, options), 2) .await?; Ok(fd.into()) } /// Available source types. /// /// # Specifications /// /// See also [`AvailableDeviceTypes`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html#org-freedesktop-portal-remotedesktop-availabledevicetypes). #[doc(alias = "AvailableDeviceTypes")] pub async fn available_device_types(&self) -> Result, Error> { self.0.property("AvailableDeviceTypes").await } } impl<'a> std::ops::Deref for RemoteDesktop<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } impl SessionPortal for RemoteDesktop<'_> {} ashpd-0.9.1/src/desktop/request.rs000064400000000000000000000223771046102023000152430ustar 00000000000000use std::{ collections::HashMap, fmt::{self, Debug}, marker::PhantomData, sync::Mutex, }; use futures_util::StreamExt; use serde::{ de::{self, Error as SeError, Visitor}, ser::SerializeTuple, Deserialize, Deserializer, Serialize, }; use zbus::{ proxy::SignalStream, zvariant::{ObjectPath, Type, Value}, }; use crate::{desktop::HandleToken, proxy::Proxy, Error}; /// A typical response returned by the [`Request::response`]. /// of a [`Request`]. #[derive(Debug, Type)] #[zvariant(signature = "(ua{sv})")] pub enum Response where T: for<'de> Deserialize<'de> + Type, { /// Success, the request is carried out. Ok(T), /// The user cancelled the request or something else happened. Err(ResponseError), } #[cfg(feature = "backend")] impl Response where T: for<'de> Deserialize<'de> + Type, { pub fn ok(inner: T) -> Self { Self::Ok(inner) } pub fn cancelled() -> Self { Self::Err(ResponseError::Cancelled) } pub fn other() -> Self { Self::Err(ResponseError::Other) } } impl<'de, T> Deserialize<'de> for Response where T: for<'d> Deserialize<'d> + Type, { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct ResponseVisitor(PhantomData (ResponseType, T)>); impl<'de, T> Visitor<'de> for ResponseVisitor where T: Deserialize<'de>, { type Value = (ResponseType, Option); fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!( formatter, "a tuple composed of the response status along with the response" ) } fn visit_seq(self, mut seq: A) -> Result where A: de::SeqAccess<'de>, { let type_: ResponseType = seq.next_element()?.ok_or_else(|| A::Error::custom( "Failed to deserialize the response. Expected a numeric (u) value as the first item of the returned tuple", ))?; if type_ == ResponseType::Success { let data: T = seq.next_element()?.ok_or_else(|| A::Error::custom( "Failed to deserialize the response. Expected a vardict (a{sv}) with the returned results", ))?; Ok((type_, Some(data))) } else { Ok((type_, None)) } } } let visitor = ResponseVisitor::(PhantomData); let response: (ResponseType, Option) = deserializer.deserialize_tuple(2, visitor)?; Ok(response.into()) } } impl Serialize for Response where T: for<'de> Deserialize<'de> + Serialize + Type, { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { let mut map = serializer.serialize_tuple(2)?; match self { Self::Err(err) => { map.serialize_element(&ResponseType::from(*err))?; map.serialize_element(&HashMap::<&str, Value<'_>>::new())?; } Self::Ok(response) => { map.serialize_element(&ResponseType::Success)?; map.serialize_element(response)?; } }; map.end() } } #[doc(hidden)] impl From<(ResponseType, Option)> for Response where T: for<'de> Deserialize<'de> + Type, { fn from(f: (ResponseType, Option)) -> Self { match f.0 { ResponseType::Success => { Response::Ok(f.1.expect("Expected a valid response, found nothing.")) } ResponseType::Cancelled => Response::Err(ResponseError::Cancelled), ResponseType::Other => Response::Err(ResponseError::Other), } } } #[derive(Debug, Copy, PartialEq, Eq, Hash, Clone)] /// An error returned a portal request caused by either the user cancelling the /// request or something else. pub enum ResponseError { /// The user canceled the request. Cancelled, /// Something else happened. Other, } impl std::error::Error for ResponseError {} impl std::fmt::Display for ResponseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Cancelled => f.write_str("Cancelled"), Self::Other => f.write_str("Other"), } } } #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Type)] #[doc(hidden)] enum ResponseType { /// Success, the request is carried out. Success = 0, /// The user cancelled the interaction. Cancelled = 1, /// The user interaction was ended in some other way. Other = 2, } #[doc(hidden)] impl From for ResponseType { fn from(err: ResponseError) -> Self { match err { ResponseError::Other => Self::Other, ResponseError::Cancelled => Self::Cancelled, } } } /// The Request interface is shared by all portal interfaces. /// When a portal method is called, the reply includes a handle (i.e. object /// path) for a Request object, which will stay alive for the duration of the /// user interaction related to the method call. /// /// The portal indicates that a portal request interaction is over by emitting /// the "Response" signal on the Request object. /// /// The application can abort the interaction calling /// [`close()`][`Request::close`] on the Request object. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.Request`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html). #[doc(alias = "org.freedesktop.portal.Request")] pub struct Request( Proxy<'static>, SignalStream<'static>, Mutex>>, PhantomData, ) where T: for<'de> Deserialize<'de> + Type + Debug; impl Request where T: for<'de> Deserialize<'de> + Type + Debug, { pub(crate) async fn new

(path: P) -> Result, Error> where P: TryInto>, P::Error: Into, { let proxy = Proxy::new_desktop_with_path("org.freedesktop.portal.Request", path).await?; // Start listening for a response signal the moment request is created let stream = proxy.receive_signal("Response").await?; Ok(Self(proxy, stream, Default::default(), PhantomData)) } pub(crate) async fn from_unique_name(handle_token: &HandleToken) -> Result, Error> { let path = Proxy::unique_name("/org/freedesktop/portal/desktop/request", handle_token).await?; #[cfg(feature = "tracing")] tracing::info!("Creating a org.freedesktop.portal.Request {}", path); Self::new(path).await } pub(crate) async fn prepare_response(&mut self) -> Result<(), Error> { let message = self.1.next().await.ok_or(Error::NoResponse)?; #[cfg(feature = "tracing")] tracing::info!("Received signal 'Response' on '{}'", self.0.interface()); let response = match message.body().deserialize::>()? { Response::Err(e) => Err(e.into()), Response::Ok(r) => Ok(r), }; #[cfg(feature = "tracing")] tracing::debug!("Received response {:#?}", response); let r = response as Result; *self.2.get_mut().unwrap() = Some(r); Ok(()) } /// The corresponding response if the request was successful. /// /// # Specifications /// /// See also [`Response`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html#org-freedesktop-portal-request-response). pub fn response(&self) -> Result { // It should be safe to unwrap here as we are sure we have received a response // by the time the user calls response self.2.lock().unwrap().take().unwrap() } /// Closes the portal request to which this object refers and ends all /// related user interaction (dialogs, etc). A Response signal will not /// be emitted in this case. /// /// # Specifications /// /// See also [`Close`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html#org-freedesktop-portal-request-close). #[doc(alias = "Close")] pub async fn close(&self) -> Result<(), Error> { self.0.call("Close", &()).await } pub(crate) fn path(&self) -> &ObjectPath<'_> { self.0.path() } } impl Debug for Request where T: for<'de> Deserialize<'de> + Type + Debug, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("Request") .field(&self.path().as_str()) .finish() } } #[cfg(test)] mod tests { use zbus::zvariant::Value; use super::*; #[test] fn response_signature() { use crate::desktop::account::UserInformation; assert_eq!( <(ResponseType, HashMap<&str, Value<'_>>)>::signature(), Response::<()>::signature(), ); assert_eq!( <(ResponseType, UserInformation)>::signature(), Response::::signature(), ); assert_eq!(Response::<()>::signature(), "(ua{sv})"); } } ashpd-0.9.1/src/desktop/screencast.rs000064400000000000000000000344441046102023000157030ustar 00000000000000//! Start a screencast session and get the PipeWire remote of it. //! //! # Examples //! //! How to create a screen cast session & start it. //! The portal is currently useless without PipeWire & Rust support. //! //! ```rust,no_run //! use ashpd::{ //! desktop::{ //! screencast::{CursorMode, Screencast, SourceType}, //! PersistMode, //! }, //! WindowIdentifier, //! }; //! //! async fn run() -> ashpd::Result<()> { //! let proxy = Screencast::new().await?; //! let session = proxy.create_session().await?; //! proxy //! .select_sources( //! &session, //! CursorMode::Metadata, //! SourceType::Monitor | SourceType::Window, //! true, //! None, //! PersistMode::DoNot, //! ) //! .await?; //! //! let response = proxy //! .start(&session, &WindowIdentifier::default()) //! .await? //! .response()?; //! response.streams().iter().for_each(|stream| { //! println!("node id: {}", stream.pipe_wire_node_id()); //! println!("size: {:?}", stream.size()); //! println!("position: {:?}", stream.position()); //! }); //! Ok(()) //! } //! ``` use std::{collections::HashMap, fmt::Debug, os::fd::OwnedFd}; use enumflags2::{bitflags, BitFlags}; use futures_util::TryFutureExt; use serde::Deserialize; use serde_repr::{Deserialize_repr, Serialize_repr}; use zbus::zvariant::{self, DeserializeDict, SerializeDict, Type, Value}; use super::{ remote_desktop::RemoteDesktop, session::SessionPortal, HandleToken, PersistMode, Request, Session, }; use crate::{desktop::session::CreateSessionResponse, proxy::Proxy, Error, WindowIdentifier}; #[bitflags] #[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Copy, Clone, Debug, Type)] #[repr(u32)] #[doc(alias = "XdpOutputType")] /// A bit flag for the available sources to record. pub enum SourceType { #[doc(alias = "XDP_OUTPUT_MONITOR")] /// A monitor. Monitor, #[doc(alias = "XDP_OUTPUT_WINDOW")] /// A specific window Window, #[doc(alias = "XDP_OUTPUT_VIRTUAL")] /// Virtual Virtual, } #[bitflags] #[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Debug, Copy, Clone, Type)] #[repr(u32)] #[doc(alias = "XdpCursorMode")] /// A bit flag for the possible cursor modes. pub enum CursorMode { #[doc(alias = "XDP_CURSOR_MODE_HIDDEN")] /// The cursor is not part of the screen cast stream. Hidden, #[doc(alias = "XDP_CURSOR_MODE_EMBEDDED")] /// The cursor is embedded as part of the stream buffers. Embedded, #[doc(alias = "XDP_CURSOR_MODE_METADATA")] /// The cursor is not part of the screen cast stream, but sent as PipeWire /// stream metadata. Metadata, } #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`Screencast::create_session`] request. #[zvariant(signature = "dict")] struct CreateSessionOptions { /// A string that will be used as the last element of the handle. handle_token: HandleToken, /// A string that will be used as the last element of the session handle. session_handle_token: HandleToken, } #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`Screencast::select_sources`] request. #[zvariant(signature = "dict")] struct SelectSourcesOptions { /// A string that will be used as the last element of the handle. handle_token: HandleToken, /// What types of content to record. types: Option>, /// Whether to allow selecting multiple sources. multiple: Option, /// Determines how the cursor will be drawn in the screen cast stream. cursor_mode: Option, restore_token: Option, persist_mode: Option, } impl SelectSourcesOptions { /// Sets whether to allow selecting multiple sources. #[must_use] pub fn multiple(mut self, multiple: impl Into>) -> Self { self.multiple = multiple.into(); self } /// Sets how the cursor will be drawn on the screen cast stream. #[must_use] pub fn cursor_mode(mut self, cursor_mode: impl Into>) -> Self { self.cursor_mode = cursor_mode.into(); self } /// Sets the types of content to record. #[must_use] pub fn types(mut self, types: impl Into>>) -> Self { self.types = types.into(); self } #[must_use] pub fn persist_mode(mut self, persist_mode: impl Into>) -> Self { self.persist_mode = persist_mode.into(); self } #[must_use] pub fn restore_token<'a>(mut self, token: impl Into>) -> Self { self.restore_token = token.into().map(ToOwned::to_owned); self } } #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`Screencast::start`] request. #[zvariant(signature = "dict")] struct StartCastOptions { /// A string that will be used as the last element of the handle. handle_token: HandleToken, } #[derive(DeserializeDict, Type)] /// A response to a [`Screencast::start`] request. #[zvariant(signature = "dict")] pub struct Streams { streams: Vec, restore_token: Option, } impl Streams { /// The session restore token. pub fn restore_token(&self) -> Option<&str> { self.restore_token.as_deref() } /// The list of streams. pub fn streams(&self) -> &[Stream] { &self.streams } } impl Debug for Streams { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("Streams") .field(&self.restore_token) .field(&self.streams) .finish() } } #[derive(Clone, Deserialize, Type)] /// A PipeWire stream. pub struct Stream(u32, StreamProperties); impl Stream { /// The PipeWire stream Node ID pub fn pipe_wire_node_id(&self) -> u32 { self.0 } /// A tuple consisting of the position (x, y) in the compositor coordinate /// space. /// /// **Note** the position may not be equivalent to a position in a pixel /// coordinate space. Only available for monitor streams. pub fn position(&self) -> Option<(i32, i32)> { self.1.position } /// A tuple consisting of (width, height). /// The size represents the size of the stream as it is displayed in the /// compositor coordinate space. /// /// **Note** the size may not be equivalent to a size in a pixel coordinate /// space. The size may differ from the size of the stream. pub fn size(&self) -> Option<(i32, i32)> { self.1.size } /// The source type of the stream. pub fn source_type(&self) -> Option { self.1.source_type } /// The stream identifier. pub fn id(&self) -> Option<&str> { self.1.id.as_deref() } // TODO Added in version 5 of the interface. /// The stream mapping id. pub fn mapping_id(&self) -> Option<&str> { self.1.mapping_id.as_deref() } } impl Debug for Stream { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Stream") .field("pipewire_node_id", &self.pipe_wire_node_id()) .field("position", &self.position()) .field("size", &self.size()) .field("source_type", &self.source_type()) .field("id", &self.id()) .finish() } } #[derive(Clone, DeserializeDict, Type, Debug)] /// The stream properties. #[zvariant(signature = "dict")] struct StreamProperties { id: Option, position: Option<(i32, i32)>, size: Option<(i32, i32)>, source_type: Option, mapping_id: Option, } /// The interface lets sandboxed applications create screen cast sessions. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.ScreenCast`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.ScreenCast")] pub struct Screencast<'a>(Proxy<'a>); impl<'a> Screencast<'a> { /// Create a new instance of [`Screencast`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.ScreenCast").await?; Ok(Self(proxy)) } /// Create a screen cast session. /// /// # Specifications /// /// See also [`CreateSession`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html#org-freedesktop-portal-screencast-createsession). #[doc(alias = "CreateSession")] #[doc(alias = "xdp_portal_create_screencast_session")] pub async fn create_session(&self) -> Result, Error> { let options = CreateSessionOptions::default(); let (request, proxy) = futures_util::try_join!( self.0 .request::(&options.handle_token, "CreateSession", &options) .into_future(), Session::from_unique_name(&options.session_handle_token).into_future(), )?; assert_eq!(proxy.path(), &request.response()?.session_handle.as_ref()); Ok(proxy) } /// Open a file descriptor to the PipeWire remote where the screen cast /// streams are available. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`Screencast::create_session`]. /// /// # Returns /// /// File descriptor of an open PipeWire remote. /// /// # Specifications /// /// See also [`OpenPipeWireRemote`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html#org-freedesktop-portal-screencast-openpipewireremote). #[doc(alias = "OpenPipeWireRemote")] pub async fn open_pipe_wire_remote( &self, session: &Session<'_, impl HasScreencastSession>, ) -> Result { // `options` parameter doesn't seems to be used yet // see https://github.com/flatpak/xdg-desktop-portal/blob/master/src/screen-cast.c#L812 let options: HashMap<&str, Value<'_>> = HashMap::new(); let fd = self .0 .call::("OpenPipeWireRemote", &(session, options)) .await?; Ok(fd.into()) } /// Configure what the screen cast session should record. /// This method must be called before starting the session. /// /// Passing invalid input to this method will cause the session to be /// closed. An application may only attempt to select sources once per /// session. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`Screencast::create_session`]. /// * `cursor_mode` - Sets how the cursor will be drawn on the screen cast /// stream. /// * `types` - Sets the types of content to record. /// * `multiple`- Sets whether to allow selecting multiple sources. /// /// # Specifications /// /// See also [`SelectSources`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html#org-freedesktop-portal-screencast-selectsources). #[doc(alias = "SelectSources")] pub async fn select_sources( &self, session: &Session<'_, impl HasScreencastSession>, cursor_mode: CursorMode, types: BitFlags, multiple: bool, restore_token: Option<&str>, persist_mode: PersistMode, ) -> Result, Error> { let options = SelectSourcesOptions::default() .cursor_mode(cursor_mode) .multiple(multiple) .types(types) .persist_mode(persist_mode) .restore_token(restore_token); self.0 .empty_request(&options.handle_token, "SelectSources", &(session, &options)) .await } /// Start the screen cast session. /// /// This will typically result the portal presenting a dialog letting the /// user do the selection set up by `select_sources`. /// /// An application can only attempt start a session once. /// /// # Arguments /// /// * `session` - A [`Session`], created with /// [`create_session()`][`Screencast::create_session`]. /// * `identifier` - Identifier for the application window. /// /// # Return /// /// A list of [`Stream`] and an optional restore token. /// /// # Specifications /// /// See also [`Start`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html#org-freedesktop-portal-screencast-start). #[doc(alias = "Start")] pub async fn start( &self, session: &Session<'_, impl HasScreencastSession>, identifier: &WindowIdentifier, ) -> Result, Error> { let options = StartCastOptions::default(); self.0 .request( &options.handle_token, "Start", &(session, &identifier, &options), ) .await } /// Available cursor mode. /// /// # Specifications /// /// See also [`AvailableCursorModes`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html#org-freedesktop-portal-screencast-availablecursormodes). #[doc(alias = "AvailableCursorModes")] pub async fn available_cursor_modes(&self) -> Result, Error> { self.0.property_versioned("AvailableCursorModes", 2).await } /// Available source types. /// /// # Specifications /// /// See also [`AvailableSourceTypes`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html#org-freedesktop-portal-screencast-availablesourcetypes). #[doc(alias = "AvailableSourceTypes")] pub async fn available_source_types(&self) -> Result, Error> { self.0.property("AvailableSourceTypes").await } } impl<'a> std::ops::Deref for Screencast<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } impl SessionPortal for Screencast<'_> {} /// Defines which portals session can be used in a screen-cast. pub trait HasScreencastSession: SessionPortal {} impl HasScreencastSession for Screencast<'_> {} impl HasScreencastSession for RemoteDesktop<'_> {} ashpd-0.9.1/src/desktop/screenshot.rs000064400000000000000000000147001046102023000157170ustar 00000000000000//! Take a screenshot or pick a color. //! //! Wrapper of the DBus interface: [`org.freedesktop.portal.Screenshot`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Screenshot.html). //! //! # Examples //! //! ## Taking a screenshot //! //! ```rust,no_run //! use ashpd::desktop::screenshot::Screenshot; //! //! async fn run() -> ashpd::Result<()> { //! let response = Screenshot::request() //! .interactive(true) //! .modal(true) //! .send() //! .await? //! .response()?; //! println!("URI: {}", response.uri()); //! Ok(()) //! } //! ``` //! //! ## Picking a color //! //! ```rust,no_run //! use ashpd::desktop::Color; //! //! async fn run() -> ashpd::Result<()> { //! let color = Color::pick().send().await?.response()?; //! println!("({}, {}, {})", color.red(), color.green(), color.blue()); //! //! Ok(()) //! } //! ``` use std::fmt::Debug; use zbus::zvariant::{DeserializeDict, SerializeDict, Type}; use super::{HandleToken, Request}; use crate::{desktop::Color, proxy::Proxy, Error, WindowIdentifier}; #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct ScreenshotOptions { handle_token: HandleToken, modal: Option, interactive: Option, } #[derive(DeserializeDict, Type)] #[zvariant(signature = "dict")] /// The response of a [`ScreenshotRequest`] request. pub struct Screenshot { uri: url::Url, } impl Screenshot { /// Creates a new builder-pattern struct instance to construct /// [`Screenshot`]. /// /// This method returns an instance of [`ScreenshotRequest`]. pub fn request() -> ScreenshotRequest { ScreenshotRequest::default() } /// The screenshot URI. pub fn uri(&self) -> &url::Url { &self.uri } } impl Debug for Screenshot { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.uri.as_str()) } } #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct ColorOptions { handle_token: HandleToken, } #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.Screenshot")] struct ScreenshotProxy<'a>(Proxy<'a>); impl<'a> ScreenshotProxy<'a> { /// Create a new instance of [`ScreenshotProxy`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.Screenshot").await?; Ok(Self(proxy)) } /// Obtains the color of a single pixel. /// /// # Arguments /// /// * `identifier` - Identifier for the application window. /// /// # Specifications /// /// See also [`PickColor`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Screenshot.html#org-freedesktop-portal-screenshot-pickcolor). #[doc(alias = "PickColor")] #[doc(alias = "xdp_portal_pick_color")] pub async fn pick_color( &self, identifier: &WindowIdentifier, options: ColorOptions, ) -> Result, Error> { self.0 .request(&options.handle_token, "PickColor", &(&identifier, &options)) .await } /// Takes a screenshot. /// /// # Arguments /// /// * `identifier` - Identifier for the application window. /// * `interactive` - Sets whether the dialog should offer customization /// before a screenshot or not. /// * `modal` - Sets whether the dialog should be a modal. /// /// # Returns /// /// The screenshot URI. /// /// # Specifications /// /// See also [`Screenshot`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Screenshot.html#org-freedesktop-portal-screenshot-screenshot). #[doc(alias = "Screenshot")] #[doc(alias = "xdp_portal_take_screenshot")] pub async fn screenshot( &self, identifier: &WindowIdentifier, options: ScreenshotOptions, ) -> Result, Error> { self.0 .request( &options.handle_token, "Screenshot", &(&identifier, &options), ) .await } } impl<'a> std::ops::Deref for ScreenshotProxy<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } #[derive(Debug, Default)] #[doc(alias = "xdp_portal_pick_color")] /// A [builder-pattern] type to construct [`Color`]. /// /// [builder-pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html pub struct ColorRequest { identifier: WindowIdentifier, options: ColorOptions, } impl ColorRequest { #[must_use] /// Sets a window identifier. pub fn identifier(mut self, identifier: WindowIdentifier) -> Self { self.identifier = identifier; self } /// Build the [`Color`]. pub async fn send(self) -> Result, Error> { let proxy = ScreenshotProxy::new().await?; proxy.pick_color(&self.identifier, self.options).await } } impl Color { /// Creates a new builder-pattern struct instance to construct /// [`Color`]. /// /// This method returns an instance of [`ColorRequest`]. pub fn pick() -> ColorRequest { ColorRequest::default() } } #[derive(Debug, Default)] #[doc(alias = "xdp_portal_take_screenshot")] /// A [builder-pattern] type to construct a screenshot [`Screenshot`]. /// /// [builder-pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html pub struct ScreenshotRequest { options: ScreenshotOptions, identifier: WindowIdentifier, } impl ScreenshotRequest { #[must_use] /// Sets a window identifier. pub fn identifier(mut self, identifier: impl Into>) -> Self { self.identifier = identifier.into().unwrap_or_default(); self } /// Sets whether the dialog should be a modal. #[must_use] pub fn modal(mut self, modal: impl Into>) -> Self { self.options.modal = modal.into(); self } /// Sets whether the dialog should offer customization before a screenshot /// or not. #[must_use] pub fn interactive(mut self, interactive: impl Into>) -> Self { self.options.interactive = interactive.into(); self } /// Build the [`Screenshot`]. pub async fn send(self) -> Result, Error> { let proxy = ScreenshotProxy::new().await?; proxy.screenshot(&self.identifier, self.options).await } } ashpd-0.9.1/src/desktop/secret.rs000064400000000000000000000060271046102023000150320ustar 00000000000000//! # Examples //! //! ```rust,no_run //! use std::{io::Read, os::fd::AsFd}; //! //! use ashpd::desktop::secret::Secret; //! //! async fn run() -> ashpd::Result<()> { //! let secret = Secret::new().await?; //! //! let (mut x1, x2) = std::os::unix::net::UnixStream::pair()?; //! secret.retrieve(&x2.as_fd()).await?; //! drop(x2); //! let mut buf = Vec::new(); //! x1.read_to_end(&mut buf)?; //! //! Ok(()) //! } //! ``` use std::os::fd::{AsFd, BorrowedFd}; #[cfg(feature = "async-std")] use async_net::unix::UnixStream; #[cfg(feature = "async-std")] use futures_util::AsyncReadExt; #[cfg(feature = "tokio")] use tokio::{io::AsyncReadExt, net::UnixStream}; use zbus::zvariant::{Fd, SerializeDict, Type}; use super::{HandleToken, Request}; use crate::{proxy::Proxy, Error}; #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`Secret::retrieve`] request. #[zvariant(signature = "dict")] struct RetrieveOptions { handle_token: HandleToken, /// A string returned by a previous call to `retrieve`. /// TODO: seems to not be used by the portal... token: Option, } /// The interface lets sandboxed applications retrieve a per-application secret. /// /// The secret can then be used for encrypting confidential data inside the /// sandbox. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.Secret`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Secret.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.Secret")] pub struct Secret<'a>(Proxy<'a>); impl<'a> Secret<'a> { /// Create a new instance of [`Secret`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.Secret").await?; Ok(Self(proxy)) } /// Retrieves a master secret for a sandboxed application. /// /// # Arguments /// /// * `fd` - Writaeble file descriptor for transporting the secret. /// /// # Specifications /// /// See also [`RetrieveSecret`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Secret.html#org-freedesktop-portal-secret-retrievesecret) #[doc(alias = "RetrieveSecret")] pub async fn retrieve(&self, fd: &BorrowedFd<'_>) -> Result, Error> { let options = RetrieveOptions::default(); self.0 .empty_request( &options.handle_token, "RetrieveSecret", &(Fd::from(fd), &options), ) .await } } impl<'a> std::ops::Deref for Secret<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } /// A handy wrapper around [`Secret::retrieve`]. /// /// It crates a UnixStream internally for receiving the secret. pub async fn retrieve() -> Result, Error> { let proxy = Secret::new().await?; let (mut x1, x2) = UnixStream::pair()?; proxy.retrieve(&x2.as_fd()).await?; drop(x2); let mut buf = Vec::new(); x1.read_to_end(&mut buf).await?; Ok(buf) } ashpd-0.9.1/src/desktop/session.rs000064400000000000000000000124151046102023000152260ustar 00000000000000use std::{collections::HashMap, fmt::Debug, marker::PhantomData}; use futures_util::Stream; use serde::{Deserialize, Serialize, Serializer}; use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Type}; use crate::{desktop::HandleToken, proxy::Proxy, Error}; pub type SessionDetails = HashMap; /// Shared by all portal interfaces that involve long lived sessions. /// /// When a method that creates a session is called, if successful, the reply /// will include a session handle (i.e. object path) for a Session object, which /// will stay alive for the duration of the session. /// /// The duration of the session is defined by the interface that creates it. /// For convenience, the interface contains a method [`Session::close`], /// and a signal [`Session::receive_closed`]. Whether it is allowed to /// directly call [`Session::close`] depends on the interface. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.Session`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Session.html). #[derive(Type)] #[doc(alias = "org.freedesktop.portal.Session")] #[zvariant(signature = "o")] pub struct Session<'a, T>(Proxy<'a>, PhantomData) where T: SessionPortal; impl<'a, T> Session<'a, T> where T: SessionPortal, { /// Create a new instance of [`Session`]. /// /// **Note** A [`Session`] is not supposed to be created manually. pub(crate) async fn new

(path: P) -> Result, Error> where P: TryInto>, P::Error: Into, { let proxy = Proxy::new_desktop_with_path("org.freedesktop.portal.Session", path).await?; Ok(Self(proxy, PhantomData)) } pub(crate) async fn from_unique_name( handle_token: &HandleToken, ) -> Result, crate::Error> { let path = Proxy::unique_name("/org/freedesktop/portal/desktop/session", handle_token).await?; #[cfg(feature = "tracing")] tracing::info!("Creating a org.freedesktop.portal.Session {}", path); Self::new(path).await } /// Emitted when a session is closed. /// /// # Specifications /// /// See also [`Closed`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Session.html#org-freedesktop-portal-session-closed). #[doc(alias = "Closed")] pub async fn receive_closed(&self) -> Result, Error> { self.0.signal("Closed").await } /// Closes the portal session to which this object refers and ends all /// related user interaction (dialogs, etc). /// /// # Specifications /// /// See also [`Close`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Session.html#org-freedesktop-portal-session-close). #[doc(alias = "Close")] pub async fn close(&self) -> Result<(), Error> { self.0.call("Close", &()).await } pub(crate) fn path(&self) -> &ObjectPath<'_> { self.0.path() } } impl<'a, T> Serialize for Session<'a, T> where T: SessionPortal, { fn serialize(&self, serializer: S) -> Result where S: Serializer, { ObjectPath::serialize(self.path(), serializer) } } impl<'a, T> Debug for Session<'a, T> where T: SessionPortal, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("Session") .field(&self.path().as_str()) .finish() } } /// Portals that have a long-lived interaction pub trait SessionPortal {} /// A response to a `create_session` request. #[derive(Type, Debug)] #[zvariant(signature = "dict")] pub(crate) struct CreateSessionResponse { pub(crate) session_handle: OwnedObjectPath, } // Context: Various portal were expected to actually return an OwnedObjectPath // but unfortunately this wasn't the case when the portals were implemented in // xdp. Fixing that would be an API break as well... // See // The Location, ScreenCast, Remote Desktop, Global Shortcuts and Inhibit // portals `CreateSession` calls are all affected. // // So in order to be future proof, we try to deserialize the `session_handle` // key as a string and fallback to an object path in case the situation gets // resolved in the future. impl<'de> Deserialize<'de> for CreateSessionResponse { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let map: HashMap = HashMap::deserialize(deserializer)?; let session_handle = map.get("session_handle").ok_or_else(|| { serde::de::Error::custom( "CreateSessionResponse failed to deserialize. Couldn't find a session_handle", ) })?; let path = if let Ok(object_path_str) = session_handle.downcast_ref::<&str>() { ObjectPath::try_from(object_path_str).unwrap() } else if let Ok(object_path) = session_handle.downcast_ref::>() { object_path } else { return Err(serde::de::Error::custom( "Wrong session_handle type. Expected `s` or `o`.", )); }; Ok(Self { session_handle: path.into(), }) } } ashpd-0.9.1/src/desktop/settings.rs000064400000000000000000000233231046102023000154030ustar 00000000000000//! ```rust,no_run //! use ashpd::desktop::settings::Settings; //! use futures_util::StreamExt; //! //! async fn run() -> ashpd::Result<()> { //! let proxy = Settings::new().await?; //! //! let clock_format = proxy //! .read::("org.gnome.desktop.interface", "clock-format") //! .await?; //! println!("{:#?}", clock_format); //! //! let settings = proxy.read_all(&["org.gnome.desktop.interface"]).await?; //! println!("{:#?}", settings); //! //! let setting = proxy //! .receive_setting_changed() //! .await? //! .next() //! .await //! .expect("Stream exhausted"); //! println!("{}", setting.namespace()); //! println!("{}", setting.key()); //! println!("{:#?}", setting.value()); //! //! Ok(()) //! } //! ``` use std::{collections::HashMap, convert::TryFrom, fmt::Debug, future::ready}; use futures_util::{Stream, StreamExt}; use serde::{Deserialize, Serialize}; use zbus::zvariant::{OwnedValue, Type, Value}; use crate::{desktop::Color, proxy::Proxy, Error}; /// A HashMap of the settings found on a specific namespace. pub type Namespace = HashMap; #[derive(Deserialize, Type)] /// A specific `namespace.key = value` setting. pub struct Setting(String, String, OwnedValue); impl Setting { /// The setting namespace. pub fn namespace(&self) -> &str { &self.0 } /// The setting key. pub fn key(&self) -> &str { &self.1 } /// The setting value. pub fn value(&self) -> &OwnedValue { &self.2 } } impl std::fmt::Debug for Setting { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Setting") .field("namespace", &self.namespace()) .field("key", &self.key()) .field("value", self.value()) .finish() } } /// The system's preferred color scheme #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdColorScheme"))] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum ColorScheme { /// No preference NoPreference, /// Prefers dark appearance PreferDark, /// Prefers light appearance PreferLight, } impl TryFrom for ColorScheme { type Error = Error; fn try_from(value: OwnedValue) -> Result { TryFrom::::try_from(value.into()) } } impl TryFrom> for ColorScheme { type Error = Error; fn try_from(value: Value) -> Result { Ok(match u32::try_from(value)? { 1 => Self::PreferDark, 2 => Self::PreferLight, _ => Self::NoPreference, }) } } /// The system's preferred contrast level #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdContrast"))] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Contrast { /// No preference NoPreference, /// Higher contrast High, } impl TryFrom for Contrast { type Error = Error; fn try_from(value: OwnedValue) -> Result { TryFrom::::try_from(value.into()) } } impl TryFrom> for Contrast { type Error = Error; fn try_from(value: Value) -> Result { Ok(match u32::try_from(value)? { 1 => Self::High, _ => Self::NoPreference, }) } } const APPEARANCE_NAMESPACE: &str = "org.freedesktop.appearance"; const COLOR_SCHEME_KEY: &str = "color-scheme"; const ACCENT_COLOR_SCHEME_KEY: &str = "accent-color"; const CONTRAST_KEY: &str = "contrast"; /// The interface provides read-only access to a small number of host settings /// required for toolkits similar to XSettings. It is not for general purpose /// settings. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.Settings`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.Settings")] pub struct Settings<'a>(Proxy<'a>); impl<'a> Settings<'a> { /// Create a new instance of [`Settings`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.Settings").await?; Ok(Self(proxy)) } /// Reads a single value. Returns an error on any unknown namespace or key. /// /// # Arguments /// /// * `namespaces` - List of namespaces to filter results by. /// /// If `namespaces` is an empty array or contains an empty string it matches /// all. Globing is supported but only for trailing sections, e.g. /// `org.example.*`. /// /// # Returns /// /// A `HashMap` of namespaces to its keys and values. /// /// # Specifications /// /// See also [`ReadAll`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html#org-freedesktop-portal-settings-readall). #[doc(alias = "ReadAll")] pub async fn read_all( &self, namespaces: &[impl AsRef + Type + Serialize + Debug], ) -> Result, Error> { self.0.call("ReadAll", &(namespaces)).await } /// Reads a single value. Returns an error on any unknown namespace or key. /// /// # Arguments /// /// * `namespace` - Namespace to look up key in. /// * `key` - The key to get. /// /// # Returns /// /// The value for `key` as a `zvariant::OwnedValue`. /// /// # Specifications /// /// See also [`Read`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html#org-freedesktop-portal-settings-read). #[doc(alias = "Read")] #[doc(alias = "ReadOne")] pub async fn read(&self, namespace: &str, key: &str) -> Result where T: TryFrom, Error: From<>::Error>, { let value = self.0.call::("Read", &(namespace, key)).await?; if let Ok(v) = value.downcast_ref::() { T::try_from(v.try_to_owned()?).map_err(From::from) } else { T::try_from(value).map_err(From::from) } } /// Retrieves the system's preferred accent color pub async fn accent_color(&self) -> Result { self.read::<(f64, f64, f64)>(APPEARANCE_NAMESPACE, ACCENT_COLOR_SCHEME_KEY) .await .map(Color::new) } /// Retrieves the system's preferred color scheme pub async fn color_scheme(&self) -> Result { self.read::(APPEARANCE_NAMESPACE, COLOR_SCHEME_KEY) .await } /// Retrieves the system's preferred contrast level pub async fn contrast(&self) -> Result { self.read::(APPEARANCE_NAMESPACE, CONTRAST_KEY) .await } /// Listen to changes of the system's preferred color scheme pub async fn receive_color_scheme_changed( &self, ) -> Result, Error> { Ok(self .receive_setting_changed_with_args(APPEARANCE_NAMESPACE, COLOR_SCHEME_KEY) .await? .filter_map(|t| ready(t.ok()))) } /// Listen to changes of the system's accent color pub async fn receive_accent_color_changed(&self) -> Result, Error> { Ok(self .receive_setting_changed_with_args::<(f64, f64, f64)>( APPEARANCE_NAMESPACE, ACCENT_COLOR_SCHEME_KEY, ) .await? .filter_map(|t| ready(t.ok().map(Color::new)))) } /// Listen to changes of the system's contrast level pub async fn receive_contrast_changed(&self) -> Result, Error> { Ok(self .receive_setting_changed_with_args(APPEARANCE_NAMESPACE, CONTRAST_KEY) .await? .filter_map(|t| ready(t.ok()))) } /// Signal emitted when a setting changes. /// /// # Specifications /// /// See also [`SettingChanged`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html#org-freedesktop-portal-settings-settingchanged). #[doc(alias = "SettingChanged")] pub async fn receive_setting_changed(&self) -> Result, Error> { self.0.signal("SettingChanged").await } /// Similar to [Self::receive_setting_changed] /// but allows you to filter specific settings. /// /// # Example /// ```rust,no_run /// use ashpd::desktop::settings::{ColorScheme, Settings}; /// use futures_util::StreamExt; /// /// # async fn run() -> ashpd::Result<()> { /// let settings = Settings::new().await?; /// while let Some(Ok(scheme)) = settings /// .receive_setting_changed_with_args::( /// "org.freedesktop.appearance", /// "color-scheme", /// ) /// .await? /// .next() /// .await /// { /// println!("{:#?}", scheme); /// } /// # Ok(()) /// # } /// ``` pub async fn receive_setting_changed_with_args( &self, namespace: &str, key: &str, ) -> Result>, Error> where T: TryFrom, Error: From<>::Error>, { Ok(self .0 .signal_with_args::("SettingChanged", &[(0, namespace), (1, key)]) .await? .map(|x| T::try_from(x.2).map_err(From::from))) } } impl<'a> std::ops::Deref for Settings<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } ashpd-0.9.1/src/desktop/trash.rs000064400000000000000000000064551046102023000146730ustar 00000000000000//! Move a file to the trash. //! //! # Examples //! //! //! ```rust,no_run //! use std::{fs::File, os::fd::AsFd}; //! //! use ashpd::desktop::trash; //! //! async fn run() -> ashpd::Result<()> { //! let file = File::open("/home/bilelmoussaoui/adwaita-night.jpg").unwrap(); //! trash::trash_file(&file.as_fd()).await?; //! Ok(()) //! } //! ``` //! //! Or by using the Proxy directly //! //! ```rust,no_run //! use std::{fs::File, os::fd::AsFd}; //! //! use ashpd::desktop::trash::TrashProxy; //! //! async fn run() -> ashpd::Result<()> { //! let file = File::open("/home/bilelmoussaoui/Downloads/adwaita-night.jpg").unwrap(); //! let proxy = TrashProxy::new().await?; //! proxy.trash_file(&file.as_fd()).await?; //! Ok(()) //! } //! ``` use std::os::fd::BorrowedFd; use serde_repr::{Deserialize_repr, Serialize_repr}; use zbus::zvariant::{Fd, Type}; use crate::{error::PortalError, proxy::Proxy, Error}; #[derive(Debug, Deserialize_repr, Serialize_repr, PartialEq, Type)] #[repr(u32)] enum TrashStatus { Failed = 0, Succeeded = 1, } /// The interface lets sandboxed applications send files to the trashcan. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.Trash`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Trash.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.Trash")] pub struct TrashProxy<'a>(Proxy<'a>); impl<'a> TrashProxy<'a> { /// Create a new instance of [`TrashProxy`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.Trash").await?; Ok(Self(proxy)) } /// Sends a file to the trashcan. /// Applications are allowed to trash a file if they can open it in /// read/write mode. /// /// # Arguments /// /// * `fd` - The file descriptor. /// /// # Specifications /// /// See also [`TrashFile`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Trash.html#org-freedesktop-portal-trash-trashfile). #[doc(alias = "TrashFile")] #[doc(alias = "xdp_portal_trash_file")] pub async fn trash_file(&self, fd: &BorrowedFd<'_>) -> Result<(), Error> { let status = self.0.call("TrashFile", &(Fd::from(fd))).await?; match status { TrashStatus::Failed => Err(Error::Portal(PortalError::Failed( "Failed to trash file".to_string(), ))), TrashStatus::Succeeded => Ok(()), } } } impl<'a> std::ops::Deref for TrashProxy<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } #[doc(alias = "xdp_portal_trash_file")] /// A handy wrapper around [`TrashProxy::trash_file`]. pub async fn trash_file(fd: &BorrowedFd<'_>) -> Result<(), Error> { let proxy = TrashProxy::new().await?; proxy.trash_file(fd).await } #[cfg(test)] mod test { use super::TrashStatus; #[test] fn status_serde() { #[derive(serde::Serialize, serde::Deserialize)] struct Test { status: TrashStatus, } let status = Test { status: TrashStatus::Failed, }; let x = serde_json::to_string(&status).unwrap(); let y: Test = serde_json::from_str(&x).unwrap(); assert_eq!(y.status, TrashStatus::Failed); } } ashpd-0.9.1/src/desktop/wallpaper.rs000064400000000000000000000146201046102023000155320ustar 00000000000000//! Set a wallpaper on lockscreen, background or both. //! //! Wrapper of the DBus interface: [`org.freedesktop.portal.Wallpaper`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Wallpaper.html). //! //! # Examples //! //! ## Sets a wallpaper from a file: //! //! ```rust,no_run //! use std::{fs::File, os::fd::AsFd}; //! //! use ashpd::desktop::wallpaper::{SetOn, WallpaperRequest}; //! //! async fn run() -> ashpd::Result<()> { //! let file = File::open("/home/bilelmoussaoui/adwaita-day.jpg").unwrap(); //! WallpaperRequest::default() //! .set_on(SetOn::Both) //! .show_preview(true) //! .build_file(&file.as_fd()) //! .await?; //! Ok(()) //! } //! ``` //! //! ## Sets a wallpaper from a URI: //! //! ```rust,no_run //! use ashpd::desktop::wallpaper::{SetOn, WallpaperRequest}; //! //! async fn run() -> ashpd::Result<()> { //! let uri = //! url::Url::parse("file:///home/bilelmoussaoui/Downloads/adwaita-night.jpg").unwrap(); //! WallpaperRequest::default() //! .set_on(SetOn::Both) //! .show_preview(true) //! .build_uri(&uri) //! .await?; //! Ok(()) //! } //! ``` use std::{fmt, os::fd::BorrowedFd, str::FromStr}; use serde::{self, Deserialize, Serialize}; use zbus::zvariant::{Fd, SerializeDict, Type}; use super::Request; use crate::{desktop::HandleToken, proxy::Proxy, Error, WindowIdentifier}; #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdSetOn"))] #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Type)] #[zvariant(signature = "s")] #[serde(rename_all = "lowercase")] /// Where to set the wallpaper on. pub enum SetOn { /// Set the wallpaper only on the lock-screen. Lockscreen, /// Set the wallpaper only on the background. Background, /// Set the wallpaper on both lock-screen and background. Both, } impl fmt::Display for SetOn { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Lockscreen => write!(f, "Lockscreen"), Self::Background => write!(f, "Background"), Self::Both => write!(f, "Both"), } } } impl AsRef for SetOn { fn as_ref(&self) -> &str { match self { Self::Lockscreen => "Lockscreen", Self::Background => "Background", Self::Both => "Both", } } } impl From for &'static str { fn from(s: SetOn) -> Self { match s { SetOn::Lockscreen => "Lockscreen", SetOn::Background => "Background", SetOn::Both => "Both", } } } impl FromStr for SetOn { type Err = Error; fn from_str(s: &str) -> Result { match s { "Lockscreen" => Ok(SetOn::Lockscreen), "Background" => Ok(SetOn::Background), "Both" => Ok(SetOn::Both), _ => Err(Error::ParseError("Failed to parse SetOn, invalid value")), } } } #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct WallpaperOptions { handle_token: HandleToken, #[zvariant(rename = "show-preview")] show_preview: Option, #[zvariant(rename = "set-on")] set_on: Option, } struct WallpaperProxy<'a>(Proxy<'a>); impl<'a> WallpaperProxy<'a> { pub async fn new() -> Result, Error> { let proxy = Proxy::new_desktop("org.freedesktop.portal.Wallpaper").await?; Ok(Self(proxy)) } pub async fn set_wallpaper_file( &self, identifier: &WindowIdentifier, file: &BorrowedFd<'_>, options: WallpaperOptions, ) -> Result, Error> { self.0 .empty_request( &options.handle_token, "SetWallpaperFile", &(&identifier, Fd::from(file), &options), ) .await } pub async fn set_wallpaper_uri( &self, identifier: &WindowIdentifier, uri: &url::Url, options: WallpaperOptions, ) -> Result, Error> { self.0 .empty_request( &options.handle_token, "SetWallpaperURI", &(&identifier, uri, &options), ) .await } } impl<'a> std::ops::Deref for WallpaperProxy<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } #[derive(Debug, Default)] #[doc(alias = "xdp_portal_set_wallpaper")] #[doc(alias = "org.freedesktop.portal.Wallpaper")] /// A [builder-pattern] type to set the wallpaper. /// /// [builder-pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html pub struct WallpaperRequest { identifier: WindowIdentifier, options: WallpaperOptions, } impl WallpaperRequest { #[must_use] /// Sets a window identifier. pub fn identifier(mut self, identifier: impl Into>) -> Self { self.identifier = identifier.into().unwrap_or_default(); self } /// Whether to show a preview of the picture. /// **Note** the portal may decide to show a preview even if this option is /// not set. #[must_use] pub fn show_preview(mut self, show_preview: impl Into>) -> Self { self.options.show_preview = show_preview.into(); self } /// Sets where to set the wallpaper on. #[must_use] pub fn set_on(mut self, set_on: impl Into>) -> Self { self.options.set_on = set_on.into(); self } /// Build using a URI. pub async fn build_uri(self, uri: &url::Url) -> Result, Error> { let proxy = WallpaperProxy::new().await?; proxy .set_wallpaper_uri(&self.identifier, uri, self.options) .await } /// Build using a file. pub async fn build_file(self, file: &BorrowedFd<'_>) -> Result, Error> { let proxy = WallpaperProxy::new().await?; proxy .set_wallpaper_file(&self.identifier, file, self.options) .await } } #[cfg(test)] mod tests { use super::SetOn; #[test] fn serialize_deserialize() { let set_on = SetOn::Both; let string = serde_json::to_string(&set_on).unwrap(); assert_eq!(string, "\"both\""); let decoded = serde_json::from_str(&string).unwrap(); assert_eq!(set_on, decoded); } } ashpd-0.9.1/src/documents/file_transfer.rs000064400000000000000000000171241046102023000167200ustar 00000000000000//! # Examples //! //! ```rust,no_run //! use std::{fs::File, os::fd::AsFd}; //! //! use ashpd::documents::FileTransfer; //! //! async fn run() -> ashpd::Result<()> { //! let proxy = FileTransfer::new().await?; //! //! let key = proxy.start_transfer(true, true).await?; //! let file = File::open("/home/bilelmoussaoui/Downloads/adwaita-night.jpg").unwrap(); //! proxy.add_files(&key, &[&file.as_fd()]).await?; //! //! // The files would be retrieved by another process //! let files = proxy.retrieve_files(&key).await?; //! println!("{:#?}", files); //! //! proxy.stop_transfer(&key).await?; //! //! Ok(()) //! } //! ``` use std::{collections::HashMap, os::fd::BorrowedFd}; use futures_util::Stream; use zbus::zvariant::{Fd, SerializeDict, Type, Value}; use crate::{proxy::Proxy, Error}; #[derive(SerializeDict, Debug, Type, Default)] /// Specified options for a [`FileTransfer::start_transfer`] request. #[zvariant(signature = "dict")] struct TransferOptions { /// Whether to allow the chosen application to write to the files. writeable: Option, /// Whether to stop the transfer automatically after the first /// [`retrieve_files()`][`FileTransfer::retrieve_files`] call. #[zvariant(rename = "autostop")] auto_stop: Option, } impl TransferOptions { /// Sets whether the chosen application can write to the files or not. #[must_use] pub fn writeable(mut self, writeable: impl Into>) -> Self { self.writeable = writeable.into(); self } /// Whether to stop the transfer automatically after the first /// [`retrieve_files()`][`FileTransfer::retrieve_files`] call. #[must_use] pub fn auto_stop(mut self, auto_stop: impl Into>) -> Self { self.auto_stop = auto_stop.into(); self } } /// The interface operates as a middle-man between apps when transferring files /// via drag-and-drop or copy-paste, taking care of the necessary exporting of /// files in the document portal. /// /// Toolkits are expected to use the application/vnd.portal.filetransfer /// mimetype when using this mechanism for file exchange via copy-paste or /// drag-and-drop. /// /// The data that is transmitted with this mimetype should be the key returned /// by the StartTransfer method. Upon receiving this mimetype, the target should /// call RetrieveFiles with the key, to obtain the list of files. The portal /// will take care of exporting files in the document store as necessary to make /// them accessible to the target. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.FileTransfer`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileTransfer.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.FileTransfer")] pub struct FileTransfer<'a>(Proxy<'a>); impl<'a> FileTransfer<'a> { /// Create a new instance of [`FileTransfer`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_documents("org.freedesktop.portal.FileTransfer").await?; Ok(Self(proxy)) } /// Adds files to a session. This method can be called multiple times on a /// given session. **Note** only regular files (not directories) can be /// added. /// /// # Arguments /// /// * `key` - A key returned by /// [`start_transfer()`][`FileTransfer::start_transfer`]. /// * `fds` - A list of file descriptors of the files to register. /// /// # Specifications /// /// See also [`AddFiles`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileTransfer.html#org-freedesktop-portal-filetransfer-addfiles). #[doc(alias = "AddFiles")] pub async fn add_files(&self, key: &str, fds: &[&BorrowedFd<'_>]) -> Result<(), Error> { // `options` parameter doesn't seems to be used yet let options: HashMap<&str, Value<'_>> = HashMap::new(); let files: Vec = fds.iter().map(Fd::from).collect(); self.0.call("AddFiles", &(key, files, options)).await } /// Retrieves files that were previously added to the session with /// [`add_files()`][`FileTransfer::add_files`]. The files will be /// exported in the document portal as-needed for the caller, and they /// will be writeable if the owner of the session allowed it. /// /// # Arguments /// /// * `key` - A key returned by /// [`start_transfer()`][`FileTransfer::start_transfer`]. /// /// # Returns /// /// The list of file paths. /// /// # Specifications /// /// See also [`RetrieveFiles`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileTransfer.html#org-freedesktop-portal-filetransfer-retrievefiles). #[doc(alias = "RetrieveFiles")] pub async fn retrieve_files(&self, key: &str) -> Result, Error> { // `options` parameter doesn't seems to be used yet // see https://github.com/GNOME/gtk/blob/master/gdk/filetransferportal.c#L284 let options: HashMap<&str, Value<'_>> = HashMap::new(); self.0.call("RetrieveFiles", &(key, options)).await } /// Starts a session for a file transfer. /// The caller should call [`add_files()`][`FileTransfer::add_files`] /// at least once, to add files to this session. /// /// # Arguments /// /// * `writeable` - Sets whether the chosen application can write to the /// files or not. /// * `auto_stop` - Whether to stop the transfer automatically after the /// first [`retrieve_files()`][`FileTransfer::retrieve_files`] call. /// /// # Returns /// /// Key that can be passed to /// [`retrieve_files()`][`FileTransfer::retrieve_files`] to obtain the /// files. /// /// # Specifications /// /// See also [`StartTransfer`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileTransfer.html#org-freedesktop-portal-filetransfer-starttransfer). pub async fn start_transfer(&self, writeable: bool, auto_stop: bool) -> Result { let options = TransferOptions::default() .writeable(writeable) .auto_stop(auto_stop); self.0.call("StartTransfer", &(options)).await } /// Ends the transfer. /// Further calls to [`add_files()`][`FileTransfer::add_files`] or /// [`retrieve_files()`][`FileTransfer::retrieve_files`] for this key /// will return an error. /// /// # Arguments /// /// * `key` - A key returned by /// [`start_transfer()`][`FileTransfer::start_transfer`]. /// /// # Specifications /// /// See also [`StopTransfer`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileTransfer.html#org-freedesktop-portal-filetransfer-stoptransfer). #[doc(alias = "StopTransfer")] pub async fn stop_transfer(&self, key: &str) -> Result<(), Error> { self.0.call("StopTransfer", &(key)).await } /// Emitted when the transfer is closed. /// /// # Returns /// /// * The key returned by /// [`start_transfer()`][`FileTransfer::start_transfer`]. /// /// # Specifications /// /// See also [`TransferClosed`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileTransfer.html#org-freedesktop-portal-filetransfer-transferclosed). #[doc(alias = "TransferClosed")] pub async fn transfer_closed(&self) -> Result, Error> { self.0.signal("TransferClosed").await } } impl<'a> std::ops::Deref for FileTransfer<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } ashpd-0.9.1/src/documents/mod.rs000064400000000000000000000440451046102023000146560ustar 00000000000000//! # Examples //! //! ```rust,no_run //! use std::str::FromStr; //! //! use ashpd::{ //! documents::{Documents, Permission}, //! AppID, //! }; //! //! async fn run() -> ashpd::Result<()> { //! let proxy = Documents::new().await?; //! //! println!("{:#?}", proxy.mount_point().await?); //! let app_id = AppID::from_str("org.mozilla.firefox").unwrap(); //! for (doc_id, host_path) in proxy.list(Some(&app_id)).await? { //! if doc_id == "f2ee988d".into() { //! let info = proxy.info(doc_id).await?; //! println!("{:#?}", info); //! } //! } //! //! proxy //! .grant_permissions("f2ee988d", &app_id, &[Permission::GrantPermissions]) //! .await?; //! proxy //! .revoke_permissions("f2ee988d", &app_id, &[Permission::Write]) //! .await?; //! //! proxy.delete("f2ee988d").await?; //! //! Ok(()) //! } //! ``` use std::{collections::HashMap, fmt, os::fd::BorrowedFd, path::Path, str::FromStr}; use enumflags2::{bitflags, BitFlags}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use zbus::zvariant::{Fd, OwnedValue, Type}; pub use crate::app_id::DocumentID; use crate::{proxy::Proxy, AppID, Error, FilePath}; #[bitflags] #[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Copy, Clone, Debug, Type)] #[repr(u32)] /// Document flags pub enum DocumentFlags { /// Reuse the existing document store entry for the file. ReuseExisting, /// Persistent file. Persistent, /// Depends on the application needs. AsNeededByApp, /// Export a directory. ExportDirectory, } /// A [`HashMap`] mapping application IDs to the permissions for that /// application pub type Permissions = HashMap>; #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdPermission"))] #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Eq, Type)] #[zvariant(signature = "s")] #[serde(rename_all = "kebab-case")] /// The possible permissions to grant to a specific application for a specific /// document. pub enum Permission { /// Read access. Read, /// Write access. Write, /// The possibility to grant new permissions to the file. GrantPermissions, /// Delete access. Delete, } impl fmt::Display for Permission { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Read => write!(f, "Read"), Self::Write => write!(f, "Write"), Self::GrantPermissions => write!(f, "Grant Permissions"), Self::Delete => write!(f, "Delete"), } } } impl AsRef for Permission { fn as_ref(&self) -> &str { match self { Self::Read => "Read", Self::Write => "Write", Self::GrantPermissions => "Grant Permissions", Self::Delete => "Delete", } } } impl From for &'static str { fn from(p: Permission) -> Self { match p { Permission::Read => "Read", Permission::Write => "Write", Permission::GrantPermissions => "Grant Permissions", Permission::Delete => "Delete", } } } impl FromStr for Permission { type Err = Error; fn from_str(s: &str) -> Result { match s { "Read" | "read" => Ok(Permission::Read), "Write" | "write" => Ok(Permission::Write), "GrantPermissions" | "grant-permissions" => Ok(Permission::GrantPermissions), "Delete" | "delete" => Ok(Permission::Delete), _ => Err(Error::ParseError("Failed to parse priority, invalid value")), } } } /// The interface lets sandboxed applications make files from the outside world /// available to sandboxed applications in a controlled way. /// /// Exported files will be made accessible to the application via a fuse /// filesystem that gets mounted at `/run/user/$UID/doc/`. The filesystem gets /// mounted both outside and inside the sandbox, but the view inside the sandbox /// is restricted to just those files that the application is allowed to access. /// /// Individual files will appear at `/run/user/$UID/doc/$DOC_ID/filename`, /// where `$DOC_ID` is the ID of the file in the document store. /// It is returned by the [`Documents::add`] and /// [`Documents::add_named`] calls. /// /// The permissions that the application has for a document store entry (see /// [`Documents::grant_permissions`]) are reflected in the POSIX mode bits /// in the fuse filesystem. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.Documents`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.Documents")] pub struct Documents<'a>(Proxy<'a>); impl<'a> Documents<'a> { /// Create a new instance of [`Documents`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_documents("org.freedesktop.portal.Documents").await?; Ok(Self(proxy)) } /// Adds a file to the document store. /// The file is passed in the form of an open file descriptor /// to prove that the caller has access to the file. /// /// # Arguments /// /// * `o_path_fd` - Open file descriptor for the file to add. /// * `reuse_existing` - Whether to reuse an existing document store entry /// for the file. /// * `persistent` - Whether to add the file only for this session or /// permanently. /// /// # Returns /// /// The ID of the file in the document store. /// /// # Specifications /// /// See also [`Add`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-add). #[doc(alias = "Add")] pub async fn add( &self, o_path_fd: &BorrowedFd<'_>, reuse_existing: bool, persistent: bool, ) -> Result { self.0 .call("Add", &(Fd::from(o_path_fd), reuse_existing, persistent)) .await } /// Adds multiple files to the document store. /// The files are passed in the form of an open file descriptor /// to prove that the caller has access to the file. /// /// # Arguments /// /// * `o_path_fds` - Open file descriptors for the files to export. /// * `flags` - A [`DocumentFlags`]. /// * `app_id` - An application ID, or `None`. /// * `permissions` - The permissions to grant. /// /// # Returns /// /// The IDs of the files in the document store along with other extra info. /// /// # Required version /// /// The method requires the 2nd version implementation of the portal and /// would fail with [`Error::RequiresVersion`] otherwise. /// /// # Specifications /// /// See also [`AddFull`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-addfull). #[doc(alias = "AddFull")] pub async fn add_full( &self, o_path_fds: &[&BorrowedFd<'_>], flags: BitFlags, app_id: Option<&AppID>, permissions: &[Permission], ) -> Result<(Vec, HashMap), Error> { let o_path: Vec = o_path_fds.iter().map(Fd::from).collect(); let app_id = app_id.map(|id| id.as_ref()).unwrap_or(""); self.0 .call_versioned("AddFull", &(o_path, flags, app_id, permissions), 2) .await } /// Creates an entry in the document store for writing a new file. /// /// # Arguments /// /// * `o_path_parent_fd` - Open file descriptor for the parent directory. /// * `filename` - The basename for the file. /// * `reuse_existing` - Whether to reuse an existing document store entry /// for the file. /// * `persistent` - Whether to add the file only for this session or /// permanently. /// /// # Returns /// /// The ID of the file in the document store. /// /// # Specifications /// /// See also [`AddNamed`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-addnamed). #[doc(alias = "AddNamed")] pub async fn add_named( &self, o_path_parent_fd: &BorrowedFd<'_>, filename: impl AsRef, reuse_existing: bool, persistent: bool, ) -> Result { let filename = FilePath::new(filename)?; self.0 .call( "AddNamed", &( Fd::from(o_path_parent_fd), filename, reuse_existing, persistent, ), ) .await } /// Adds multiple files to the document store. /// The files are passed in the form of an open file descriptor /// to prove that the caller has access to the file. /// /// # Arguments /// /// * `o_path_fd` - Open file descriptor for the parent directory. /// * `filename` - The basename for the file. /// * `flags` - A [`DocumentFlags`]. /// * `app_id` - An application ID, or `None`. /// * `permissions` - The permissions to grant. /// /// # Returns /// /// The ID of the file in the document store along with other extra info. /// /// # Required version /// /// The method requires the 3nd version implementation of the portal and /// would fail with [`Error::RequiresVersion`] otherwise. /// /// # Specifications /// /// See also [`AddNamedFull`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-addnamedfull). #[doc(alias = "AddNamedFull")] pub async fn add_named_full( &self, o_path_fd: &BorrowedFd<'_>, filename: impl AsRef, flags: BitFlags, app_id: Option<&AppID>, permissions: &[Permission], ) -> Result<(DocumentID, HashMap), Error> { let app_id = app_id.map(|id| id.as_ref()).unwrap_or(""); let filename = FilePath::new(filename)?; self.0 .call_versioned( "AddNamedFull", &(Fd::from(o_path_fd), filename, flags, app_id, permissions), 3, ) .await } /// Removes an entry from the document store. The file itself is not /// deleted. /// /// **Note** This call is available inside the sandbox if the /// application has the [`Permission::Delete`] for the document. /// /// # Arguments /// /// * `doc_id` - The ID of the file in the document store. /// /// # Specifications /// /// See also [`Delete`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-delete). #[doc(alias = "Delete")] pub async fn delete(&self, doc_id: impl Into) -> Result<(), Error> { self.0.call("Delete", &(doc_id.into())).await } /// Returns the path at which the document store fuse filesystem is mounted. /// This will typically be `/run/user/$UID/doc/`. /// /// # Specifications /// /// See also [`GetMountPoint`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-getmountpoint). #[doc(alias = "GetMountPoint")] #[doc(alias = "get_mount_point")] pub async fn mount_point(&self) -> Result { self.0.call("GetMountPoint", &()).await } /// Grants access permissions for a file in the document store to an /// application. /// /// **Note** This call is available inside the sandbox if the /// application has the [`Permission::GrantPermissions`] for the document. /// /// # Arguments /// /// * `doc_id` - The ID of the file in the document store. /// * `app_id` - The ID of the application to which permissions are granted. /// * `permissions` - The permissions to grant. /// /// # Specifications /// /// See also [`GrantPermissions`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-grantpermissions). #[doc(alias = "GrantPermissions")] pub async fn grant_permissions( &self, doc_id: impl Into, app_id: &AppID, permissions: &[Permission], ) -> Result<(), Error> { self.0 .call("GrantPermissions", &(doc_id.into(), app_id, permissions)) .await } /// Gets the filesystem path and application permissions for a document /// store entry. /// /// **Note** This call is not available inside the sandbox. /// /// # Arguments /// /// * `doc_id` - The ID of the file in the document store. /// /// # Returns /// /// The path of the file in the host filesystem along with the /// [`Permissions`]. /// /// # Specifications /// /// See also [`Info`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-info). #[doc(alias = "Info")] pub async fn info( &self, doc_id: impl Into, ) -> Result<(FilePath, Permissions), Error> { self.0.call("Info", &(doc_id.into())).await } /// Lists documents in the document store for an application (or for all /// applications). /// /// **Note** This call is not available inside the sandbox. /// /// # Arguments /// /// * `app-id` - The application ID, or `None` to list all documents. /// /// # Returns /// /// [`HashMap`] mapping document IDs to their filesystem path on the host /// system. /// /// # Specifications /// /// See also [`List`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-list). #[doc(alias = "List")] pub async fn list( &self, app_id: Option<&AppID>, ) -> Result, Error> { let app_id = app_id.map(|id| id.as_ref()).unwrap_or(""); let response: HashMap = self.0.call("List", &(app_id)).await?; let mut new_response: HashMap = HashMap::new(); for (key, file_name) in response { new_response.insert(DocumentID::from(key), file_name); } Ok(new_response) } /// Looks up the document ID for a file. /// /// **Note** This call is not available inside the sandbox. /// /// # Arguments /// /// * `filename` - A path in the host filesystem. /// /// # Returns /// /// The ID of the file in the document store, or [`None`] if the file is not /// in the document store. /// /// # Specifications /// /// See also [`Lookup`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-lookup). #[doc(alias = "Lookup")] pub async fn lookup(&self, filename: impl AsRef) -> Result, Error> { let filename = FilePath::new(filename)?; let doc_id: String = self.0.call("Lookup", &(filename)).await?; if doc_id.is_empty() { Ok(None) } else { Ok(Some(doc_id.into())) } } /// Revokes access permissions for a file in the document store from an /// application. /// /// **Note** This call is available inside the sandbox if the /// application has the [`Permission::GrantPermissions`] for the document. /// /// # Arguments /// /// * `doc_id` - The ID of the file in the document store. /// * `app_id` - The ID of the application from which the permissions are /// revoked. /// * `permissions` - The permissions to revoke. /// /// # Specifications /// /// See also [`RevokePermissions`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-revokepermissions). #[doc(alias = "RevokePermissions")] pub async fn revoke_permissions( &self, doc_id: impl Into, app_id: &AppID, permissions: &[Permission], ) -> Result<(), Error> { self.0 .call("RevokePermissions", &(doc_id.into(), app_id, permissions)) .await } /// Retrieves the host filesystem paths from their document IDs. /// /// # Arguments /// /// * `doc_ids` - A list of file IDs in the document store. /// /// # Returns /// /// A dictionary mapping document IDs to the paths in the host filesystem /// /// # Specifications /// /// See also [`GetHostPaths`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Documents.html#org-freedesktop-portal-documents-gethostpaths). #[doc(alias = "GetHostPaths")] pub async fn host_paths( &self, doc_ids: &[DocumentID], ) -> Result, Error> { self.0.call_versioned("GetHostPaths", &(doc_ids,), 5).await } } impl<'a> std::ops::Deref for Documents<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } /// Interact with `org.freedesktop.portal.FileTransfer` interface. mod file_transfer; pub use file_transfer::FileTransfer; #[cfg(test)] mod tests { use std::collections::HashMap; use zbus::zvariant::Type; use crate::{app_id::DocumentID, documents::Permission, FilePath}; #[test] fn serialize_deserialize() { let permission = Permission::GrantPermissions; let string = serde_json::to_string(&permission).unwrap(); assert_eq!(string, "\"grant-permissions\""); let decoded = serde_json::from_str(&string).unwrap(); assert_eq!(permission, decoded); assert_eq!(HashMap::::signature(), "a{say}"); } } ashpd-0.9.1/src/error.rs000064400000000000000000000126711046102023000132270ustar 00000000000000use zbus::DBusError; use crate::desktop::{dynamic_launcher::UnexpectedIconError, request::ResponseError}; /// An error type that describes the various DBus errors. /// /// See . #[allow(missing_docs)] #[derive(DBusError, Debug)] #[zbus(prefix = "org.freedesktop.portal.Error")] pub enum PortalError { #[zbus(error)] /// ZBus specific error. ZBus(zbus::Error), /// Request failed. Failed(String), /// Invalid arguments passed. InvalidArgument(String), /// Not found. NotFound(String), /// Exists already. Exist(String), /// Method not allowed to be called. NotAllowed(String), /// Request cancelled. Cancelled(String), /// Window destroyed. WindowDestroyed(String), } #[derive(Debug)] #[non_exhaustive] /// The error type for ashpd. pub enum Error { /// The portal request didn't succeed. Response(ResponseError), /// Something Failed on the portal request. Portal(PortalError), /// A zbus::fdo specific error. Zbus(zbus::Error), /// A signal returned no response. NoResponse, /// Failed to parse a string into an enum variant ParseError(&'static str), /// Input/Output IO(std::io::Error), /// A pipewire error #[cfg(feature = "pipewire")] Pipewire(pipewire::Error), /// Invalid AppId /// /// See InvalidAppID, /// An error indicating that an interior nul byte was found NulTerminated(usize), /// Requires a newer interface version. /// /// The inner fields are the required version and the version advertised by /// the interface. RequiresVersion(u32, u32), /// Returned when the portal wasn't found. Either the user has no portals /// frontend installed or the frontend doesn't support the used portal. PortalNotFound(zbus::names::OwnedInterfaceName), /// An error indicating that a Icon::Bytes was expected but wrong type was /// passed UnexpectedIcon, } impl std::error::Error for Error {} impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Response(e) => f.write_str(&format!("Portal request didn't succeed: {e}")), Self::Zbus(e) => f.write_str(&format!("ZBus Error: {e}")), Self::Portal(e) => f.write_str(&format!("Portal request failed: {e}")), Self::NoResponse => f.write_str("Portal error: no response"), Self::IO(e) => f.write_str(&format!("IO: {e}")), #[cfg(feature = "pipewire")] Self::Pipewire(e) => f.write_str(&format!("Pipewire: {e}")), Self::ParseError(e) => f.write_str(e), Self::InvalidAppID => f.write_str("Invalid app id"), Self::NulTerminated(u) => write!(f, "Nul byte found in provided data at position {u}"), Self::RequiresVersion(required, current) => write!( f, "This interface requires version {required}, but {current} is available" ), Self::PortalNotFound(portal) => { write!(f, "A portal frontend implementing `{portal}` was not found") } Self::UnexpectedIcon => write!( f, "Expected icon of type Icon::Bytes but a different type was used." ), } } } impl From for Error { fn from(e: ResponseError) -> Self { Self::Response(e) } } impl From for Error { fn from(e: PortalError) -> Self { Self::Portal(e) } } #[cfg(feature = "pipewire")] impl From for Error { fn from(e: pipewire::Error) -> Self { Self::Pipewire(e) } } impl From for Error { fn from(e: zbus::fdo::Error) -> Self { Self::Zbus(zbus::Error::FDO(Box::new(e))) } } impl From for Error { fn from(e: zbus::Error) -> Self { match &e { zbus::Error::MethodError(_name, Some(details), _reply) => { // This is really a gross hack, needs to find a better way but it works. let iface = details .trim_start_matches("No such interface") .trim_end_matches("on object at path /org/freedesktop/portal/desktop") .trim() .trim_matches('`') .trim_matches('“') .trim_matches('”'); match zbus::names::OwnedInterfaceName::try_from(iface) { Ok(iface) => Self::PortalNotFound(iface), Err(_err) => { #[cfg(feature = "tracing")] { tracing::warn!("Hack! The parsing of the iface name has failed: iface {iface}, error details {details}") }; Self::Zbus(e) } } } _ => Self::Zbus(e), } } } impl From for Error { fn from(e: zbus::zvariant::Error) -> Self { Self::Zbus(zbus::Error::Variant(e)) } } impl From for Error { fn from(e: std::io::Error) -> Self { Self::IO(e) } } impl From for Error { fn from(_: UnexpectedIconError) -> Self { Self::UnexpectedIcon } } ashpd-0.9.1/src/file_path.rs000064400000000000000000000047451046102023000140340ustar 00000000000000use std::{ ffi::{CString, OsStr}, os::unix::ffi::OsStrExt, path::Path, }; use serde::{Deserialize, Serialize}; use zbus::zvariant::Type; /// A file name represented as a nul-terminated byte array. #[derive(Type, Debug, Default, PartialEq)] #[zvariant(signature = "ay")] pub struct FilePath(CString); impl AsRef for FilePath { fn as_ref(&self) -> &Path { OsStr::from_bytes(self.0.as_bytes()).as_ref() } } impl FilePath { pub(crate) fn new>(s: T) -> Result { let c_string = CString::new(s.as_ref().as_os_str().as_bytes()) .map_err(|err| crate::Error::NulTerminated(err.nul_position()))?; Ok(Self(c_string)) } } impl Serialize for FilePath { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_bytes(self.0.as_bytes_with_nul()) } } impl<'de> Deserialize<'de> for FilePath { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let bytes = >::deserialize(deserializer)?; let c_string = CString::from_vec_with_nul(bytes) .map_err(|_| serde::de::Error::custom("Bytes are not nul-terminated"))?; Ok(Self(c_string)) } } #[cfg(test)] mod tests { use zbus::zvariant::{ serialized::{Context, Data}, to_bytes, Endian, }; use super::*; #[test] fn test_serialize_is_nul_terminated() { let bytes = vec![97, 98, 99, 0]; // b"abc\0" assert_eq!(b"abc\0", bytes.as_slice()); let c_string = CString::from_vec_with_nul(bytes.clone()).unwrap(); assert_eq!(c_string.as_bytes_with_nul(), &bytes); let ctxt = Context::new_dbus(Endian::Little, 0); let file_path = FilePath(c_string); let file_path_2 = FilePath::new("abc").unwrap(); let encoded_filename = to_bytes(ctxt, &file_path).unwrap().to_vec(); let encoded_filename_2 = to_bytes(ctxt, &file_path_2).unwrap().to_vec(); let encoded_bytes = to_bytes(ctxt, &bytes).unwrap().to_vec(); // It does not matter whether we use new("abc") or deserialize from b"abc\0". assert_eq!(encoded_filename, encoded_bytes); assert_eq!(encoded_filename_2, encoded_bytes); let decoded: FilePath = Data::new(encoded_bytes, ctxt).deserialize().unwrap().0; assert_eq!(decoded, file_path); assert_eq!(decoded, file_path_2); } } ashpd-0.9.1/src/flatpak/development.rs000064400000000000000000000110461046102023000160350ustar 00000000000000//! The Development interface lets any client, possibly in a sandbox if it has //! access to the session helper, spawn a process on the host, outside any //! sandbox. use std::{collections::HashMap, os::fd::BorrowedFd, path::Path}; use enumflags2::{bitflags, BitFlags}; use futures_util::Stream; use serde_repr::{Deserialize_repr, Serialize_repr}; use zbus::zvariant::{Fd, Type}; use crate::{proxy::Proxy, Error, FilePath}; #[bitflags] #[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Copy, Clone, Debug, Type)] #[repr(u32)] /// Flags affecting the running of commands on the host pub enum HostCommandFlags { #[doc(alias = "FLATPAK_HOST_COMMAND_FLAGS_CLEAR_ENV")] /// Clear the environment. ClearEnv, #[doc(alias = "FLATPAK_HOST_COMMAND_FLAGS_WATCH_BUS")] /// Kill the sandbox when the caller disappears from the session bus. WatchBus, } /// The Development interface lets any client, possibly in a sandbox if it has /// access to the session helper, spawn a process on the host, outside any /// sandbox. /// /// Wrapper of the DBus interface: [`org.freedesktop.Flatpak.Development`](https://docs.flatpak.org/en/latest/libflatpak-api-reference.html#gdbus-org.freedesktop.Flatpak.Development) #[derive(Debug)] #[doc(alias = "org.freedesktop.Flatpak.Development")] pub struct Development<'a>(Proxy<'a>); impl<'a> Development<'a> { /// Create a new instance of [`Development`] pub async fn new() -> Result, Error> { let proxy = Proxy::new_flatpak_development("org.freedesktop.Flatpak.Development").await?; Ok(Self(proxy)) } /// Emitted when a process started by /// [`host_command()`][`Development::host_command`] exits. /// /// # Specifications /// /// See also [`HostCommandExited`](https://docs.flatpak.org/en/latest/libflatpak-api-reference.html#gdbus-signal-org-freedesktop-Flatpak-Development.HostCommandExited). #[doc(alias = "HostCommandExited")] pub async fn receive_spawn_exited(&self) -> Result, Error> { self.0.signal("HostCommandExited").await } /// This method lets trusted applications (insider or outside a sandbox) run /// arbitrary commands in the user's session, outside any sandbox. /// /// # Arguments /// /// * `cwd_path` - The working directory for the new process. /// * `argv` - The argv for the new process, starting with the executable to /// launch. /// * `fds` - Array of file descriptors to pass to the new process. /// * `envs` - Array of variable/value pairs for the environment of the new /// process. /// * `flags` /// /// # Returns /// /// The PID of the new process. /// /// # Specifications /// /// See also [`HostCommand`](https://docs.flatpak.org/en/latest/libflatpak-api-reference.html#gdbus-method-org-freedesktop-Flatpak-Development.HostCommand). pub async fn host_command( &self, cwd_path: impl AsRef, argv: &[impl AsRef], fds: HashMap>, envs: HashMap<&str, &str>, flags: BitFlags, ) -> Result { let cwd_path = FilePath::new(cwd_path)?; let argv = argv .iter() .map(FilePath::new) .collect::, _>>()?; let fds: HashMap = fds.iter().map(|(k, val)| (*k, Fd::from(val))).collect(); self.0 .call("HostCommand", &(cwd_path, argv, fds, envs, flags)) .await } /// This methods let you send a Unix signal to a process that was started /// [`host_command()`][`Development::host_command`]. /// /// # Arguments /// /// * `pid` - The PID of the process to send the signal to. /// * `signal` - The signal to send. /// * `to_process_group` - Whether to send the signal to the process group. /// /// # Specifications /// /// See also [`HostCommandSignal`](https://docs.flatpak.org/en/latest/libflatpak-api-reference.html#gdbus-method-org-freedesktop-Flatpak-Development.HostCommandSignal). #[doc(alias = "SpawnSignal")] #[doc(alias = "xdp_portal_spawn_signal")] pub async fn host_command_signal( &self, pid: u32, signal: u32, to_process_group: bool, ) -> Result<(), Error> { self.0 .call("HostCommandSignal", &(pid, signal, to_process_group)) .await } } impl<'a> std::ops::Deref for Development<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } ashpd-0.9.1/src/flatpak/mod.rs000064400000000000000000000332601046102023000142740ustar 00000000000000//! # Examples //! //! Spawn a process inside of the sandbox, only works in a Flatpak. //! //! ```rust,no_run //! use std::collections::HashMap; //! //! use ashpd::flatpak::{Flatpak, SpawnFlags, SpawnOptions}; //! //! async fn run() -> ashpd::Result<()> { //! let proxy = Flatpak::new().await?; //! //! proxy //! .spawn( //! "/", //! &["contrast"], //! HashMap::new(), //! HashMap::new(), //! SpawnFlags::ClearEnv | SpawnFlags::NoNetwork, //! SpawnOptions::default(), //! ) //! .await?; //! //! Ok(()) //! } //! ``` use std::{ collections::HashMap, fmt::Debug, os::fd::{BorrowedFd, OwnedFd}, path::Path, }; use enumflags2::{bitflags, BitFlags}; use futures_util::Stream; use serde::Serialize; use serde_repr::{Deserialize_repr, Serialize_repr}; use zbus::zvariant::{self, Fd, OwnedObjectPath, SerializeDict, Type}; use crate::{proxy::Proxy, Error, FilePath}; #[bitflags] #[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Copy, Clone, Debug, Type)] #[repr(u32)] /// A bitmask representing the "permissions" of a newly created sandbox. pub enum SandboxFlags { /// Share the display access (X11, Wayland) with the caller. DisplayAccess, /// Share the sound access (PulseAudio) with the caller. SoundAccess, /// Share the gpu access with the caller. GpuAccess, /// Allow sandbox access to (filtered) session bus. SessionBusAccess, /// Allow sandbox access to accessibility bus. AccessibilityBusAccess, } #[bitflags] #[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Copy, Clone, Debug, Type)] #[repr(u32)] #[doc(alias = "XdpSpawnFlags")] /// Flags affecting the created sandbox. pub enum SpawnFlags { #[doc(alias = "XDP_SPAWN_FLAG_CLEARENV")] /// Clear the environment. ClearEnv, #[doc(alias = "XDP_SPAWN_FLAG_LATEST")] /// Spawn the latest version of the app. LatestVersion, #[doc(alias = "XDP_SPAWN_FLAG_SANDBOX")] /// Spawn in a sandbox (equivalent of the sandbox option of `flatpak run`). Sandbox, #[doc(alias = "XDP_SPAWN_FLAG_NO_NETWORK")] /// Spawn without network (equivalent of the `unshare=network` option of /// `flatpak run`). NoNetwork, #[doc(alias = "XDP_SPAWN_FLAG_WATCH")] /// Kill the sandbox when the caller disappears from the session bus. WatchBus, /// Expose the sandbox pids in the callers sandbox, only supported if using /// user namespaces for containers (not setuid), see the support property. ExposePids, /// Emit a SpawnStarted signal once the sandboxed process has been fully /// started. NotifyStart, /// Expose the sandbox process IDs in the caller's sandbox and the caller's /// process IDs in the new sandbox. SharePids, /// Don't provide app files at `/app` in the new sandbox. EmptyApp, } #[bitflags] #[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Copy, Clone, Debug, Type)] #[repr(u32)] /// Flags marking what optional features are available. pub enum SupportsFlags { /// Supports the expose sandbox pids flag of Spawn. ExposePids, } #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`Flatpak::spawn`] request. #[zvariant(signature = "dict")] pub struct SpawnOptions { /// A list of filenames for files inside the sandbox that will be exposed to /// the new sandbox, for reading and writing. #[zvariant(rename = "sandbox-expose")] sandbox_expose: Option>, /// A list of filenames for files inside the sandbox that will be exposed to /// the new sandbox, read-only. #[zvariant(rename = "sandbox-expose-ro")] sandbox_expose_ro: Option>, /// A list of file descriptor for files inside the sandbox that will be /// exposed to the new sandbox, for reading and writing. #[zvariant(rename = "sandbox-expose-fd")] sandbox_expose_fd: Option>, /// A list of file descriptor for files inside the sandbox that will be /// exposed to the new sandbox, read-only. #[zvariant(rename = "sandbox-expose-fd-ro")] sandbox_expose_fd_ro: Option>, /// Flags affecting the created sandbox. #[zvariant(rename = "sandbox-flags")] sandbox_flags: Option>, /// A list of environment variables to remove. #[zvariant(rename = "unset-env")] unset_env: Option>, /// A file descriptor of the directory that will be used as `/usr` in the /// new sandbox. #[zvariant(rename = "usr-fd")] usr_fd: Option, /// A file descriptor of the directory that will be used as `/app` in the /// new sandbox. #[zvariant(rename = "app-fd")] app_fd: Option, } impl SpawnOptions { /// Sets the list of filenames for files to expose the new sandbox. /// **Note** absolute paths or subdirectories are not allowed. #[must_use] pub fn sandbox_expose, I: AsRef + Type + Serialize>( mut self, sandbox_expose: impl Into>, ) -> Self { self.sandbox_expose = sandbox_expose .into() .map(|a| a.into_iter().map(|s| s.as_ref().to_owned()).collect()); self } /// Sets the list of filenames for files to expose the new sandbox, /// read-only. /// **Note** absolute paths or subdirectories are not allowed. #[must_use] pub fn sandbox_expose_ro, I: AsRef + Type + Serialize>( mut self, sandbox_expose_ro: impl Into>, ) -> Self { self.sandbox_expose_ro = sandbox_expose_ro .into() .map(|a| a.into_iter().map(|s| s.as_ref().to_owned()).collect()); self } /// Sets the list of file descriptors of files to expose the new sandbox. #[must_use] pub fn sandbox_expose_fd>( mut self, sandbox_expose_fd: impl Into>, ) -> Self { self.sandbox_expose_fd = sandbox_expose_fd .into() .map(|a| a.into_iter().map(zvariant::OwnedFd::from).collect()); self } /// Sets the list of file descriptors of files to expose the new sandbox, /// read-only. #[must_use] pub fn sandbox_expose_fd_ro>( mut self, sandbox_expose_fd_ro: impl Into>, ) -> Self { self.sandbox_expose_fd_ro = sandbox_expose_fd_ro .into() .map(|a| a.into_iter().map(zvariant::OwnedFd::from).collect()); self } /// Sets the created sandbox flags. #[must_use] pub fn sandbox_flags( mut self, sandbox_flags: impl Into>>, ) -> Self { self.sandbox_flags = sandbox_flags.into(); self } /// Env variables to unset. #[must_use] pub fn unset_env, I: AsRef + Type + Serialize>( mut self, env: impl Into>, ) -> Self { self.unset_env = env .into() .map(|a| a.into_iter().map(|s| s.as_ref().to_owned()).collect()); self } /// Set a file descriptor of the directory that will be used as `/usr` in /// the new sandbox. #[must_use] pub fn usr_fd(mut self, fd: impl Into>) -> Self { self.usr_fd = fd.into().map(|f| f.into()); self } /// Set a file descriptor of the directory that will be used as `/app` in /// the new sandbox. #[must_use] pub fn app_fd(mut self, fd: impl Into>) -> Self { self.app_fd = fd.into().map(|f| f.into()); self } } #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`Flatpak::create_update_monitor`] request. /// /// Currently there are no possible options yet. #[zvariant(signature = "dict")] struct CreateMonitorOptions {} /// The interface exposes some interactions with Flatpak on the host to the /// sandbox. For example, it allows you to restart the applications or start a /// more sandboxed instance. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.Flatpak`](https://docs.flatpak.org/en/latest/portal-api-reference.html#gdbus-org.freedesktop.portal.Flatpak). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.Flatpak")] pub struct Flatpak<'a>(Proxy<'a>); impl<'a> Flatpak<'a> { /// Create a new instance of [`Flatpak`]. pub async fn new() -> Result, Error> { let proxy = Proxy::new_flatpak("org.freedesktop.portal.Flatpak").await?; Ok(Self(proxy)) } /// Creates an update monitor object that will emit signals /// when an update for the caller becomes available, and can be used to /// install it. /// /// # Required version /// /// The method requires the 2nd version implementation of the portal and /// would fail with [`Error::RequiresVersion`] otherwise. /// /// # Specifications /// /// See also [`CreateUpdateMonitor`](https://docs.flatpak.org/en/latest/portal-api-reference.html#gdbus-method-org-freedesktop-portal-Flatpak.CreateUpdateMonitor). #[doc(alias = "CreateUpdateMonitor")] #[doc(alias = "xdp_portal_update_monitor_start")] pub async fn create_update_monitor(&self) -> Result, Error> { let options = CreateMonitorOptions::default(); let path = self .0 .call_versioned::("CreateUpdateMonitor", &(options), 2) .await?; UpdateMonitor::new(path.into_inner()).await } /// Emitted when a process starts by [`spawn()`][`Flatpak::spawn`]. /// /// # Specifications /// /// See also [`SpawnStarted`](https://docs.flatpak.org/en/latest/portal-api-reference.html#gdbus-signal-org-freedesktop-portal-Flatpak.SpawnStarted). #[doc(alias = "SpawnStarted")] pub async fn receive_spawn_started(&self) -> Result, Error> { self.0.signal("SpawnStarted").await } /// Emitted when a process started by [`spawn()`][`Flatpak::spawn`] /// exits. /// /// # Specifications /// /// See also [`SpawnExited`](https://docs.flatpak.org/en/latest/portal-api-reference.html#gdbus-signal-org-freedesktop-portal-Flatpak.SpawnExited). #[doc(alias = "SpawnExited")] #[doc(alias = "XdpPortal::spawn-exited")] pub async fn receive_spawn_exited(&self) -> Result, Error> { self.0.signal("SpawnExited").await } /// This methods let you start a new instance of your application, /// optionally enabling a tighter sandbox. /// /// # Arguments /// /// * `cwd_path` - The working directory for the new process. /// * `argv` - The argv for the new process, starting with the executable to /// launch. /// * `fds` - Array of file descriptors to pass to the new process. /// * `envs` - Array of variable/value pairs for the environment of the new /// process. /// * `flags` /// * `options` - A [`SpawnOptions`]. /// /// # Returns /// /// The PID of the new process. /// /// # Specifications /// /// See also [`Spawn`](https://docs.flatpak.org/en/latest/portal-api-reference.html#gdbus-method-org-freedesktop-portal-Flatpak.Spawn). #[doc(alias = "Spawn")] #[doc(alias = "xdp_portal_spawn")] pub async fn spawn( &self, cwd_path: impl AsRef, argv: &[impl AsRef], fds: HashMap>, envs: HashMap<&str, &str>, flags: BitFlags, options: SpawnOptions, ) -> Result { let cwd_path = FilePath::new(cwd_path)?; let argv = argv .iter() .map(FilePath::new) .collect::, _>>()?; let fds: HashMap = fds.iter().map(|(k, val)| (*k, Fd::from(val))).collect(); self.0 .call("Spawn", &(cwd_path, argv, fds, envs, flags, options)) .await } /// This methods let you send a Unix signal to a process that was started /// [`spawn()`][`Flatpak::spawn`]. /// /// # Arguments /// /// * `pid` - The PID of the process to send the signal to. /// * `signal` - The signal to send. /// * `to_process_group` - Whether to send the signal to the process group. /// /// # Specifications /// /// See also [`SpawnSignal`](https://docs.flatpak.org/en/latest/portal-api-reference.html#gdbus-method-org-freedesktop-portal-Flatpak.SpawnSignal). #[doc(alias = "SpawnSignal")] #[doc(alias = "xdp_portal_spawn_signal")] pub async fn spawn_signal( &self, pid: u32, signal: u32, to_process_group: bool, ) -> Result<(), Error> { self.0 .call("SpawnSignal", &(pid, signal, to_process_group)) .await } /// Flags marking what optional features are available. /// /// # Specifications /// /// See also [`supports`](https://docs.flatpak.org/en/latest/portal-api-reference.html#gdbus-property-org-freedesktop-portal-Flatpak.supports). pub async fn supports(&self) -> Result, Error> { self.0 .property_versioned::>("supports", 3) .await } } impl<'a> std::ops::Deref for Flatpak<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } /// Monitor if there's an update it and install it. mod update_monitor; pub use update_monitor::{UpdateInfo, UpdateMonitor, UpdateProgress, UpdateStatus}; /// Provide for a way to execute processes outside of the sandbox mod development; pub use development::{Development, HostCommandFlags}; ashpd-0.9.1/src/flatpak/update_monitor.rs000064400000000000000000000144631046102023000165520ustar 00000000000000//! # Examples //! //! How to monitor if there's a new update and install it. //! Only available for Flatpak applications. //! //! ```rust,no_run //! use ashpd::{flatpak::Flatpak, WindowIdentifier}; //! use futures_util::StreamExt; //! //! async fn run() -> ashpd::Result<()> { //! let proxy = Flatpak::new().await?; //! //! let monitor = proxy.create_update_monitor().await?; //! let info = monitor.receive_update_available().await?; //! //! monitor.update(&WindowIdentifier::default()).await?; //! let progress = monitor //! .receive_progress() //! .await? //! .next() //! .await //! .expect("Stream exhausted"); //! println!("{:#?}", progress); //! //! Ok(()) //! } //! ``` use futures_util::Stream; use serde_repr::{Deserialize_repr, Serialize_repr}; use zbus::zvariant::{DeserializeDict, ObjectPath, SerializeDict, Type}; use crate::{proxy::Proxy, Error, WindowIdentifier}; #[derive(SerializeDict, Type, Debug, Default)] /// Specified options for a [`UpdateMonitor::update`] request. /// /// Currently there are no possible options yet. #[zvariant(signature = "dict")] struct UpdateOptions {} #[derive(DeserializeDict, Type, Debug)] /// A response containing the update information when an update is available. #[zvariant(signature = "dict")] pub struct UpdateInfo { #[zvariant(rename = "running-commit")] running_commit: String, #[zvariant(rename = "local-commit")] local_commit: String, #[zvariant(rename = "remote-commit")] remote_commit: String, } impl UpdateInfo { /// The currently running OSTree commit. pub fn running_commit(&self) -> &str { &self.running_commit } /// The locally installed OSTree commit. pub fn local_commit(&self) -> &str { &self.local_commit } /// The available commit to install. pub fn remote_commit(&self) -> &str { &self.remote_commit } } #[cfg_attr(feature = "glib", derive(glib::Enum))] #[cfg_attr(feature = "glib", enum_type(name = "AshpdUpdateStatus"))] #[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Copy, Clone, Debug, Type)] #[repr(u32)] /// The update status. pub enum UpdateStatus { #[doc(alias = "XDP_UPDATE_STATUS_RUNNING")] /// Running. Running = 0, #[doc(alias = "XDP_UPDATE_STATUS_EMPTY")] /// No update to install. Empty = 1, #[doc(alias = "XDP_UPDATE_STATUS_DONE")] /// Done. Done = 2, #[doc(alias = "XDP_UPDATE_STATUS_FAILED")] /// Failed. Failed = 3, } #[derive(DeserializeDict, Type, Debug)] /// A response of the update progress signal. #[zvariant(signature = "dict")] pub struct UpdateProgress { /// The number of operations that the update consists of. pub n_ops: Option, /// The position of the currently active operation. pub op: Option, /// The progress of the currently active operation, as a number between 0 /// and 100. pub progress: Option, /// The overall status of the update. pub status: Option, /// The error name, sent when status is `UpdateStatus::Failed`. pub error: Option, /// The error message, sent when status is `UpdateStatus::Failed`. pub error_message: Option, } /// The interface exposes some interactions with Flatpak on the host to the /// sandbox. For example, it allows you to restart the applications or start a /// more sandboxed instance. /// /// Wrapper of the DBus interface: [`org.freedesktop.portal.Flatpak.UpdateMonitor`](https://docs.flatpak.org/en/latest/portal-api-reference.html#gdbus-org.freedesktop.portal.Flatpak.UpdateMonitor). #[derive(Debug)] #[doc(alias = "org.freedesktop.portal.Flatpak.UpdateMonitor")] pub struct UpdateMonitor<'a>(Proxy<'a>); impl<'a> UpdateMonitor<'a> { /// Create a new instance of [`UpdateMonitor`]. /// /// **Note** A [`UpdateMonitor`] is not supposed to be created /// manually. pub(crate) async fn new(path: ObjectPath<'a>) -> Result, Error> { let proxy = Proxy::new_flatpak_with_path("org.freedesktop.portal.Flatpak.UpdateMonitor", path) .await?; Ok(Self(proxy)) } /// A signal received when there's progress during the application update. /// /// # Specifications /// /// See also [`Progress`](https://docs.flatpak.org/en/latest/portal-api-reference.html#gdbus-signal-org-freedesktop-portal-Flatpak-UpdateMonitor.Progress). #[doc(alias = "Progress")] #[doc(alias = "XdpPortal::update-progress")] pub async fn receive_progress(&self) -> Result, Error> { self.0.signal("Progress").await } /// A signal received when there's an application update. /// /// # Specifications /// /// See also [`UpdateAvailable`](https://docs.flatpak.org/en/latest/portal-api-reference.html#gdbus-signal-org-freedesktop-portal-Flatpak-UpdateMonitor.UpdateAvailable). #[doc(alias = "UpdateAvailable")] #[doc(alias = "XdpPortal::update-available")] pub async fn receive_update_available(&self) -> Result, Error> { self.0.signal("UpdateAvailable").await } /// Asks to install an update of the calling app. /// /// **Note** updates are only allowed if the new version has the same /// permissions (or less) than the currently installed version. /// /// # Specifications /// /// See also [`Update`](https://docs.flatpak.org/en/latest/portal-api-reference.html#gdbus-method-org-freedesktop-portal-Flatpak-UpdateMonitor.Update). #[doc(alias = "Update")] #[doc(alias = "xdp_portal_update_install")] pub async fn update(&self, identifier: &WindowIdentifier) -> Result<(), Error> { let options = UpdateOptions::default(); self.0.call("Update", &(&identifier, options)).await } /// Ends the update monitoring and cancels any ongoing installation. /// /// # Specifications /// /// See also [`Close`](https://docs.flatpak.org/en/latest/portal-api-reference.html#gdbus-method-org-freedesktop-portal-Flatpak-UpdateMonitor.Close). #[doc(alias = "Close")] pub async fn close(&self) -> Result<(), Error> { self.0.call("Close", &()).await } } impl<'a> std::ops::Deref for UpdateMonitor<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.0 } } ashpd-0.9.1/src/helpers.rs000064400000000000000000000050721046102023000135350ustar 00000000000000#[cfg(feature = "async-std")] use async_fs::File; #[cfg(feature = "async-std")] use futures_util::AsyncReadExt; #[cfg(feature = "tokio")] use tokio::{fs::File, io::AsyncReadExt}; pub(crate) async fn is_flatpak() -> bool { #[cfg(feature = "async-std")] { async_fs::metadata("/.flatpak-info").await.is_ok() } #[cfg(not(feature = "async-std"))] { std::path::PathBuf::from("/.flatpak-info").exists() } } pub(crate) async fn is_snap() -> bool { let pid = std::process::id(); let path = format!("/proc/{pid}/cgroup"); let mut file = match File::open(path).await { Ok(file) => file, Err(_) => return false, }; let mut buffer = String::new(); match file.read_to_string(&mut buffer).await { Ok(_) => cgroup_v2_is_snap(&buffer), Err(_) => false, } } fn cgroup_v2_is_snap(cgroups: &str) -> bool { cgroups .lines() .map(|line| { let (n, rest) = line.split_once(':')?; // Check that n is a number. n.parse::().ok()?; let unit = match rest.split_once(':') { Some(("", unit)) => Some(unit), Some(("freezer", unit)) => Some(unit), Some(("name=systemd", unit)) => Some(unit), _ => None, }?; let scope = std::path::Path::new(unit).file_name()?.to_str()?; Some(scope.starts_with("snap.")) }) .any(|x| x.unwrap_or(false)) } #[cfg(test)] mod tests { use super::*; #[test] fn test_cgroup_v2_is_snap() { let data = "0::/user.slice/user-1000.slice/user@1000.service/apps.slice/snap.something.scope\n"; assert!(cgroup_v2_is_snap(data)); let data = "0::/user.slice/user-1000.slice/user@1000.service/apps.slice\n"; assert!(!cgroup_v2_is_snap(data)); let data = "12:pids:/user.slice/user-1000.slice/user@1000.service 11:perf_event:/ 10:net_cls,net_prio:/ 9:cpuset:/ 8:memory:/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-228ae109-a869-4533-8988-65ea4c10b492.scope 7:rdma:/ 6:devices:/user.slice 5:blkio:/user.slice 4:hugetlb:/ 3:freezer:/snap.portal-test 2:cpu,cpuacct:/user.slice 1:name=systemd:/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-228ae109-a869-4533-8988-65ea4c10b492.scope 0::/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-228ae109-a869-4533-8988-65ea4c10b492.scope\n"; assert!(cgroup_v2_is_snap(data)); } } ashpd-0.9.1/src/lib.rs000064400000000000000000000043671046102023000126470ustar 00000000000000#![cfg_attr(docsrs, feature(doc_cfg))] #![deny(rustdoc::broken_intra_doc_links)] #![deny(missing_docs)] #![doc( html_logo_url = "https://raw.githubusercontent.com/bilelmoussaoui/ashpd/master/ashpd-demo/data/icons/com.belmoussaoui.ashpd.demo.svg", html_favicon_url = "https://raw.githubusercontent.com/bilelmoussaoui/ashpd/master/ashpd-demo/data/icons/com.belmoussaoui.ashpd.demo-symbolic.svg" )] #![doc = include_str!("../README.md")] #[cfg(all(all(feature = "tokio", feature = "async-std"), not(doc)))] compile_error!("You can't enable both async-std & tokio features at once"); /// Alias for a [`Result`] with the error type `ashpd::Error`. pub type Result = std::result::Result; static IS_SANDBOXED: OnceLock = OnceLock::new(); mod activation_token; /// Interact with the user's desktop such as taking a screenshot, setting a /// background or querying the user's location. pub mod desktop; /// Interact with the documents store or transfer files across apps. pub mod documents; mod error; mod window_identifier; pub use self::{activation_token::ActivationToken, window_identifier::WindowIdentifier}; mod app_id; pub use self::app_id::AppID; mod file_path; pub use self::file_path::FilePath; mod proxy; /// Spawn commands outside the sandbox or monitor if the running application has /// received an update & install it. pub mod flatpak; mod helpers; use std::sync::OnceLock; pub use enumflags2; pub use url; pub use zbus::{self, zvariant}; /// Check whether the application is running inside a sandbox. /// /// The function checks whether the file `/.flatpak-info` exists, or if the app /// is running as a snap, or if the environment variable `GTK_USE_PORTAL` is set /// to `1`. As the return value of this function will not change during the /// runtime of a program; it is cached for future calls. pub async fn is_sandboxed() -> bool { if let Some(cached_value) = IS_SANDBOXED.get() { return *cached_value; } let new_value = crate::helpers::is_flatpak().await || crate::helpers::is_snap().await || std::env::var("GTK_USE_PORTAL") .map(|v| v == "1") .unwrap_or(false); IS_SANDBOXED.set(new_value).unwrap(); // Safe to unwrap here new_value } pub use self::error::{Error, PortalError}; ashpd-0.9.1/src/proxy.rs000064400000000000000000000216671046102023000132640ustar 00000000000000use std::{fmt::Debug, future::ready, ops::Deref, sync::OnceLock}; use futures_util::{Stream, StreamExt}; use serde::{Deserialize, Serialize}; use zbus::zvariant::{ObjectPath, OwnedValue, Type}; #[cfg(feature = "tracing")] use zbus::Message; use crate::{ desktop::{HandleToken, Request}, Error, PortalError, }; pub(crate) const DESKTOP_DESTINATION: &str = "org.freedesktop.portal.Desktop"; pub(crate) const DESKTOP_PATH: &str = "/org/freedesktop/portal/desktop"; pub(crate) const DOCUMENTS_DESTINATION: &str = "org.freedesktop.portal.Documents"; pub(crate) const DOCUMENTS_PATH: &str = "/org/freedesktop/portal/documents"; pub(crate) const FLATPAK_DESTINATION: &str = "org.freedesktop.portal.Flatpak"; pub(crate) const FLATPAK_PATH: &str = "/org/freedesktop/portal/Flatpak"; pub(crate) const FLATPAK_DEVELOPMENT_DESTINATION: &str = "org.freedesktop.Flatpak"; pub(crate) const FLATPAK_DEVELOPMENT_PATH: &str = "/org/freedesktop/Flatpak/Development"; static SESSION: OnceLock = OnceLock::new(); #[derive(Debug)] pub struct Proxy<'a> { inner: zbus::Proxy<'a>, version: u32, } impl<'a> Proxy<'a> { pub(crate) async fn connection() -> zbus::Result { if let Some(cnx) = SESSION.get() { Ok(cnx.clone()) } else { let cnx = zbus::Connection::session().await?; // during `await` another task may have initialized the cell Ok(SESSION.get_or_init(|| cnx).clone()) } } pub async fn unique_name( prefix: &str, handle_token: &HandleToken, ) -> Result, Error> { let connection = Self::connection().await?; let unique_name = connection.unique_name().unwrap(); let unique_identifier = unique_name.trim_start_matches(':').replace('.', "_"); ObjectPath::try_from(format!("{prefix}/{unique_identifier}/{handle_token}")) .map_err(From::from) } pub async fn new

( interface: &'a str, path: P, destination: &'a str, ) -> Result, Error> where P: TryInto>, P::Error: Into, { let connection = Self::connection().await?; let inner: zbus::Proxy = zbus::ProxyBuilder::new(&connection) .interface(interface)? .path(path)? .destination(destination)? .build() .await?; let version = match inner.get_property::("version").await { Ok(v) => Ok(v), Err(e) => { // We forward the `PortalNotFound` error as this the perfect time to do so. // As all the interfaces used inside the crate have a `version` property making // getting the property would only fail if there is no portal implementation // found. let err = crate::Error::from(e); match err { crate::Error::PortalNotFound(_) => Err(err), _ => Ok(1), } } }?; Ok(Self { inner, version }) } pub async fn new_desktop_with_path

(interface: &'a str, path: P) -> Result, Error> where P: TryInto>, P::Error: Into, { Self::new(interface, path, DESKTOP_DESTINATION).await } pub async fn new_desktop(interface: &'a str) -> Result, Error> { Self::new(interface, DESKTOP_PATH, DESKTOP_DESTINATION).await } pub async fn new_documents(interface: &'a str) -> Result, Error> { Self::new(interface, DOCUMENTS_PATH, DOCUMENTS_DESTINATION).await } pub async fn new_flatpak(interface: &'a str) -> Result, Error> { Self::new(interface, FLATPAK_PATH, FLATPAK_DESTINATION).await } pub async fn new_flatpak_with_path

(interface: &'a str, path: P) -> Result, Error> where P: TryInto>, P::Error: Into, { Self::new(interface, path, FLATPAK_DESTINATION).await } pub async fn new_flatpak_development(interface: &'a str) -> Result, Error> { Self::new( interface, FLATPAK_DEVELOPMENT_PATH, FLATPAK_DEVELOPMENT_DESTINATION, ) .await } pub async fn request( &self, handle_token: &HandleToken, method_name: &'static str, body: impl Serialize + Type + Debug, ) -> Result, Error> where T: for<'de> Deserialize<'de> + Type + Debug, { let mut request = Request::from_unique_name(handle_token).await?; futures_util::try_join!(request.prepare_response(), async { self.call_method(method_name, &body) .await .map_err(From::from) })?; Ok(request) } pub(crate) async fn empty_request( &self, handle_token: &HandleToken, method_name: &'static str, body: impl Serialize + Type + Debug, ) -> Result, Error> { self.request(handle_token, method_name, body).await } /// Returns the version of the interface pub fn version(&self) -> u32 { self.version } pub(crate) async fn call( &self, method_name: &'static str, body: impl Serialize + Type + Debug, ) -> Result where R: for<'de> Deserialize<'de> + Type, { #[cfg(feature = "tracing")] { tracing::info!("Calling method {}:{}", self.interface(), method_name); tracing::debug!("With body {:#?}", body); } let msg = self .call_method(method_name, &body) .await .map_err::(From::from)?; let reply = msg.body().deserialize::()?; Ok(reply) } pub(crate) async fn call_versioned( &self, method_name: &'static str, body: impl Serialize + Type + Debug, req_version: u32, ) -> Result where R: for<'de> Deserialize<'de> + Type, { let version = self.version(); if version >= req_version { self.call::(method_name, body).await } else { Err(Error::RequiresVersion(req_version, version)) } } pub async fn property(&self, property_name: &'static str) -> Result where T: TryFrom, zbus::Error: From<>::Error>, { self.inner .get_property::(property_name) .await .map_err(From::from) } pub(crate) async fn property_versioned( &self, property_name: &'static str, req_version: u32, ) -> Result where T: TryFrom, zbus::Error: From<>::Error>, { let version = self.version(); if version >= req_version { self.property::(property_name).await } else { Err(Error::RequiresVersion(req_version, version)) } } pub(crate) async fn signal_with_args( &self, name: &'static str, args: &[(u8, &str)], ) -> Result, Error> where I: for<'de> Deserialize<'de> + Type + Debug, { Ok(self .inner .receive_signal_with_args(name, args) .await? .filter_map({ #[cfg(not(feature = "tracing"))] { move |msg| ready(msg.body().deserialize().ok()) } #[cfg(feature = "tracing")] { let ifc = self.interface().to_owned(); move |msg| ready(trace_body(name, &ifc, msg)) } })) } pub(crate) async fn signal(&self, name: &'static str) -> Result, Error> where I: for<'de> Deserialize<'de> + Type + Debug, { Ok(self.inner.receive_signal(name).await?.filter_map({ #[cfg(not(feature = "tracing"))] { move |msg| ready(msg.body().deserialize().ok()) } #[cfg(feature = "tracing")] { let ifc = self.interface().to_owned(); move |msg| ready(trace_body(name, &ifc, msg)) } })) } } #[cfg(feature = "tracing")] fn trace_body(name: &'static str, ifc: &str, msg: Message) -> Option where I: for<'de> Deserialize<'de> + Type + Debug, { tracing::info!("Received signal '{name}' on '{ifc}'"); match msg.body().deserialize() { Ok(body) => { tracing::debug!("With body {body:#?}"); Some(body) } Err(e) => { tracing::warn!("Error obtaining body: {e:#?}"); None } } } impl<'a> Deref for Proxy<'a> { type Target = zbus::Proxy<'a>; fn deref(&self) -> &Self::Target { &self.inner } } ashpd-0.9.1/src/window_identifier/gtk4.rs000064400000000000000000000174431046102023000164620ustar 00000000000000#[cfg(feature = "raw_handle")] use std::ptr::NonNull; #[cfg(feature = "gtk4_wayland")] use std::sync::Arc; #[cfg(feature = "gtk4_wayland")] use futures_util::lock::Mutex; use gdk::Backend; #[cfg(feature = "raw_handle")] use glib::translate::ToGlibPtr; use gtk4::{gdk, glib, prelude::*}; #[cfg(feature = "raw_handle")] use raw_window_handle::{ DisplayHandle, RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, WindowHandle, XlibDisplayHandle, XlibWindowHandle, }; use super::WindowIdentifierType; #[cfg(feature = "gtk4_wayland")] const WINDOW_HANDLE_KEY: &str = "ashpd-wayland-gtk4-window-handle"; pub struct Gtk4WindowIdentifier { #[allow(dead_code)] native: gtk4::Native, type_: WindowIdentifierType, exported: bool, } impl Gtk4WindowIdentifier { pub async fn new(native: &impl glib::prelude::IsA) -> Option { let surface = native.surface()?; match surface.display().backend() { #[cfg(feature = "gtk4_wayland")] Backend::Wayland => { let top_level = surface .downcast_ref::() .unwrap(); let handle = unsafe { if let Some(mut handle) = top_level.data(WINDOW_HANDLE_KEY) { let (handle, ref_count): &mut (Option, u8) = handle.as_mut(); *ref_count += 1; handle.clone() } else { let (sender, receiver) = futures_channel::oneshot::channel::>(); let sender = Arc::new(Mutex::new(Some(sender))); let result = top_level.export_handle(glib::clone!(#[strong] sender, move |_, handle| { let handle = handle.map(ToOwned::to_owned); glib::spawn_future_local(glib::clone!(#[strong] sender, #[strong] handle, async move { if let Some(m) = sender.lock().await.take() { match handle { Ok(h) => { let _ = m.send(Some(h.to_string())); }, Err(_err) => { let _ = m.send(None); #[cfg(feature = "tracing")] tracing::warn!("Failed to export window identifier. The compositor doesn't support xdg-foreign protocol. {_err}"); } } } })); })); if !result { return None; } let handle = receiver.await.ok().flatten(); top_level.set_data(WINDOW_HANDLE_KEY, (handle.clone(), 1)); handle } }; Some(Gtk4WindowIdentifier { native: native.clone().upcast(), exported: handle.is_some(), type_: WindowIdentifierType::Wayland(handle.unwrap_or_default()), }) } #[cfg(feature = "gtk4_x11")] Backend::X11 => { let xid = surface .downcast_ref::() .map(|w| w.xid())?; Some(Gtk4WindowIdentifier { native: native.clone().upcast(), exported: false, type_: WindowIdentifierType::X11(xid), }) } _ => None, } } #[cfg(feature = "raw_handle")] pub fn as_raw_window_handle(&self) -> WindowHandle<'_> { unsafe { let raw_handle = match self.type_ { #[cfg(feature = "gtk4_wayland")] WindowIdentifierType::Wayland(_) => { let surface = self.native.surface().unwrap(); RawWindowHandle::Wayland(WaylandWindowHandle::new( NonNull::new(gdk4wayland::ffi::gdk_wayland_surface_get_wl_surface( surface .downcast_ref::() .unwrap() .to_glib_none() .0, )) .expect("Identifier must be attached to a wl_surface"), )) } #[cfg(feature = "gtk4_x11")] WindowIdentifierType::X11(xid) => RawWindowHandle::Xlib(XlibWindowHandle::new(xid)), }; WindowHandle::borrow_raw(raw_handle) } } #[cfg(feature = "raw_handle")] pub fn as_raw_display_handle(&self) -> DisplayHandle<'_> { let surface = self.native.surface().unwrap(); let display = surface.display(); unsafe { let raw_handle = match self.type_ { #[cfg(feature = "gtk4_wayland")] WindowIdentifierType::Wayland(_) => { RawDisplayHandle::Wayland(WaylandDisplayHandle::new( NonNull::new(gdk4wayland::ffi::gdk_wayland_display_get_wl_display( display .downcast_ref::() .unwrap() .to_glib_none() .0, )) .expect("Identifier must be attached to a wl_display"), )) } #[cfg(feature = "gtk4_x11")] WindowIdentifierType::X11(_xid) => RawDisplayHandle::Xlib(XlibDisplayHandle::new( NonNull::new(gdk4x11::ffi::gdk_x11_display_get_xdisplay( display .downcast_ref::() .unwrap() .to_glib_none() .0, )), display .downcast_ref::() .unwrap() .screen() .screen_number(), )), }; DisplayHandle::borrow_raw(raw_handle) } } } impl std::fmt::Display for Gtk4WindowIdentifier { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&format!("{}", self.type_)) } } impl Drop for Gtk4WindowIdentifier { fn drop(&mut self) { if !self.exported { return; } match self.type_ { #[cfg(feature = "gtk4_wayland")] WindowIdentifierType::Wayland(_) => { let surface = self.native.surface().unwrap(); let top_level = surface .downcast_ref::() .unwrap(); unsafe { let (_handle, ref_count): &mut (Option, u8) = top_level.data(WINDOW_HANDLE_KEY).unwrap().as_mut(); if ref_count > &mut 1 { *ref_count -= 1; return; } top_level.unexport_handle(); #[cfg(feature = "tracing")] tracing::debug!("Unexporting handle: {_handle:?}"); let _ = top_level.steal_data::<(Option, u8)>(WINDOW_HANDLE_KEY); } } _ => (), } } } ashpd-0.9.1/src/window_identifier/mod.rs000064400000000000000000000276231046102023000163710ustar 00000000000000use std::{fmt, str::FromStr}; #[cfg(all(feature = "raw_handle", feature = "gtk4"))] use raw_window_handle::{ DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WindowHandle, }; #[cfg(feature = "raw_handle")] use raw_window_handle::{RawDisplayHandle, RawWindowHandle}; use serde::{ser::Serializer, Deserialize, Serialize}; use zbus::zvariant::Type; /// Most portals interact with the user by showing dialogs. /// These dialogs should generally be placed on top of the application window /// that triggered them. To arrange this, the compositor needs to know about the /// application window. Many portal requests expect a [`WindowIdentifier`] for /// this reason. /// /// Under X11, the [`WindowIdentifier`] should have the form `x11:XID`, where /// XID is the XID of the application window in hexadecimal. Under Wayland, it /// should have the form `wayland:HANDLE`, where HANDLE is a surface handle /// obtained with the [xdg-foreign](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/unstable/xdg-foreign/xdg-foreign-unstable-v2.xml) protocol. /// /// See also [Parent window identifiers](https://flatpak.github.io/xdg-desktop-portal/docs/window-identifiers.html). /// /// # Usage /// /// ## From an X11 XID /// /// ```rust,ignore /// let identifier = WindowIdentifier::from_xid(212321); /// /// /// Open some portals /// ``` /// /// ## From a Wayland Surface /// /// The `wayland` feature must be enabled. The exported surface handle will be /// unexported on `Drop`. /// /// ```text /// // let wl_surface = some_surface; /// // let identifier = WindowIdentifier::from_wayland(wl_surface).await; /// /// /// Open some portals /// ``` /// /// Or using a raw `wl_surface` pointer /// /// ```text /// // let wl_surface_ptr = some_surface; /// // let wl_display_ptr = corresponding_display; /// // let identifier = WindowIdentifier::from_wayland_raw(wl_surface_ptr, wl_display_ptr).await; /// /// /// Open some portals /// ``` /// /// ## With GTK 4 /// /// The feature `gtk4` must be enabled. You can get a /// [`WindowIdentifier`] from a [`IsA`](https://gtk-rs.org/gtk4-rs/stable/latest/docs/gtk4/struct.Native.html) using `WindowIdentifier::from_native` /// /// ```rust, ignore /// let widget = gtk4::Button::new(); /// /// let ctx = glib::MainContext::default(); /// ctx.spawn_async(async move { /// let identifier = WindowIdentifier::from_native(&widget.native().unwrap()).await; /// /// /// Open some portals /// }); /// ``` /// The constructor should return a valid identifier under both X11 and Wayland /// and fallback to the [`Default`] implementation otherwise. /// /// ## Other Toolkits /// /// If you have access to `RawWindowHandle` you can convert it to a /// [`WindowIdentifier`] with /// /// ```rust, ignore /// let handle = RawWindowHandle::Xlib(XlibHandle::empty()); /// let identifier = WindowIdentifier::from_raw_handle(handle, None); /// /// /// Open some portals /// ``` /// /// In case you don't have access to a WindowIdentifier: /// ```rust /// use ashpd::WindowIdentifier; /// /// let identifier = WindowIdentifier::default(); /// ``` #[derive(Default, Type)] #[zvariant(signature = "s")] #[doc(alias = "XdpParent")] #[non_exhaustive] pub enum WindowIdentifier { /// Gtk 4 Window Identifier #[cfg(any(feature = "gtk4_wayland", feature = "gtk4_x11"))] #[doc(hidden)] Gtk4(Gtk4WindowIdentifier), #[cfg(feature = "wayland")] #[doc(hidden)] Wayland(WaylandWindowIdentifier), #[doc(hidden)] X11(WindowIdentifierType), #[doc(hidden)] #[default] None, } unsafe impl Send for WindowIdentifier {} unsafe impl Sync for WindowIdentifier {} impl Serialize for WindowIdentifier { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&self.to_string()) } } impl std::fmt::Display for WindowIdentifier { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { #[cfg(any(feature = "gtk4_wayland", feature = "gtk4_x11"))] Self::Gtk4(identifier) => f.write_str(&format!("{identifier}")), #[cfg(feature = "wayland")] Self::Wayland(identifier) => f.write_str(&format!("{identifier}")), Self::X11(identifier) => f.write_str(&format!("{identifier}")), Self::None => f.write_str(""), } } } impl std::fmt::Debug for WindowIdentifier { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("WindowIdentifier") .field(&format!("{self}")) .finish() } } impl WindowIdentifier { #[cfg(any(feature = "gtk4_wayland", feature = "gtk4_x11"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "gtk4_wayland", feature = "gtk4_x11"))))] /// Creates a [`WindowIdentifier`] from a [`gtk4::Native`](https://docs.gtk.org/gtk4/class.Native.html). /// /// The constructor returns a valid handle under both Wayland & x11. /// /// **Note** the function has to be async as the Wayland handle retrieval /// API is async as well. #[doc(alias = "xdp_parent_new_gtk")] pub async fn from_native(native: &impl ::gtk4::prelude::IsA<::gtk4::Native>) -> Self { match Gtk4WindowIdentifier::new(native).await { Some(identifier) => Self::Gtk4(identifier), None => Self::default(), } } #[cfg(feature = "raw_handle")] #[cfg_attr(docsrs, doc(cfg(feature = "raw_handle")))] /// Create an instance of [`WindowIdentifier`] from a /// [`RawWindowHandle`](raw_window_handle::RawWindowHandle). /// /// The constructor returns a valid handle under both Wayland & X11. /// /// This method is only async and requires a `RawDisplayHandle` only for /// Wayland handles. pub async fn from_raw_handle( window_handle: &RawWindowHandle, display_handle: Option<&RawDisplayHandle>, ) -> Self { use raw_window_handle::{ RawDisplayHandle::Wayland as DisplayHandle, RawWindowHandle::{Wayland, Xcb, Xlib}, }; match (window_handle, display_handle) { (Wayland(wl_handle), Some(DisplayHandle(wl_display))) => unsafe { Self::from_wayland_raw(wl_handle.surface.as_ptr(), wl_display.display.as_ptr()) .await }, (Xlib(x_handle), _) => Self::from_xid(x_handle.window), (Xcb(x_handle), _) => Self::from_xid(x_handle.window.get().into()), _ => Self::default(), // Fallback to default } } /// Create an instance of [`WindowIdentifier`] from an X11 window's XID. pub fn from_xid(xid: std::os::raw::c_ulong) -> Self { Self::X11(WindowIdentifierType::X11(xid)) } #[cfg(feature = "wayland")] #[cfg_attr(docsrs, doc(cfg(feature = "wayland")))] /// Create an instance of [`WindowIdentifier`] from a Wayland surface. /// /// # Safety /// /// Both pointers have to be valid surface and display pointers. You must /// ensure the `display_ptr` lives longer than the returned /// `WindowIdentifier`. pub async unsafe fn from_wayland_raw( surface_ptr: *mut std::ffi::c_void, display_ptr: *mut std::ffi::c_void, ) -> Self { match WaylandWindowIdentifier::from_raw(surface_ptr, display_ptr).await { Some(identifier) => Self::Wayland(identifier), None => Self::default(), } } #[cfg(feature = "wayland")] #[cfg_attr(docsrs, doc(cfg(feature = "wayland")))] /// Create an instance of [`WindowIdentifier`] from a Wayland surface. pub async fn from_wayland(surface: &wayland_client::protocol::wl_surface::WlSurface) -> Self { match WaylandWindowIdentifier::new(surface).await { Some(identifier) => Self::Wayland(identifier), None => Self::default(), } } } #[cfg(all(feature = "raw_handle", feature = "gtk4"))] impl HasDisplayHandle for WindowIdentifier { /// Convert a [`WindowIdentifier`] to /// [`RawDisplayHandle`](raw_window_handle::RawDisplayHandle`). /// /// # Panics /// /// If you attempt to convert a [`WindowIdentifier`] created from a /// [`RawDisplayHandle`](raw_window_handle::RawDisplayHandle`) instead of /// the gtk4 constructors. fn display_handle(&self) -> Result, HandleError> { match self { #[cfg(feature = "gtk4")] Self::Gtk4(identifier) => Ok(identifier.as_raw_display_handle()), _ => unreachable!(), } } } #[cfg(all(feature = "raw_handle", feature = "gtk4"))] impl HasWindowHandle for WindowIdentifier { /// Convert a [`WindowIdentifier`] to /// [`RawWindowHandle`](raw_window_handle::RawWindowHandle`). /// /// # Panics /// /// If you attempt to convert a [`WindowIdentifier`] created from a /// [`RawWindowHandle`](raw_window_handle::RawWindowHandle`) instead of /// the gtk4 constructors. fn window_handle(&self) -> Result, HandleError> { match self { #[cfg(feature = "gtk4")] Self::Gtk4(identifier) => Ok(identifier.as_raw_window_handle()), _ => unreachable!(), } } } /// Supported WindowIdentifier kinds #[derive(Debug, Clone, PartialEq, Eq, Type)] #[zvariant(signature = "s")] pub enum WindowIdentifierType { X11(std::os::raw::c_ulong), #[allow(dead_code)] Wayland(String), } impl fmt::Display for WindowIdentifierType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::X11(xid) => f.write_str(&format!("x11:0x{xid:x}")), Self::Wayland(handle) => f.write_str(&format!("wayland:{handle}")), } } } impl FromStr for WindowIdentifierType { type Err = PortalError; fn from_str(s: &str) -> Result { let (kind, handle) = s .split_once(':') .ok_or_else(|| PortalError::InvalidArgument("Invalid Window Identifier".to_owned()))?; match kind { "x11" => { let handle = handle.trim_start_matches("0x"); Ok(Self::X11( std::os::raw::c_ulong::from_str_radix(handle, 16) .map_err(|_| PortalError::InvalidArgument(format!("Wrong XID {handle}")))?, )) } "wayland" => Ok(Self::Wayland(handle.to_owned())), t => Err(PortalError::InvalidArgument(format!( "Invalid Window Identifier type {t}", ))), } } } impl<'de> Deserialize<'de> for WindowIdentifierType { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let handle = String::deserialize(deserializer)?; Self::from_str(&handle) .map_err(|e| serde::de::Error::custom(format!("Invalid Window identifier {e}"))) } } #[cfg(any(feature = "gtk4_wayland", feature = "gtk4_x11"))] mod gtk4; #[cfg(any(feature = "gtk4_wayland", feature = "gtk4_x11"))] pub use self::gtk4::Gtk4WindowIdentifier; use crate::PortalError; #[cfg(feature = "wayland")] mod wayland; #[cfg(feature = "wayland")] pub use self::wayland::WaylandWindowIdentifier; #[cfg(test)] mod tests { use std::str::FromStr; use super::WindowIdentifier; use crate::window_identifier::WindowIdentifierType; #[test] fn test_serialize() { let x11 = WindowIdentifier::from_xid(1024); assert_eq!(x11.to_string(), "x11:0x400"); assert_eq!(WindowIdentifier::default().to_string(), ""); assert_eq!( WindowIdentifierType::from_str("x11:0x11432").unwrap(), WindowIdentifierType::X11(70706) ); assert_eq!( WindowIdentifierType::from_str("wayland:Somerandomchars").unwrap(), WindowIdentifierType::Wayland("Somerandomchars".to_owned()) ); assert!(WindowIdentifierType::from_str("some_handle").is_err()); assert!(WindowIdentifierType::from_str("some_type:some_handle").is_err()); } } ashpd-0.9.1/src/window_identifier/wayland.rs000064400000000000000000000174061046102023000172470ustar 00000000000000use std::fmt; use wayland_backend::sys::client::Backend; use wayland_client::{ protocol::{wl_registry, wl_surface::WlSurface}, Proxy, QueueHandle, }; use wayland_protocols::xdg::foreign::{ zv1::client::{ zxdg_exported_v1::{self, ZxdgExportedV1}, zxdg_exporter_v1::ZxdgExporterV1, }, zv2::client::{ zxdg_exported_v2::{self, ZxdgExportedV2}, zxdg_exporter_v2::ZxdgExporterV2, }, }; use super::WindowIdentifierType; // Supported versions. const ZXDG_EXPORTER_V1: u32 = 1; const ZXDG_EXPORTER_V2: u32 = 1; #[derive(Debug)] pub struct WaylandWindowIdentifier { exported: Exported, type_: WindowIdentifierType, } #[derive(Debug)] enum Exported { V1(ZxdgExportedV1), V2(ZxdgExportedV2), } impl Exported { fn destroy(&self) { match self { Self::V1(exported) => exported.destroy(), Self::V2(exported) => exported.destroy(), } } } #[derive(Debug)] enum Exporter { V1(ZxdgExporterV1), V2(ZxdgExporterV2), } impl WaylandWindowIdentifier { pub async fn new(surface: &WlSurface) -> Option { let backend = surface.backend().upgrade()?; let conn = wayland_client::Connection::from_backend(backend); Self::new_inner(conn, surface).await } pub async unsafe fn from_raw( surface_ptr: *mut std::ffi::c_void, display_ptr: *mut std::ffi::c_void, ) -> Option { if surface_ptr.is_null() || display_ptr.is_null() { return None; } let backend = Backend::from_foreign_display(display_ptr as *mut _); let conn = wayland_client::Connection::from_backend(backend); let obj_id = wayland_backend::sys::client::ObjectId::from_ptr( WlSurface::interface(), surface_ptr as *mut _, ) .ok()?; let surface = WlSurface::from_id(&conn, obj_id).ok()?; Self::new_inner(conn, &surface).await } async fn new_inner(conn: wayland_client::Connection, surface: &WlSurface) -> Option { let (sender, receiver) = futures_channel::oneshot::channel::>(); // Cheap clone, protocol objects are essentially smart pointers let surface = surface.clone(); std::thread::spawn(move || match wayland_export_handle(conn, &surface) { Ok(window_handle) => sender.send(Some(window_handle)).unwrap(), Err(_err) => { #[cfg(feature = "tracing")] tracing::info!("Could not get wayland window identifier: {_err}"); sender.send(None).unwrap(); } }); receiver.await.unwrap() } } impl fmt::Display for WaylandWindowIdentifier { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&format!("{}", self.type_)) } } impl Drop for WaylandWindowIdentifier { fn drop(&mut self) { self.exported.destroy(); #[cfg(feature = "tracing")] if let WindowIdentifierType::Wayland(ref handle) = self.type_ { tracing::debug!("Unexporting handle: {handle}"); } } } #[derive(Default, Debug)] struct State { handle: String, exporter: Option, } impl wayland_client::Dispatch for State { fn event( state: &mut Self, _proxy: &ZxdgExportedV1, event: ::Event, _data: &(), _connhandle: &wayland_client::Connection, _qhandle: &QueueHandle, ) { if let zxdg_exported_v1::Event::Handle { handle } = event { state.handle = handle; } } } impl wayland_client::Dispatch for State { fn event( state: &mut Self, _proxy: &ZxdgExportedV2, event: ::Event, _data: &(), _connhandle: &wayland_client::Connection, _qhandle: &QueueHandle, ) { if let zxdg_exported_v2::Event::Handle { handle } = event { state.handle = handle; } } } impl wayland_client::Dispatch for State { fn event( _state: &mut Self, _proxy: &ZxdgExporterV1, _event: ::Event, _data: &(), _connhandle: &wayland_client::Connection, _qhandle: &QueueHandle, ) { } } impl wayland_client::Dispatch for State { fn event( _state: &mut Self, _proxy: &ZxdgExporterV2, _event: ::Event, _data: &(), _connhandle: &wayland_client::Connection, _qhandle: &QueueHandle, ) { } } impl wayland_client::Dispatch for State { fn event( state: &mut Self, registry: &wl_registry::WlRegistry, event: wl_registry::Event, _: &(), _: &wayland_client::Connection, qhandle: &QueueHandle, ) { if let wl_registry::Event::Global { name, interface, version, } = event { match interface.as_str() { "zxdg_exporter_v1" => { #[cfg(feature = "tracing")] tracing::info!("Found wayland interface {interface} v{version}"); let exporter = registry.bind::( name, version.min(ZXDG_EXPORTER_V1), qhandle, (), ); match state.exporter { Some(Exporter::V2(_)) => (), _ => state.exporter = Some(Exporter::V1(exporter)), } } "zxdg_exporter_v2" => { #[cfg(feature = "tracing")] tracing::info!("Found wayland interface {interface} v{version}"); let exporter = registry.bind::( name, version.min(ZXDG_EXPORTER_V2), qhandle, (), ); state.exporter = Some(Exporter::V2(exporter)); } _ => (), } } } } /// A helper to export a wayland handle from a surface and a connection /// /// Needed for converting a RawWindowHandle to a WindowIdentifier. fn wayland_export_handle( conn: wayland_client::Connection, surface: &WlSurface, ) -> Result> { let display = conn.display(); let mut event_queue = conn.new_event_queue(); let qhandle = event_queue.handle(); let mut state = State::default(); display.get_registry(&qhandle, ()); event_queue.roundtrip(&mut state)?; let exported = match state.exporter.take() { Some(Exporter::V2(exporter)) => { let exp = exporter.export_toplevel(surface, &qhandle, ()); event_queue.roundtrip(&mut state)?; exporter.destroy(); Some(Exported::V2(exp)) } Some(Exporter::V1(exporter)) => { let exp = exporter.export(surface, &qhandle, ()); event_queue.roundtrip(&mut state)?; exporter.destroy(); Some(Exported::V1(exp)) } None => { #[cfg(feature = "tracing")] tracing::error!( "The compositor does not support the zxdg_exporter_v1 nor zxdg_exporter_v2 protocols" ); None } }; if let Some(exported) = exported { Ok(WaylandWindowIdentifier { exported, type_: WindowIdentifierType::Wayland(state.handle), }) } else { Err(Box::new(crate::Error::NoResponse)) } }