Skip to main content

cosmic/
dbus_activation.rs

1// Copyright 2024 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4use crate::ApplicationExt;
5use iced::Subscription;
6use iced_futures::futures::SinkExt;
7use iced_futures::futures::channel::mpsc::{Receiver, Sender};
8use std::any::TypeId;
9use std::collections::HashMap;
10use url::Url;
11use zbus::zvariant::Value;
12use zbus::{interface, proxy};
13
14#[cold]
15pub fn subscription<App: ApplicationExt>() -> Subscription<crate::Action<App::Message>> {
16    use iced_futures::futures::StreamExt;
17    iced_futures::Subscription::run_with(TypeId::of::<DbusActivation>(), |_| {
18        iced::stream::channel(
19            10,
20            move |mut output: Sender<crate::Action<App::Message>>| async move {
21                let mut single_instance: DbusActivation = DbusActivation::new();
22                let mut rx = single_instance.rx();
23                if let Ok(builder) = zbus::connection::Builder::session() {
24                    let path: String = format!("/{}", App::APP_ID.replace('.', "/"));
25                    if let Ok(conn) = builder.build().await {
26                        // XXX Setup done this way seems to be more reliable.
27                        //
28                        // the docs for serve_at seem to imply it will replace the
29                        // existing interface at the requested path, but it doesn't
30                        // seem to work that way all the time. The docs for
31                        // object_server().at() imply it won't replace the existing
32                        // interface.
33                        //
34                        // request_name is used either way, with the builder or
35                        // with the connection, but it must be done after the
36                        // object server is setup.
37                        if conn.object_server().at(path, single_instance).await != Ok(true) {
38                            tracing::error!("Failed to serve dbus");
39                            std::process::exit(1);
40                        }
41                        if conn.request_name(App::APP_ID).await.is_err() {
42                            tracing::error!("Failed to serve dbus");
43                            std::process::exit(1);
44                        }
45
46                        output
47                            .send(crate::Action::Cosmic(crate::app::Action::DbusConnection(
48                                conn.clone(),
49                            )))
50                            .await;
51
52                        #[cfg(feature = "smol")]
53                        let handle = {
54                            std::thread::spawn(move || {
55                                let conn_clone = _conn.clone();
56
57                                zbus::block_on(async move {
58                                    loop {
59                                        conn_clone.executor().tick().await;
60                                    }
61                                })
62                            })
63                        };
64                        while let Some(mut msg) = rx.next().await {
65                            if let Some(token) = msg.activation_token.take() {
66                                if let Err(err) = output
67                                    .send(crate::Action::Cosmic(crate::app::Action::Activate(
68                                        token,
69                                    )))
70                                    .await
71                                {
72                                    tracing::error!(?err, "Failed to send message");
73                                }
74                            }
75                            if let Err(err) = output.send(crate::Action::DbusActivation(msg)).await
76                            {
77                                tracing::error!(?err, "Failed to send message");
78                            }
79                        }
80                    }
81                } else {
82                    tracing::warn!("Failed to connect to dbus for single instance");
83                }
84
85                loop {
86                    iced::futures::pending!();
87                }
88            },
89        )
90    })
91}
92
93#[derive(Debug, Clone)]
94pub struct Message<Action = String, Args = Vec<String>> {
95    pub activation_token: Option<String>,
96    pub desktop_startup_id: Option<String>,
97    pub msg: Details<Action, Args>,
98}
99
100#[derive(Debug, Clone)]
101pub enum Details<Action = String, Args = Vec<String>> {
102    Activate,
103    Open {
104        url: Vec<Url>,
105    },
106    /// action can be deserialized as Flags
107    ActivateAction {
108        action: Action,
109        args: Args,
110    },
111}
112
113#[derive(Debug, Default)]
114pub struct DbusActivation(Option<Sender<Message>>);
115
116impl DbusActivation {
117    #[must_use]
118    #[inline]
119    pub fn new() -> Self {
120        Self(None)
121    }
122
123    #[inline]
124    pub fn rx(&mut self) -> Receiver<Message> {
125        let (tx, rx) = iced_futures::futures::channel::mpsc::channel(10);
126        self.0 = Some(tx);
127        rx
128    }
129}
130
131#[proxy(interface = "org.freedesktop.DbusActivation", assume_defaults = true)]
132pub trait DbusActivationInterface {
133    /// Activate the application.
134    fn activate(&mut self, platform_data: HashMap<&str, Value<'_>>) -> zbus::Result<()>;
135
136    /// Open the given URIs.
137    fn open(
138        &mut self,
139        uris: Vec<&str>,
140        platform_data: HashMap<&str, Value<'_>>,
141    ) -> zbus::Result<()>;
142
143    /// Activate the given action.
144    fn activate_action(
145        &mut self,
146        action_name: &str,
147        parameter: Vec<&str>,
148        platform_data: HashMap<&str, Value<'_>>,
149    ) -> zbus::Result<()>;
150}
151
152#[interface(name = "org.freedesktop.DbusActivation")]
153impl DbusActivation {
154    #[cold]
155    async fn activate(&mut self, platform_data: HashMap<&str, Value<'_>>) {
156        if let Some(tx) = &mut self.0 {
157            let _ = tx
158                .send(Message {
159                    activation_token: platform_data.get("activation-token").and_then(|t| match t {
160                        Value::Str(t) => Some(t.to_string()),
161                        _ => None,
162                    }),
163                    desktop_startup_id: platform_data.get("desktop-startup-id").and_then(
164                        |t| match t {
165                            Value::Str(t) => Some(t.to_string()),
166                            _ => None,
167                        },
168                    ),
169                    msg: Details::Activate,
170                })
171                .await;
172        }
173    }
174
175    #[cold]
176    async fn open(&mut self, uris: Vec<&str>, platform_data: HashMap<&str, Value<'_>>) {
177        if let Some(tx) = &mut self.0 {
178            let _ = tx
179                .send(Message {
180                    activation_token: platform_data.get("activation-token").and_then(|t| match t {
181                        Value::Str(t) => Some(t.to_string()),
182                        _ => None,
183                    }),
184                    desktop_startup_id: platform_data.get("desktop-startup-id").and_then(
185                        |t| match t {
186                            Value::Str(t) => Some(t.to_string()),
187                            _ => None,
188                        },
189                    ),
190                    msg: Details::Open {
191                        url: uris.iter().filter_map(|u| Url::parse(u).ok()).collect(),
192                    },
193                })
194                .await;
195        }
196    }
197
198    #[cold]
199    async fn activate_action(
200        &mut self,
201        action_name: &str,
202        parameter: Vec<&str>,
203        platform_data: HashMap<&str, Value<'_>>,
204    ) {
205        if let Some(tx) = &mut self.0 {
206            let _ = tx
207                .send(Message {
208                    activation_token: platform_data.get("activation-token").and_then(|t| match t {
209                        Value::Str(t) => Some(t.to_string()),
210                        _ => None,
211                    }),
212                    desktop_startup_id: platform_data.get("desktop-startup-id").and_then(
213                        |t| match t {
214                            Value::Str(t) => Some(t.to_string()),
215                            _ => None,
216                        },
217                    ),
218                    msg: Details::ActivateAction {
219                        action: action_name.to_string(),
220                        args: parameter
221                            .iter()
222                            .map(std::string::ToString::to_string)
223                            .collect(),
224                    },
225                })
226                .await;
227        }
228    }
229}