ashpd/desktop/
dynamic_launcher.rs

1//! Install launchers like Web Application from your browser or Steam.
2//!
3//! # Examples
4//!
5//! ```rust,no_run
6//! use std::io::Read;
7//! use ashpd::{
8//!     desktop::{
9//!         dynamic_launcher::{DynamicLauncherProxy, PrepareInstallOptions},
10//!         Icon,
11//!     },
12//!     WindowIdentifier,
13//! };
14//!
15//! async fn run() -> ashpd::Result<()> {
16//!     let proxy = DynamicLauncherProxy::new().await?;
17//!
18//!     let filename = "/home/bilalelmoussaoui/Projects/ashpd/ashpd-demo/data/icons/com.belmoussaoui.ashpd.demo.svg";
19//!     let mut f = std::fs::File::open(&filename).expect("no file found");
20//!     let metadata = std::fs::metadata(&filename).expect("unable to read metadata");
21//!     let mut buffer = vec![0; metadata.len() as usize];
22//!     f.read(&mut buffer).expect("buffer overflow");
23//!
24//!     let icon = Icon::Bytes(buffer);
25//!     let response = proxy
26//!         .prepare_install(
27//!             None,
28//!             "SomeApp",
29//!             icon,
30//!             PrepareInstallOptions::default()
31//!         )
32//!         .await?
33//!         .response()?;
34//!     let token = response.token();
35//!
36//!
37//!     // Name and Icon will be overwritten from what we provided above
38//!     // Exec will be overridden to call `flatpak run our-app` if the application is sandboxed
39//!     let desktop_entry = r#"
40//!         [Desktop Entry]
41//!         Comment=My Web App
42//!         Type=Application
43//!     "#;
44//!     proxy
45//!         .install(&token, "some_file.desktop", desktop_entry)
46//!         .await?;
47//!
48//!     proxy.uninstall("some_file.desktop").await?;
49//!     Ok(())
50//! }
51//! ```
52
53use std::collections::HashMap;
54
55use enumflags2::{bitflags, BitFlags};
56use serde::{Deserialize, Serialize};
57use serde_repr::{Deserialize_repr, Serialize_repr};
58use zbus::zvariant::{self, DeserializeDict, OwnedValue, SerializeDict, Type, Value};
59
60use super::{HandleToken, Icon, Request};
61use crate::{proxy::Proxy, ActivationToken, Error, WindowIdentifier};
62
63#[bitflags]
64#[derive(Default, Serialize_repr, Deserialize_repr, PartialEq, Eq, Debug, Copy, Clone, Type)]
65#[repr(u32)]
66#[doc(alias = "XdpLauncherType")]
67/// The type of the launcher.
68pub enum LauncherType {
69    #[doc(alias = "XDP_LAUNCHER_APPLICATION")]
70    #[default]
71    /// A launcher that represents an application
72    Application,
73    #[doc(alias = "XDP_LAUNCHER_WEBAPP")]
74    /// A launcher that represents a web application
75    WebApplication,
76}
77
78#[cfg_attr(feature = "glib", derive(glib::Enum))]
79#[cfg_attr(feature = "glib", enum_type(name = "AshpdIconType"))]
80#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Type)]
81#[zvariant(signature = "s")]
82#[serde(rename_all = "lowercase")]
83/// The icon format.
84pub enum IconType {
85    /// PNG.
86    Png,
87    /// JPEG.
88    Jpeg,
89    /// SVG.
90    Svg,
91}
92
93#[derive(Debug, Deserialize, Type)]
94#[zvariant(signature = "(vsu)")]
95/// The icon of the launcher.
96pub struct LauncherIcon(zvariant::OwnedValue, IconType, u32);
97
98impl LauncherIcon {
99    /// The actual icon.
100    pub fn icon(&self) -> Icon {
101        Icon::try_from(&self.0).unwrap()
102    }
103
104    /// The icon type.
105    pub fn type_(&self) -> IconType {
106        self.1
107    }
108
109    /// The icon size.
110    pub fn size(&self) -> u32 {
111        self.2
112    }
113}
114
115#[derive(Debug, Default, SerializeDict, Type)]
116#[zvariant(signature = "dict")]
117/// Options to pass to [`DynamicLauncherProxy::prepare_install`]
118pub struct PrepareInstallOptions {
119    handle_token: HandleToken,
120    modal: Option<bool>,
121    launcher_type: LauncherType,
122    target: Option<String>,
123    editable_name: Option<bool>,
124    editable_icon: Option<bool>,
125}
126
127impl PrepareInstallOptions {
128    /// Sets whether the dialog should be a modal.
129    pub fn modal(mut self, modal: impl Into<Option<bool>>) -> Self {
130        self.modal = modal.into();
131        self
132    }
133
134    /// Sets the launcher type.
135    pub fn launcher_type(mut self, launcher_type: LauncherType) -> Self {
136        self.launcher_type = launcher_type;
137        self
138    }
139
140    /// The URL for a [`LauncherType::WebApplication`] otherwise it is not
141    /// needed.
142    pub fn target<'a>(mut self, target: impl Into<Option<&'a str>>) -> Self {
143        self.target = target.into().map(ToOwned::to_owned);
144        self
145    }
146
147    /// Sets whether the name should be editable.
148    pub fn editable_name(mut self, editable_name: impl Into<Option<bool>>) -> Self {
149        self.editable_name = editable_name.into();
150        self
151    }
152
153    /// Sets whether the icon should be editable.
154    pub fn editable_icon(mut self, editable_icon: impl Into<Option<bool>>) -> Self {
155        self.editable_icon = editable_icon.into();
156        self
157    }
158}
159
160#[derive(DeserializeDict, Type)]
161#[zvariant(signature = "dict")]
162/// A response of [`DynamicLauncherProxy::prepare_install`]
163pub struct PrepareInstallResponse {
164    name: String,
165    icon: OwnedValue,
166    token: String,
167}
168
169impl PrepareInstallResponse {
170    /// The user defined name or a predefined one
171    pub fn name(&self) -> &str {
172        &self.name
173    }
174
175    /// A token to pass to [`DynamicLauncherProxy::install`]
176    pub fn token(&self) -> &str {
177        &self.token
178    }
179
180    /// The user selected icon or a predefined one
181    pub fn icon(&self) -> Icon {
182        let inner = self.icon.downcast_ref::<Value>().unwrap();
183        Icon::try_from(inner).unwrap()
184    }
185}
186
187impl std::fmt::Debug for PrepareInstallResponse {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        f.debug_struct("PrepareInstallResponse")
190            .field("name", &self.name())
191            .field("icon", &self.icon())
192            .field("token", &self.token())
193            .finish()
194    }
195}
196
197#[derive(SerializeDict, Type, Debug, Default)]
198#[zvariant(signature = "dict")]
199/// Options to pass to [`DynamicLauncherProxy::launch`]
200pub struct LaunchOptions {
201    activation_token: Option<ActivationToken>,
202}
203
204impl LaunchOptions {
205    /// Sets the token that can be used to activate the chosen application.
206    #[must_use]
207    pub fn activation_token(
208        mut self,
209        activation_token: impl Into<Option<ActivationToken>>,
210    ) -> Self {
211        self.activation_token = activation_token.into();
212        self
213    }
214}
215
216#[derive(Debug)]
217/// Wrong type of [`crate::desktop::Icon`] was used.
218pub struct UnexpectedIconError;
219
220impl std::error::Error for UnexpectedIconError {}
221impl std::fmt::Display for UnexpectedIconError {
222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        f.write_str("Unexpected icon type. Only Icon::Bytes is supported")
224    }
225}
226
227/// The interface lets sandboxed applications install launchers like Web
228/// Application from your browser or Steam.
229///
230/// Wrapper of the DBus interface: [`org.freedesktop.portal.DynamicLauncher`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html).
231#[derive(Debug)]
232#[doc(alias = "org.freedesktop.portal.DynamicLauncher")]
233pub struct DynamicLauncherProxy<'a>(Proxy<'a>);
234
235impl<'a> DynamicLauncherProxy<'a> {
236    /// Create a new instance of [`DynamicLauncherProxy`].
237    pub async fn new() -> Result<DynamicLauncherProxy<'a>, Error> {
238        let proxy = Proxy::new_desktop("org.freedesktop.portal.DynamicLauncher").await?;
239        Ok(Self(proxy))
240    }
241
242    /// *Note* Only `Icon::Bytes` is accepted.
243    ///
244    ///  # Specifications
245    ///
246    /// See also [`PrepareInstall`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html#org-freedesktop-portal-dynamiclauncher-prepareinstall).
247    #[doc(alias = "PrepareInstall")]
248    #[doc(alias = "xdp_portal_dynamic_launcher_prepare_install")]
249    #[doc(alias = "xdp_portal_dynamic_launcher_prepare_install_finish")]
250    pub async fn prepare_install(
251        &self,
252        identifier: Option<&WindowIdentifier>,
253        name: &str,
254        icon: Icon,
255        options: PrepareInstallOptions,
256    ) -> Result<Request<PrepareInstallResponse>, Error> {
257        if !icon.is_bytes() {
258            return Err(UnexpectedIconError {}.into());
259        }
260        let identifier = identifier.map(|i| i.to_string()).unwrap_or_default();
261        self.0
262            .request(
263                &options.handle_token,
264                "PrepareInstall",
265                &(identifier, name, icon.as_value(), &options),
266            )
267            .await
268    }
269
270    /// *Note* Only `Icon::Bytes` is accepted.
271    ///
272    /// # Specifications
273    ///
274    /// See also [`RequestInstallToken`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html#org-freedesktop-portal-dynamiclauncher-requestinstalltoken).
275    #[doc(alias = "RequestInstallToken")]
276    #[doc(alias = "xdp_portal_dynamic_launcher_request_install_token")]
277    pub async fn request_install_token(&self, name: &str, icon: Icon) -> Result<String, Error> {
278        if !icon.is_bytes() {
279            return Err(UnexpectedIconError {}.into());
280        }
281
282        // No supported options for now
283        let options: HashMap<&str, zvariant::Value<'_>> = HashMap::new();
284        self.0
285            .call::<String>("RequestInstallToken", &(name, icon.as_value(), options))
286            .await
287    }
288
289    /// # Specifications
290    ///
291    /// See also [`Install`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html#org-freedesktop-portal-dynamiclauncher-install).
292    #[doc(alias = "Install")]
293    #[doc(alias = "xdp_portal_dynamic_launcher_install")]
294    pub async fn install(
295        &self,
296        token: &str,
297        desktop_file_id: &str,
298        desktop_entry: &str,
299    ) -> Result<(), Error> {
300        // No supported options for now
301        let options: HashMap<&str, zvariant::Value<'_>> = HashMap::new();
302        self.0
303            .call::<()>("Install", &(token, desktop_file_id, desktop_entry, options))
304            .await
305    }
306
307    /// # Specifications
308    ///
309    /// See also [`Uninstall`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html#org-freedesktop-portal-dynamiclauncher-uninstall).
310    #[doc(alias = "Uninstall")]
311    #[doc(alias = "xdp_portal_dynamic_launcher_uninstall")]
312    pub async fn uninstall(&self, desktop_file_id: &str) -> Result<(), Error> {
313        // No supported options for now
314        let options: HashMap<&str, zvariant::Value<'_>> = HashMap::new();
315        self.0
316            .call::<()>("Uninstall", &(desktop_file_id, options))
317            .await
318    }
319
320    /// # Specifications
321    ///
322    /// See also [`GetDesktopEntry`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html#org-freedesktop-portal-dynamiclauncher-getdesktopentry).
323    #[doc(alias = "GetDesktopEntry")]
324    #[doc(alias = "xdp_portal_dynamic_launcher_get_desktop_entry")]
325    pub async fn desktop_entry(&self, desktop_file_id: &str) -> Result<String, Error> {
326        self.0.call("GetDesktopEntry", &(desktop_file_id)).await
327    }
328
329    /// # Specifications
330    ///
331    /// See also [`GetIcon`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html#org-freedesktop-portal-dynamiclauncher-geticon).
332    #[doc(alias = "GetIcon")]
333    #[doc(alias = "xdp_portal_dynamic_launcher_get_icon")]
334    pub async fn icon(&self, desktop_file_id: &str) -> Result<LauncherIcon, Error> {
335        self.0.call("GetIcon", &(desktop_file_id)).await
336    }
337
338    /// # Specifications
339    ///
340    /// See also [`Launch`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html#org-freedesktop-portal-dynamiclauncher-launch).
341    #[doc(alias = "Launch")]
342    #[doc(alias = "xdp_portal_dynamic_launcher_launch")]
343    pub async fn launch(&self, desktop_file_id: &str, options: LaunchOptions) -> Result<(), Error> {
344        self.0.call("Launch", &(desktop_file_id, &options)).await
345    }
346
347    /// # Specifications
348    ///
349    /// See also [`SupportedLauncherTypes`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher.html#org-freedesktop-portal-dynamiclauncher-supportedlaunchertypes).
350    #[doc(alias = "SupportedLauncherTypes")]
351    pub async fn supported_launcher_types(&self) -> Result<BitFlags<LauncherType>, Error> {
352        self.0
353            .property::<BitFlags<LauncherType>>("SupportedLauncherTypes")
354            .await
355    }
356}
357
358impl<'a> std::ops::Deref for DynamicLauncherProxy<'a> {
359    type Target = zbus::Proxy<'a>;
360
361    fn deref(&self) -> &Self::Target {
362        &self.0
363    }
364}
365
366#[cfg(test)]
367mod test {
368    use super::*;
369
370    #[test]
371    fn test_icon_signature() {
372        assert_eq!(LauncherIcon::SIGNATURE, "(vsu)");
373
374        let icon = vec![IconType::Png];
375        assert_eq!(serde_json::to_string(&icon).unwrap(), "[\"png\"]");
376    }
377}