diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d12b57b76..347db5997 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -23,7 +23,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: check - args: --features "gtk4,pipewire,wayland,raw_handle,tracing" + args: --features "gtk4,pipewire,wayland,raw_handle,tracing,backend" test: name: Test Suite @@ -42,7 +42,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: test - args: --features "gtk4,pipewire,wayland,raw_handle,tracing" + args: --features "gtk4,pipewire,wayland,raw_handle,tracing,backend" fmt: name: Rustfmt @@ -82,4 +82,4 @@ jobs: - uses: actions-rs/cargo@v1 with: command: clippy - args: --features "gtk4,pipewire,wayland,raw_handle,tracing" -- -D warnings + args: --features "gtk4,pipewire,wayland,raw_handle,tracing,backend" -- -D warnings diff --git a/.github/workflows/backend-demo-ci.yml b/.github/workflows/backend-demo-ci.yml new file mode 100644 index 000000000..f6697e061 --- /dev/null +++ b/.github/workflows/backend-demo-ci.yml @@ -0,0 +1,69 @@ +on: + push: + branches: [master] + pull_request: + +name: Backend Demo CI + +jobs: + check: + name: Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - uses: actions-rs/cargo@v1 + with: + command: check + args: --manifest-path=backend-demo/Cargo.toml + + test: + name: Test Suite + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=backend-demo/Cargo.toml + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - 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: --manifest-path=backend-demo/Cargo.toml --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - 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: --manifest-path=backend-demo/Cargo.toml -- -D warnings diff --git a/.gitignore b/.gitignore index 3cb4bd095..7a7d0521c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,9 +15,11 @@ ashpd-demo/_build/ ashpd-demo/builddir/ ashpd-demo/src/config.rs ashpd-demo/target/ +backend-demo/target .flatpak .vscode _build/ builddir/ session-test +.fenv diff --git a/Cargo.toml b/Cargo.toml index e8f131666..ae20be837 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,9 @@ rust-version = "1.75" [features] async-std = ["zbus/async-io", "dep:async-fs", "dep:async-net"] default = ["async-std"] + +backend = ["async-trait", "nix", "futures-channel/sink", "tokio"] + gtk4 = ["gtk4_x11", "gtk4_wayland"] gtk4_wayland = ["gdk4wayland", "glib", "dep:gtk4"] gtk4_x11 = ["gdk4x11", "glib", "dep:gtk4"] @@ -25,6 +28,7 @@ wayland = ["wayland-client", "wayland-protocols", "wayland-backend"] [dependencies] async-fs = { version = "2.1.0", optional = true } async-net = { version = "2.0.0", optional = true } +async-trait = {version = "0.1.60", optional = true} enumflags2 = "0.7" futures-channel = "0.3" futures-util = "0.3" @@ -32,6 +36,7 @@ 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 } +nix = { version = "0.29", optional = true, features = ["user"], default-features = false} pipewire = { version = "0.8", optional = true } rand = { version = "0.8", default-features = false } raw-window-handle = { version = "0.6", optional = true } diff --git a/README.md b/README.md index e13da470c..fe9b4e843 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ pub async fn run() -> ashpd::Result<()> { | 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 | +| backend | *unstable* Enables APIs useful for writing portals implementations | No | | 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 | diff --git a/backend-demo/Cargo.toml b/backend-demo/Cargo.toml new file mode 100644 index 000000000..9ca545df2 --- /dev/null +++ b/backend-demo/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "ashpd-backend-demo" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0" +async-trait = "0.1.60" +tokio = { version = "1.0", features = ["io-util", "net", "time", "macros", "rt-multi-thread"] } +byteorder = "1.4.3" +futures-channel = "0.3.25" +futures-util = "0.3.25" +tracing = "0.1" +tracing-subscriber = "0.3.16" +url = "2.3.1" +zbus = "4.2" + +[dependencies.ashpd] +path = "../" +features = ["backend", "tracing"] +default-features = false diff --git a/backend-demo/data/ashpd-backend-demo.portal b/backend-demo/data/ashpd-backend-demo.portal new file mode 100644 index 000000000..4952a076a --- /dev/null +++ b/backend-demo/data/ashpd-backend-demo.portal @@ -0,0 +1,3 @@ +[portal] +DBusName=org.freedesktop.impl.portal.desktop.ashpd-backend-demo +Interfaces=org.freedesktop.impl.portal.Account;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Wallpaper; diff --git a/backend-demo/src/account.rs b/backend-demo/src/account.rs new file mode 100644 index 000000000..f6fe32513 --- /dev/null +++ b/backend-demo/src/account.rs @@ -0,0 +1,41 @@ +use std::num::NonZeroU32; + +use ashpd::{ + backend::{ + account::{AccountImpl, UserInformationOptions}, + request::RequestImpl, + }, + desktop::{account::UserInformation, Response}, + AppID, WindowIdentifierType, +}; +use async_trait::async_trait; + +#[derive(Default)] +pub struct Account; + +#[async_trait] +impl RequestImpl for Account { + async fn close(&self) { + tracing::debug!("IN Close()"); + } +} + +#[async_trait] +impl AccountImpl for Account { + const VERSION: NonZeroU32 = NonZeroU32::MIN; + + async fn get_information( + &self, + _app_id: AppID, + _window_identifier: Option, + _options: UserInformationOptions, + ) -> Response { + match UserInformation::current_user().await { + Ok(info) => Response::ok(info), + Err(err) => { + tracing::error!("Failed to get user info: {err}"); + Response::other() + } + } + } +} diff --git a/backend-demo/src/main.rs b/backend-demo/src/main.rs new file mode 100644 index 000000000..ff9418838 --- /dev/null +++ b/backend-demo/src/main.rs @@ -0,0 +1,54 @@ +use futures_util::future::pending; +mod account; +mod screenshot; +mod secret; +mod wallpaper; + +use account::Account; +use screenshot::Screenshot; +use secret::Secret; +use wallpaper::Wallpaper; + +// NOTE Uncomment if you have ashpd-backend-demo.portal installed. +// const NAME: &str = "org.freedesktop.impl.portal.desktop.ashpd-backend-demo"; +const NAME: &str = "org.freedesktop.impl.portal.desktop.gnome"; +// Run with +// RUST_LOG=ashpd_backend_demo=debug,ashpd=debug cargo run --manifest-path +// ./backend-demo/Cargo.toml + +#[tokio::main] +async fn main() -> ashpd::Result<()> { + // Enable debug with `RUST_LOG=ashpd_backend_demo=debug COMMAND`. + tracing_subscriber::fmt::init(); + + tracing::debug!("Serving interfaces at {NAME}"); + let backend = ashpd::backend::Backend::new(NAME).await?; + let account = ashpd::backend::account::Account::new(Account, &backend).await?; + tokio::task::spawn(async move { + loop { + account.try_next().await.unwrap(); + } + }); + let wallpaper = ashpd::backend::wallpaper::Wallpaper::new(Wallpaper, &backend).await?; + tokio::task::spawn(async move { + loop { + wallpaper.try_next().await.unwrap(); + } + }); + let screenshot = ashpd::backend::screenshot::Screenshot::new(Screenshot, &backend).await?; + tokio::task::spawn(async move { + loop { + screenshot.try_next().await.unwrap(); + } + }); + let secret = ashpd::backend::secret::Secret::new(Secret, &backend).await?; + tokio::task::spawn(async move { + loop { + secret.try_next().await.unwrap(); + } + }); + + loop { + pending::<()>().await; + } +} diff --git a/backend-demo/src/screenshot.rs b/backend-demo/src/screenshot.rs new file mode 100644 index 000000000..126b052eb --- /dev/null +++ b/backend-demo/src/screenshot.rs @@ -0,0 +1,46 @@ +use std::num::NonZeroU32; + +use ashpd::{ + backend::{ + request::RequestImpl, + screenshot::{ColorOptions, ScreenshotImpl, ScreenshotOptions}, + }, + desktop::{screenshot::Screenshot as ScreenshotResponse, Color, Response}, + AppID, WindowIdentifierType, +}; +use async_trait::async_trait; + +#[derive(Default)] +pub struct Screenshot; + +#[async_trait] +impl RequestImpl for Screenshot { + async fn close(&self) { + tracing::debug!("IN Close()"); + } +} + +#[async_trait] +impl ScreenshotImpl for Screenshot { + const VERSION: NonZeroU32 = NonZeroU32::MIN; + + async fn screenshot( + &self, + _app_id: AppID, + _window_identifier: Option, + _options: ScreenshotOptions, + ) -> Response { + Response::ok(ScreenshotResponse::new( + url::Url::parse("file:///some/sreenshot").unwrap(), + )) + } + + async fn pick_color( + &self, + _app_id: AppID, + _window_identifier: Option, + _options: ColorOptions, + ) -> Response { + Response::ok(Color::new(1.0, 1.0, 1.0)) + } +} diff --git a/backend-demo/src/secret.rs b/backend-demo/src/secret.rs new file mode 100644 index 000000000..cce03270a --- /dev/null +++ b/backend-demo/src/secret.rs @@ -0,0 +1,32 @@ +use std::{collections::HashMap, num::NonZeroU32}; + +use ashpd::{ + backend::{request::RequestImpl, secret::SecretImpl}, + desktop::Response, + zbus::zvariant::{self, OwnedValue}, + AppID, +}; +use async_trait::async_trait; + +#[derive(Default)] +pub struct Secret; + +#[async_trait] +impl RequestImpl for Secret { + async fn close(&self) { + tracing::debug!("IN Close()"); + } +} + +#[async_trait] +impl SecretImpl for Secret { + const VERSION: NonZeroU32 = NonZeroU32::MIN; + + async fn retrieve( + &self, + app_id: AppID, + fd: zvariant::OwnedFd, + ) -> Response> { + Response::Ok(Default::default()) + } +} diff --git a/backend-demo/src/wallpaper.rs b/backend-demo/src/wallpaper.rs new file mode 100644 index 000000000..58e13fa4c --- /dev/null +++ b/backend-demo/src/wallpaper.rs @@ -0,0 +1,36 @@ +use std::num::NonZeroU32; + +use ashpd::{ + backend::{ + request::RequestImpl, + wallpaper::{WallpaperImpl, WallpaperOptions}, + }, + desktop::Response, + AppID, WindowIdentifierType, +}; +use async_trait::async_trait; + +#[derive(Default)] +pub struct Wallpaper; + +#[async_trait] +impl RequestImpl for Wallpaper { + async fn close(&self) { + tracing::debug!("IN Close()"); + } +} + +#[async_trait] +impl WallpaperImpl for Wallpaper { + const VERSION: NonZeroU32 = NonZeroU32::MIN; + + async fn with_uri( + &self, + _app_id: AppID, + _window_identifier: Option, + _uri: url::Url, + _options: WallpaperOptions, + ) -> Response<()> { + Response::ok(()) + } +} diff --git a/src/app_id.rs b/src/app_id.rs index 6a11a755d..41a85de47 100644 --- a/src/app_id.rs +++ b/src/app_id.rs @@ -9,6 +9,18 @@ use zbus::zvariant::Type; #[derive(Debug, Serialize, Type, PartialEq, Eq, Hash, Clone)] pub struct AppID(String); +impl AppID { + #[cfg(all( + feature = "backend", + any(feature = "gtk4_x11", feature = "gtk4_wayland") + ))] + /// Retrieves the associated `gio::DesktopAppInfo` if found + pub fn app_info(&self) -> Option { + let desktop_file = format!("{}.desktop", self.0); + gtk4::gio::DesktopAppInfo::new(&desktop_file) + } +} + impl FromStr for AppID { type Err = crate::Error; fn from_str(value: &str) -> Result { diff --git a/src/backend/access.rs b/src/backend/access.rs new file mode 100644 index 000000000..572eb2de6 --- /dev/null +++ b/src/backend/access.rs @@ -0,0 +1,178 @@ +use std::{collections::HashMap, num::NonZeroU32, sync::Arc}; + +use async_trait::async_trait; +use futures_channel::{ + mpsc::{UnboundedReceiver as Receiver, UnboundedSender as Sender}, + oneshot, +}; +use futures_util::{ + future::{try_select, Either}, + pin_mut, SinkExt, StreamExt, +}; +use tokio::sync::Mutex; + +use crate::{ + backend::{ + request::{Request, RequestImpl}, + Backend, + }, + desktop::request::Response, + zvariant::{OwnedObjectPath, OwnedValue}, + AppID, WindowIdentifierType, +}; + +#[async_trait] +pub trait AccessImpl { + const VERSION: NonZeroU32; + + async fn access_dialog( + &self, + app_id: AppID, + window_identifier: Option, + title: String, + subtitle: String, + body: String, + options: HashMap, + ) -> Response>; +} + +pub struct Access { + receiver: Arc>>, + cnx: zbus::Connection, + imp: Arc, +} + +impl Access { + pub async fn new(imp: T, backend: &Backend) -> zbus::Result { + let (sender, receiver) = futures_channel::mpsc::unbounded(); + let iface = AccessInterface::new(sender, T::VERSION); + backend.serve(iface).await?; + let provider = Self { + receiver: Arc::new(Mutex::new(receiver)), + imp: Arc::new(imp), + cnx: backend.cnx().clone(), + }; + + Ok(provider) + } + + pub async fn try_next(&self) -> Result<(), crate::Error> { + if let Some(action) = (*self.receiver.lock().await).next().await { + self.activate(action).await?; + } + Ok(()) + } + + async fn activate(&self, action: Action) -> Result<(), crate::Error> { + let Action::AccessDialog( + handle_path, + app_id, + window_identifier, + title, + subtitle, + body, + options, + sender, + ) = action; + let request = Request::new(Arc::clone(&self.imp), handle_path, &self.cnx).await?; + let imp = Arc::clone(&self.imp); + let future1 = async { + let result = imp + .access_dialog(app_id, window_identifier, title, subtitle, body, options) + .await; + let _ = sender.send(result); + Ok(()) as Result<(), crate::Error> + }; + let future2 = async { + request.next().await?; + Ok(()) as Result<(), crate::Error> + }; + + pin_mut!(future1); // 'select' requires Future + Unpin bounds + pin_mut!(future2); + match try_select(future1, future2).await { + Ok(_) => Ok(()), + Err(Either::Left((err, _))) => Err(err), + Err(Either::Right((err, _))) => Err(err), + }?; + Ok(()) + } +} + +enum Action { + AccessDialog( + OwnedObjectPath, + AppID, + Option, + String, + String, + String, + HashMap, + oneshot::Sender>>, + ), +} + +struct AccessInterface { + sender: Arc>>, + version: NonZeroU32, +} + +impl AccessInterface { + pub fn new(sender: Sender, version: NonZeroU32) -> Self { + Self { + sender: Arc::new(Mutex::new(sender)), + version, + } + } +} + +#[zbus::interface(name = "org.freedesktop.impl.portal.Access")] +impl AccessInterface { + #[dbus_interface(property, name = "version")] + fn version(&self) -> u32 { + self.version.into() + } + + #[allow(clippy::too_many_arguments)] + async fn access_dialog( + &self, + handle: OwnedObjectPath, + app_id: AppID, + window_identifier: &str, + title: String, + subtitle: String, + body: String, + options: HashMap, + ) -> Response> { + let (sender, receiver) = futures_channel::oneshot::channel(); + #[cfg(feature = "tracing")] + tracing::debug!("Access::AccessDialog"); + + let window_identifier = if window_identifier.is_empty() { + None + } else { + window_identifier.parse::().ok() + }; + + let _ = self + .sender + .lock() + .await + .send(Action::AccessDialog( + handle, + app_id, + window_identifier, + title, + subtitle, + body, + options, + sender, + )) + .await; + + let response = receiver.await.unwrap_or(Response::cancelled()); + #[cfg(feature = "tracing")] + tracing::debug!("Access::AccessDialog returned {:#?}", response); + response + } +} diff --git a/src/backend/account.rs b/src/backend/account.rs new file mode 100644 index 000000000..bc076ad38 --- /dev/null +++ b/src/backend/account.rs @@ -0,0 +1,170 @@ +use std::{num::NonZeroU32, sync::Arc}; + +use async_trait::async_trait; +use futures_channel::{ + mpsc::{UnboundedReceiver as Receiver, UnboundedSender as Sender}, + oneshot, +}; +use futures_util::{ + future::{try_select, Either}, + pin_mut, SinkExt, StreamExt, +}; +use tokio::sync::Mutex; + +use crate::{ + backend::{ + request::{Request, RequestImpl}, + Backend, + }, + desktop::{account::UserInformation, request::Response}, + zvariant::{DeserializeDict, OwnedObjectPath, Type}, + AppID, WindowIdentifierType, +}; + +#[derive(Debug, DeserializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct UserInformationOptions { + reason: Option, +} + +impl UserInformationOptions { + pub fn reason(&self) -> Option<&str> { + self.reason.as_deref() + } +} + +#[async_trait] +pub trait AccountImpl: RequestImpl { + const VERSION: NonZeroU32; + + async fn get_information( + &self, + app_id: AppID, + window_identifier: Option, + options: UserInformationOptions, + ) -> Response; +} + +pub struct Account { + receiver: Arc>>, + cnx: zbus::Connection, + imp: Arc, +} + +impl Account { + pub async fn new(imp: T, backend: &Backend) -> zbus::Result { + let (sender, receiver) = futures_channel::mpsc::unbounded(); + let iface = AccountInterface::new(sender, T::VERSION); + backend.serve(iface).await?; + let provider = Self { + receiver: Arc::new(Mutex::new(receiver)), + imp: Arc::new(imp), + cnx: backend.cnx().clone(), + }; + + Ok(provider) + } + + pub async fn try_next(&self) -> Result<(), crate::Error> { + if let Some(action) = (*self.receiver.lock().await).next().await { + self.activate(action).await?; + } + Ok(()) + } + + async fn activate(&self, action: Action) -> Result<(), crate::Error> { + let Action::GetUserInformation(handle_path, app_id, window_identifier, options, sender) = + action; + let request = Request::new(Arc::clone(&self.imp), handle_path, &self.cnx).await?; + let imp = Arc::clone(&self.imp); + let future1 = async { + let result = imp + .get_information(app_id, window_identifier, options) + .await; + let _ = sender.send(result); + Ok(()) as Result<(), crate::Error> + }; + let future2 = async { + request.next().await?; + Ok(()) as Result<(), crate::Error> + }; + + pin_mut!(future1); // 'select' requires Future + Unpin bounds + pin_mut!(future2); + match try_select(future1, future2).await { + Ok(_) => Ok(()), + Err(Either::Left((err, _))) => Err(err), + Err(Either::Right((err, _))) => Err(err), + }?; + Ok(()) + } +} + +enum Action { + GetUserInformation( + OwnedObjectPath, + AppID, + Option, + UserInformationOptions, + oneshot::Sender>, + ), +} + +struct AccountInterface { + sender: Arc>>, + version: NonZeroU32, +} + +impl AccountInterface { + pub fn new(sender: Sender, version: NonZeroU32) -> Self { + Self { + sender: Arc::new(Mutex::new(sender)), + version, + } + } +} + +#[zbus::interface(name = "org.freedesktop.impl.portal.Account")] +impl AccountInterface { + #[zbus(property, name = "version")] + fn version(&self) -> u32 { + self.version.into() + } + + #[zbus(name = "GetUserInformation")] + async fn get_user_information( + &self, + handle: OwnedObjectPath, + app_id: AppID, + window_identifier: &str, + options: UserInformationOptions, + ) -> Response { + let (sender, receiver) = futures_channel::oneshot::channel(); + #[cfg(feature = "tracing")] + tracing::debug!("Account::GetUserInformation"); + + let window_identifier = if window_identifier.is_empty() { + None + } else { + window_identifier.parse::().ok() + }; + + let _ = self + .sender + .lock() + .await + .send(Action::GetUserInformation( + handle, + app_id, + window_identifier, + options, + sender, + )) + .await; + + let response = receiver.await.unwrap(); + #[cfg(feature = "tracing")] + tracing::debug!("Account::GetUserInformation returned {:#?}", response); + response + } +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs new file mode 100644 index 000000000..dd02ba3a5 --- /dev/null +++ b/src/backend/mod.rs @@ -0,0 +1,56 @@ +use zbus::names::WellKnownName; + +pub mod access; +pub mod account; +pub mod request; +pub mod screenshot; +pub mod secret; +pub mod settings; +pub mod wallpaper; + +pub struct Backend { + cnx: zbus::Connection, +} + +impl Backend { + pub async fn new>>(name: N) -> Result + where + zbus::Error: From<>>::Error>, + { + let name = name.try_into().map_err(zbus::Error::from)?; + let cnx = zbus::ConnectionBuilder::session()? + .name(name)? + .build() + .await?; + + Ok(Backend { cnx }) + } + + pub async fn with_connection>>( + name: N, + cnx: zbus::Connection, + ) -> Result + where + zbus::Error: From<>>::Error>, + { + let name = name.try_into().map_err(zbus::Error::from)?; + cnx.request_name(name).await?; + + Ok(Backend { cnx }) + } + + pub(crate) async fn serve(&self, iface: T) -> Result<(), zbus::Error> + where + T: zbus::Interface, + { + let object_server = self.cnx().object_server(); + #[cfg(feature = "tracing")] + tracing::debug!("Serving interface: {}", T::name()); + object_server.at(crate::proxy::DESKTOP_PATH, iface).await?; + Ok(()) + } + + fn cnx(&self) -> &zbus::Connection { + &self.cnx + } +} diff --git a/src/backend/request.rs b/src/backend/request.rs new file mode 100644 index 000000000..2f2bf263e --- /dev/null +++ b/src/backend/request.rs @@ -0,0 +1,93 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use futures_channel::{ + mpsc::{UnboundedReceiver as Receiver, UnboundedSender as Sender}, + oneshot, +}; +use futures_util::{SinkExt, StreamExt}; +use tokio::sync::Mutex; +use zbus::zvariant::OwnedObjectPath; + +#[async_trait] +pub trait RequestImpl { + async fn close(&self); +} + +pub(crate) struct Request { + receiver: Arc>>, + imp: Arc, +} + +impl Request { + pub async fn new( + imp: Arc, + handle_path: OwnedObjectPath, + cnx: &zbus::Connection, + ) -> zbus::Result { + let (sender, receiver) = futures_channel::mpsc::unbounded(); + let iface = RequestInterface::new(sender, handle_path.clone()); + let object_server = cnx.object_server(); + + #[cfg(feature = "tracing")] + tracing::debug!( + "Serving `org.freedesktop.impl.portal.Request` at {:?}", + handle_path.as_str() + ); + object_server.at(handle_path, iface).await?; + let provider = Self { + receiver: Arc::new(Mutex::new(receiver)), + imp, + }; + + Ok(provider) + } + + pub async fn next(&self) -> zbus::fdo::Result<()> { + let action = (*self.receiver.lock().await).next().await; + if let Some(Action::Close(sender)) = action { + self.imp.close().await; + let _ = sender.send(()); + }; + + Ok(()) + } +} + +enum Action { + Close(oneshot::Sender<()>), +} + +struct RequestInterface { + sender: Arc>>, + handle_path: OwnedObjectPath, +} + +impl RequestInterface { + pub fn new(sender: Sender, handle_path: OwnedObjectPath) -> Self { + Self { + sender: Arc::new(Mutex::new(sender)), + handle_path, + } + } +} + +#[zbus::interface(name = "org.freedesktop.impl.portal.Request")] +impl RequestInterface { + async fn close( + &self, + #[zbus(object_server)] server: &zbus::ObjectServer, + ) -> zbus::fdo::Result<()> { + let (sender, receiver) = futures_channel::oneshot::channel(); + let _ = self.sender.lock().await.send(Action::Close(sender)).await; + receiver.await.unwrap(); + + // Drop the request as it served it purpose once closed + #[cfg(feature = "tracing")] + tracing::debug!("Releasing request {:?}", self.handle_path.as_str()); + server + .remove::(&self.handle_path) + .await?; + Ok(()) + } +} diff --git a/src/backend/screenshot.rs b/src/backend/screenshot.rs new file mode 100644 index 000000000..c428e047e --- /dev/null +++ b/src/backend/screenshot.rs @@ -0,0 +1,259 @@ +use std::{num::NonZeroU32, sync::Arc}; + +use async_trait::async_trait; +use futures_channel::{ + mpsc::{UnboundedReceiver as Receiver, UnboundedSender as Sender}, + oneshot, +}; +use futures_util::{ + future::{try_select, Either}, + pin_mut, SinkExt, StreamExt, +}; +use tokio::sync::Mutex; + +use crate::{ + backend::{ + request::{Request, RequestImpl}, + Backend, + }, + desktop::{request::Response, screenshot::Screenshot as ScreenshotResponse, Color}, + zvariant::{DeserializeDict, OwnedObjectPath, Type}, + AppID, WindowIdentifierType, +}; + +#[derive(DeserializeDict, Type, Debug)] +#[zvariant(signature = "dict")] +pub struct ScreenshotOptions { + modal: Option, + interactive: Option, + permission_store_checked: Option, +} + +impl ScreenshotOptions { + pub fn modal(&self) -> Option { + self.modal + } + + pub fn interactive(&self) -> Option { + self.interactive + } + + pub fn permission_store_checked(&self) -> Option { + self.permission_store_checked + } +} + +#[derive(DeserializeDict, Type, Debug)] +#[zvariant(signature = "dict")] +pub struct ColorOptions; + +#[async_trait] +pub trait ScreenshotImpl { + const VERSION: NonZeroU32; + + async fn screenshot( + &self, + app_id: AppID, + window_identifier: Option, + options: ScreenshotOptions, + ) -> Response; + + async fn pick_color( + &self, + app_id: AppID, + window_identifier: Option, + options: ColorOptions, + ) -> Response; +} + +pub struct Screenshot { + receiver: Arc>>, + imp: Arc, + cnx: zbus::Connection, +} + +impl Screenshot { + pub async fn new(imp: T, backend: &Backend) -> zbus::Result { + let (sender, receiver) = futures_channel::mpsc::unbounded(); + let iface = ScreenshotInterface::new(sender, T::VERSION); + backend.serve(iface).await?; + let provider = Self { + receiver: Arc::new(Mutex::new(receiver)), + imp: Arc::new(imp), + cnx: backend.cnx().clone(), + }; + + Ok(provider) + } + + async fn activate(&self, action: Action) -> Result<(), crate::Error> { + match action { + Action::Screenshot(handle_path, app_id, window_identifier, options, sender) => { + let future1 = async { + let result = self + .imp + .screenshot(app_id, window_identifier, options) + .await; + let _ = sender.send(result); + Ok(()) as Result<(), crate::Error> + }; + + let request = Request::new(Arc::clone(&self.imp), handle_path, &self.cnx).await?; + + let future2 = async { + request.next().await?; + Ok(()) as Result<(), crate::Error> + }; + + pin_mut!(future1); // 'select' requires Future + Unpin bounds + pin_mut!(future2); + match try_select(future1, future2).await { + Ok(_) => Ok(()), + Err(Either::Left((err, _))) => Err(err), + Err(Either::Right((err, _))) => Err(err), + }?; + } + Action::PickColor(handle_path, app_id, window_identifier, options, sender) => { + let future1 = async { + let result = self + .imp + .pick_color(app_id, window_identifier, options) + .await; + let _ = sender.send(result); + Ok(()) as Result<(), crate::Error> + }; + + let request = Request::new(Arc::clone(&self.imp), handle_path, &self.cnx).await?; + + let future2 = async { + request.next().await?; + Ok(()) as Result<(), crate::Error> + }; + + pin_mut!(future1); // 'select' requires Future + Unpin bounds + pin_mut!(future2); + match try_select(future1, future2).await { + Ok(_) => Ok(()), + Err(Either::Left((err, _))) => Err(err), + Err(Either::Right((err, _))) => Err(err), + }?; + } + }; + Ok(()) + } + + pub async fn try_next(&self) -> Result<(), crate::Error> { + if let Some(action) = (*self.receiver.lock().await).next().await { + self.activate(action).await?; + } + Ok(()) + } +} + +enum Action { + Screenshot( + OwnedObjectPath, + AppID, + Option, + ScreenshotOptions, + oneshot::Sender>, + ), + PickColor( + OwnedObjectPath, + AppID, + Option, + ColorOptions, + oneshot::Sender>, + ), +} + +struct ScreenshotInterface { + sender: Arc>>, + version: NonZeroU32, +} + +impl ScreenshotInterface { + pub fn new(sender: Sender, version: NonZeroU32) -> Self { + Self { + sender: Arc::new(Mutex::new(sender)), + version, + } + } +} + +#[zbus::interface(name = "org.freedesktop.impl.portal.Screenshot")] +impl ScreenshotInterface { + #[zbus(property, name = "version")] + fn version(&self) -> u32 { + self.version.into() + } + + #[zbus(name = "Screenshot")] + async fn screenshot( + &self, + handle: OwnedObjectPath, + app_id: AppID, + window_identifier: &str, + options: ScreenshotOptions, + ) -> Response { + #[cfg(feature = "tracing")] + tracing::debug!("Screenshot::Screenshot"); + let (sender, receiver) = futures_channel::oneshot::channel(); + let window_identifier = if window_identifier.is_empty() { + None + } else { + window_identifier.parse::().ok() + }; + let _ = self + .sender + .lock() + .await + .send(Action::Screenshot( + handle, + app_id, + window_identifier, + options, + sender, + )) + .await; + let response = receiver.await.unwrap_or(Response::cancelled()); + #[cfg(feature = "tracing")] + tracing::debug!("Screenshot::Screenshot returned {:#?}", response); + response + } + + #[zbus(name = "PickColor")] + async fn pick_color( + &self, + handle: OwnedObjectPath, + app_id: AppID, + window_identifier: &str, + options: ColorOptions, + ) -> Response { + #[cfg(feature = "tracing")] + tracing::debug!("Screenshot::PickColor"); + + let (sender, receiver) = futures_channel::oneshot::channel(); + let window_identifier = if window_identifier.is_empty() { + None + } else { + window_identifier.parse::().ok() + }; + let _ = self + .sender + .lock() + .await + .send(Action::PickColor( + handle, + app_id, + window_identifier, + options, + sender, + )) + .await; + let response = receiver.await.unwrap_or(Response::cancelled()); + #[cfg(feature = "tracing")] + tracing::debug!("Screenshot::PickColor returned {:#?}", response); + response + } +} diff --git a/src/backend/secret.rs b/src/backend/secret.rs new file mode 100644 index 000000000..03fa5e189 --- /dev/null +++ b/src/backend/secret.rs @@ -0,0 +1,140 @@ +use std::{collections::HashMap, num::NonZeroU32, sync::Arc}; + +use async_trait::async_trait; +use futures_channel::{ + mpsc::{UnboundedReceiver as Receiver, UnboundedSender as Sender}, + oneshot, +}; +use futures_util::{ + future::{try_select, Either}, + pin_mut, SinkExt, StreamExt, +}; +use tokio::sync::Mutex; +use zbus::zvariant::{self, OwnedObjectPath, OwnedValue}; + +use crate::{ + backend::{ + request::{Request, RequestImpl}, + Backend, + }, + desktop::Response, + AppID, +}; + +#[async_trait] +pub trait SecretImpl { + const VERSION: NonZeroU32; + + async fn retrieve( + &self, + app_id: AppID, + fd: zvariant::OwnedFd, + ) -> Response>; +} + +enum Action { + RetrieveSecret( + OwnedObjectPath, + AppID, + zvariant::OwnedFd, + HashMap, + oneshot::Sender>>, + ), +} + +pub struct Secret { + receiver: Arc>>, + imp: Arc, + cnx: zbus::Connection, +} + +impl Secret { + pub async fn new(imp: T, backend: &Backend) -> zbus::Result { + let (sender, receiver) = futures_channel::mpsc::unbounded(); + let iface = SecretInterface::new(sender, T::VERSION); + backend.serve(iface).await?; + let provider = Self { + receiver: Arc::new(Mutex::new(receiver)), + imp: Arc::new(imp), + cnx: backend.cnx().clone(), + }; + + Ok(provider) + } + + async fn activate(&self, action: Action) -> Result<(), crate::Error> { + let Action::RetrieveSecret(handle_path, app_id, fd, _options, sender) = action; + let request = Request::new(Arc::clone(&self.imp), handle_path, &self.cnx).await?; + let imp = Arc::clone(&self.imp); + let future1 = async { + let result = imp.retrieve(app_id, fd).await; + let _ = sender.send(result); + Ok(()) as Result<(), crate::Error> + }; + let future2 = async { + request.next().await?; + Ok(()) as Result<(), crate::Error> + }; + + pin_mut!(future1); // 'select' requires Future + Unpin bounds + pin_mut!(future2); + match try_select(future1, future2).await { + Ok(_) => Ok(()), + Err(Either::Left((err, _))) => Err(err), + Err(Either::Right((err, _))) => Err(err), + }?; + Ok(()) + } + + pub async fn try_next(&self) -> Result<(), crate::Error> { + if let Some(action) = (*self.receiver.lock().await).next().await { + self.activate(action).await?; + } + Ok(()) + } +} + +struct SecretInterface { + sender: Arc>>, + version: NonZeroU32, +} + +impl SecretInterface { + pub fn new(sender: Sender, version: NonZeroU32) -> Self { + Self { + sender: Arc::new(Mutex::new(sender)), + version, + } + } +} +#[zbus::interface(name = "org.freedesktop.impl.portal.Secret")] +impl SecretInterface { + #[dbus_interface(property, name = "version")] + fn version(&self) -> u32 { + self.version.into() + } + + #[dbus_interface(out_args("response", "results"))] + async fn retrieve_secret( + &self, + handle: zvariant::OwnedObjectPath, + app_id: AppID, + fd: zvariant::OwnedFd, + options: HashMap, + ) -> Response> { + #[cfg(feature = "tracing")] + tracing::debug!("Secret::RetrieveSecret"); + + let (sender, receiver) = futures_channel::oneshot::channel(); + let _ = self + .sender + .lock() + .await + .send(Action::RetrieveSecret(handle, app_id, fd, options, sender)) + .await; + let response = receiver.await.unwrap_or(Response::cancelled()); + #[cfg(feature = "tracing")] + tracing::debug!("Secret::RetrieveSecret returned {:#?}", response); + response + } +} diff --git a/src/backend/settings.rs b/src/backend/settings.rs new file mode 100644 index 000000000..4d4595c6b --- /dev/null +++ b/src/backend/settings.rs @@ -0,0 +1,126 @@ +use std::{collections::HashMap, num::NonZeroU32, sync::Arc}; + +use async_trait::async_trait; +use futures_channel::{ + mpsc::{Receiver, Sender}, + oneshot, +}; +use futures_util::{SinkExt, StreamExt}; +use tokio::sync::Mutex; + +use crate::{backend::Backend, desktop::settings::Namespace, zvariant::OwnedValue}; + +#[async_trait] +pub trait SettingsImpl { + const VERSION: NonZeroU32; + + async fn read_all(&self, namespaces: Vec) -> HashMap; + + async fn read(&self, namespace: &str, key: &str) -> OwnedValue; +} + +pub struct Settings { + receiver: Arc>>, + imp: T, +} + +impl Settings { + pub async fn new(imp: T, backend: &Backend) -> zbus::Result { + let (sender, receiver) = futures_channel::mpsc::channel(10); + let iface = SettingsInterface::new(sender, T::VERSION); + backend.serve(iface).await?; + let provider = Self { + receiver: Arc::new(Mutex::new(receiver)), + imp, + }; + + Ok(provider) + } + + async fn activate(&self, action: Action) -> Result<(), crate::Error> { + match action { + Action::ReadAll(namespaces, sender) => { + let results = self.imp.read_all(namespaces).await; + let _ = sender.send(results); + } + Action::Read(namespace, key, sender) => { + let results = self.imp.read(&namespace, &key).await; + let _ = sender.send(results); + } + } + + Ok(()) + } + + pub async fn try_next(&self) -> Result<(), crate::Error> { + if let Some(action) = (*self.receiver.lock().await).next().await { + self.activate(action).await?; + } + Ok(()) + } +} + +enum Action { + ReadAll(Vec, oneshot::Sender>), + Read(String, String, oneshot::Sender), +} + +struct SettingsInterface { + sender: Arc>>, + version: NonZeroU32, +} + +impl SettingsInterface { + pub fn new(sender: Sender, version: NonZeroU32) -> Self { + Self { + sender: Arc::new(Mutex::new(sender)), + version, + } + } +} + +#[zbus::interface(name = "org.freedesktop.impl.portal.Settings")] +impl SettingsInterface { + #[zbus(property, name = "version")] + fn version(&self) -> u32 { + self.version.into() + } + + async fn read_all(&self, namespaces: Vec) -> HashMap { + #[cfg(feature = "tracing")] + tracing::debug!("Settings::ReadAll"); + + let (sender, receiver) = futures_channel::oneshot::channel(); + let _ = self + .sender + .lock() + .await + .send(Action::ReadAll(namespaces, sender)) + .await; + + let response = receiver.await.unwrap(); + + #[cfg(feature = "tracing")] + tracing::debug!("Settings::ReadAll returned {:#?}", response); + response + } + + async fn read(&self, namespace: String, key: String) -> OwnedValue { + #[cfg(feature = "tracing")] + tracing::debug!("Settings::Read"); + + let (sender, receiver) = futures_channel::oneshot::channel(); + let _ = self + .sender + .lock() + .await + .send(Action::Read(namespace, key, sender)) + .await; + + let response = receiver.await.unwrap(); + + #[cfg(feature = "tracing")] + tracing::debug!("Settings::Read returned {:#?}", response); + response + } +} diff --git a/src/backend/wallpaper.rs b/src/backend/wallpaper.rs new file mode 100644 index 000000000..0ad3ec661 --- /dev/null +++ b/src/backend/wallpaper.rs @@ -0,0 +1,185 @@ +use std::{num::NonZeroU32, sync::Arc}; + +use async_trait::async_trait; +use futures_channel::{ + mpsc::{UnboundedReceiver as Receiver, UnboundedSender as Sender}, + oneshot, +}; +use futures_util::{ + future::{try_select, Either}, + pin_mut, SinkExt, StreamExt, +}; +use tokio::sync::Mutex; + +use crate::{ + backend::{ + request::{Request, RequestImpl}, + Backend, + }, + desktop::{ + request::{Response, ResponseType}, + wallpaper::SetOn, + }, + zvariant::{DeserializeDict, OwnedObjectPath, Type}, + AppID, WindowIdentifierType, +}; + +#[derive(DeserializeDict, Type, Debug)] +#[zvariant(signature = "dict")] +pub struct WallpaperOptions { + #[zvariant(rename = "show-preview")] + show_preview: Option, + #[zvariant(rename = "set-on")] + set_on: Option, +} + +impl WallpaperOptions { + pub fn show_preview(&self) -> Option { + self.show_preview + } + + pub fn set_on(&self) -> Option { + self.set_on + } +} + +#[async_trait] +pub trait WallpaperImpl { + const VERSION: NonZeroU32; + + async fn with_uri( + &self, + app_id: AppID, + window_identifier: Option, + uri: url::Url, + options: WallpaperOptions, + ) -> Response<()>; +} + +pub struct Wallpaper { + receiver: Arc>>, + imp: Arc, + cnx: zbus::Connection, +} + +impl Wallpaper { + pub async fn new(imp: T, backend: &Backend) -> zbus::Result { + let (sender, receiver) = futures_channel::mpsc::unbounded(); + let iface = WallpaperInterface::new(sender, T::VERSION); + backend.serve(iface).await?; + let provider = Self { + receiver: Arc::new(Mutex::new(receiver)), + imp: Arc::new(imp), + cnx: backend.cnx().clone(), + }; + + Ok(provider) + } + + async fn activate(&self, action: Action) -> Result<(), crate::Error> { + let Action::SetWallpaperURI(handle_path, app_id, window_identifier, uri, options, sender) = + action; + let request = Request::new(Arc::clone(&self.imp), handle_path, &self.cnx).await?; + let imp = Arc::clone(&self.imp); + let future1 = async { + let result = imp.with_uri(app_id, window_identifier, uri, options).await; + let _ = sender.send(result); + Ok(()) as Result<(), crate::Error> + }; + let future2 = async { + request.next().await?; + Ok(()) as Result<(), crate::Error> + }; + + pin_mut!(future1); // 'select' requires Future + Unpin bounds + pin_mut!(future2); + match try_select(future1, future2).await { + Ok(_) => Ok(()), + Err(Either::Left((err, _))) => Err(err), + Err(Either::Right((err, _))) => Err(err), + }?; + Ok(()) + } + + pub async fn try_next(&self) -> Result<(), crate::Error> { + if let Some(action) = (*self.receiver.lock().await).next().await { + self.activate(action).await?; + } + Ok(()) + } +} + +enum Action { + SetWallpaperURI( + OwnedObjectPath, + AppID, + Option, + url::Url, + WallpaperOptions, + oneshot::Sender>, + ), +} + +struct WallpaperInterface { + sender: Arc>>, + version: NonZeroU32, +} + +impl WallpaperInterface { + pub fn new(sender: Sender, version: NonZeroU32) -> Self { + Self { + sender: Arc::new(Mutex::new(sender)), + version, + } + } +} + +#[zbus::interface(name = "org.freedesktop.impl.portal.Wallpaper")] +impl WallpaperInterface { + #[zbus(property, name = "version")] + fn version(&self) -> u32 { + self.version.into() + } + + #[zbus(name = "SetWallpaperURI")] + async fn set_wallpaper_uri( + &self, + handle: OwnedObjectPath, + app_id: AppID, + window_identifier: &str, + uri: url::Url, + options: WallpaperOptions, + ) -> ResponseType { + #[cfg(feature = "tracing")] + tracing::debug!("Wallpaper::SetWallpaperURI"); + + let (sender, receiver) = futures_channel::oneshot::channel(); + let window_identifier = if window_identifier.is_empty() { + None + } else { + window_identifier.parse::().ok() + }; + + let _ = self + .sender + .lock() + .await + .send(Action::SetWallpaperURI( + handle, + app_id, + window_identifier, + uri, + options, + sender, + )) + .await; + let response = receiver + .await + .unwrap_or(Response::cancelled()) + .response_type(); + + #[cfg(feature = "tracing")] + tracing::debug!("Wallpaper::SetWallpaperURI returned {:#?}", response); + response + } +} diff --git a/src/desktop/account.rs b/src/desktop/account.rs index 9a122d478..4dbd31b7a 100644 --- a/src/desktop/account.rs +++ b/src/desktop/account.rs @@ -34,6 +34,22 @@ struct UserInformationOptions { reason: Option, } +#[cfg(feature = "backend")] +mod fdo_account { + #[zbus::proxy( + default_service = "org.freedesktop.Accounts", + interface = "org.freedesktop.Accounts.User", + gen_blocking = false + )] + trait Accounts { + #[zbus(property, name = "IconFile")] + fn icon_file(&self) -> zbus::Result; + #[zbus(property, name = "UserName")] + fn user_name(&self) -> zbus::Result; + #[zbus(property, name = "RealName")] + fn real_name(&self) -> zbus::Result; + } +} #[derive(Debug, DeserializeDict, SerializeDict, Type)] /// The response of a [`UserInformationRequest`] request. #[zvariant(signature = "dict")] @@ -44,6 +60,37 @@ pub struct UserInformation { } impl UserInformation { + #[cfg(feature = "backend")] + /// Retrieve current user information by using the + /// `org.freedesktop.Accounts` interfaces. + pub async fn current_user() -> Result { + let cnx = zbus::Connection::system().await?; + let uid = nix::unistd::Uid::current().as_raw(); + let path = format!("/org/freedesktop/Accounts/User{}", uid); + let proxy = fdo_account::AccountsProxy::builder(&cnx) + .path(path)? + .build() + .await?; + + let uri = format!("file://{}", proxy.icon_file().await?); + + Ok(Self::new( + &proxy.user_name().await?, + &proxy.real_name().await?, + url::Url::parse(&uri)?, + )) + } + + #[cfg(feature = "backend")] + /// Create a new instance of [`UserInformation`]. + pub fn new(id: &str, name: &str, image: url::Url) -> Self { + Self { + id: id.to_owned(), + name: name.to_owned(), + image, + } + } + /// User identifier. pub fn id(&self) -> &str { &self.id diff --git a/src/desktop/color.rs b/src/desktop/color.rs index 259e444ae..f862802ed 100644 --- a/src/desktop/color.rs +++ b/src/desktop/color.rs @@ -1,6 +1,15 @@ -use crate::zvariant::{self, DeserializeDict, Type}; +use crate::zvariant::{self, DeserializeDict, SerializeDict, Type}; -#[derive(DeserializeDict, Clone, Copy, PartialEq, Type, zvariant::Value, zvariant::OwnedValue)] +#[derive( + SerializeDict, + 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. @@ -9,9 +18,18 @@ pub struct Color { color: (f64, f64, f64), } +impl From<(f64, f64, f64)> for Color { + fn from(value: (f64, f64, f64)) -> Self { + Self::new(value.0, value.1, value.2) + } +} + impl Color { - pub(crate) fn new(color: (f64, f64, f64)) -> Self { - Self { color } + /// Create a new instance of Color. + pub fn new(red: f64, green: f64, blue: f64) -> Self { + Self { + color: (red, green, blue), + } } /// Red. diff --git a/src/desktop/mod.rs b/src/desktop/mod.rs index 8696c1b09..2ebee4645 100644 --- a/src/desktop/mod.rs +++ b/src/desktop/mod.rs @@ -3,7 +3,7 @@ pub(crate) mod request; mod session; pub(crate) use self::handle_token::HandleToken; pub use self::{ - request::{Request, Response, ResponseError}, + request::{Request, Response, ResponseError, ResponseType}, session::Session, }; mod color; diff --git a/src/desktop/request.rs b/src/desktop/request.rs index 039e1a0bf..09d746724 100644 --- a/src/desktop/request.rs +++ b/src/desktop/request.rs @@ -37,14 +37,28 @@ impl Response where T: for<'de> Deserialize<'de> + Type, { + /// The corresponding response type. + pub fn response_type(self) -> ResponseType { + match self { + Self::Ok(_) => ResponseType::Success, + Self::Err(err) => match err { + ResponseError::Cancelled => ResponseType::Cancelled, + ResponseError::Other => ResponseType::Other, + }, + } + } + + /// A successful response. pub fn ok(inner: T) -> Self { Self::Ok(inner) } + /// Cancelled request. pub fn cancelled() -> Self { Self::Err(ResponseError::Cancelled) } + /// Another error. pub fn other() -> Self { Self::Err(ResponseError::Other) } @@ -158,8 +172,8 @@ impl std::fmt::Display for ResponseError { } #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Type)] -#[doc(hidden)] -enum ResponseType { +/// Possible responses. +pub enum ResponseType { /// Success, the request is carried out. Success = 0, /// The user cancelled the interaction. diff --git a/src/desktop/screenshot.rs b/src/desktop/screenshot.rs index b00faabb4..7ec80fab4 100644 --- a/src/desktop/screenshot.rs +++ b/src/desktop/screenshot.rs @@ -48,7 +48,7 @@ struct ScreenshotOptions { interactive: Option, } -#[derive(DeserializeDict, Type)] +#[derive(SerializeDict, DeserializeDict, Type)] #[zvariant(signature = "dict")] /// The response of a [`ScreenshotRequest`] request. pub struct Screenshot { @@ -56,6 +56,12 @@ pub struct Screenshot { } impl Screenshot { + #[cfg(feature = "backend")] + /// Create a new instance of the screenshot. + pub fn new(uri: url::Url) -> Self { + Self { uri } + } + /// Creates a new builder-pattern struct instance to construct /// [`Screenshot`]. /// diff --git a/src/desktop/settings.rs b/src/desktop/settings.rs index 3eb216e8b..57d8e5ec6 100644 --- a/src/desktop/settings.rs +++ b/src/desktop/settings.rs @@ -211,7 +211,7 @@ impl<'a> Settings<'a> { pub async fn accent_color(&self) -> Result { self.read::<(f64, f64, f64)>(APPEARANCE_NAMESPACE, ACCENT_COLOR_SCHEME_KEY) .await - .map(Color::new) + .map(Color::from) } /// Retrieves the system's preferred color scheme @@ -244,7 +244,7 @@ impl<'a> Settings<'a> { ACCENT_COLOR_SCHEME_KEY, ) .await? - .filter_map(|t| ready(t.ok().map(Color::new)))) + .filter_map(|t| ready(t.ok().map(Color::from)))) } /// Listen to changes of the system's contrast level diff --git a/src/error.rs b/src/error.rs index aab7c38d5..4ec8941fd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -64,6 +64,9 @@ pub enum Error { /// An error indicating that a Icon::Bytes was expected but wrong type was /// passed UnexpectedIcon, + #[cfg(feature = "backend")] + /// Failed to parse a URL. + Url(url::ParseError), } impl std::error::Error for Error {} @@ -92,6 +95,8 @@ impl std::fmt::Display for Error { f, "Expected icon of type Icon::Bytes but a different type was used." ), + #[cfg(feature = "backend")] + Self::Url(e) => f.write_str(&format!("Parse error: {e}")), } } } @@ -144,3 +149,9 @@ impl From for Error { Self::UnexpectedIcon } } +#[cfg(feature = "backend")] +impl From for Error { + fn from(e: url::ParseError) -> Self { + Self::Url(e) + } +} diff --git a/src/lib.rs b/src/lib.rs index 43b5f3694..537587388 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,12 @@ pub use self::file_path::FilePath; mod proxy; +#[cfg(feature = "backend")] +pub use self::window_identifier::WindowIdentifierType; +#[cfg(feature = "backend")] +#[allow(missing_docs)] +/// Build your custom portals backend. +pub mod backend; /// Spawn commands outside the sandbox or monitor if the running application has /// received an update & install it. pub mod flatpak; diff --git a/src/window_identifier/mod.rs b/src/window_identifier/mod.rs index 62f45efc4..63df17963 100644 --- a/src/window_identifier/mod.rs +++ b/src/window_identifier/mod.rs @@ -263,8 +263,10 @@ impl HasWindowHandle for WindowIdentifier { #[derive(Debug, Clone, PartialEq, Eq, Type)] #[zvariant(signature = "s")] pub enum WindowIdentifierType { + /// X11. X11(std::os::raw::c_ulong), #[allow(dead_code)] + /// Wayland. Wayland(String), }