Skip to main content

spell_framework/
vault.rs

1//! `vault` contains the necessary utilities/APTs for various common tasks required
2//! when creating a custom shell. This includes apps, pipewire, PAM, Mpris etc
3//! among other things.
4//!
5//! <div class="warning">
6//! For now, this module doesn't contain much utilities. As, more common methods
7//! are added, docs will expand to include examples and panic cases.
8//! </div>
9//!
10//! Current it provides three main functionalities, namely notification management
11//! interface via [`NotificationManager`]
12use crate::vault::application::desktop_entry_extracter;
13pub use mpris;
14pub use notification_manager::set_notification;
15pub use rust_fuzzy_search::fuzzy_search_best_n;
16use std::{
17    env,
18    ffi::OsStr,
19    path::{Component, Path, PathBuf},
20    sync::OnceLock,
21};
22use zbus::blocking::{Connection, Proxy};
23
24mod application;
25mod notification_manager;
26
27/// This public static is only set when a notification server instance is passed in
28/// [`cast_spell`](crate::cast_spell).
29/// It is created to maintain the compliance with freedesktop's desktop notification
30/// [specification](https://specifications.freedesktop.org/notification/1.3/index.html).
31/// It's method can be called in specific senarios to notify applications that a notification
32/// with an id has been closed. This static holds an instance of
33/// [`BlockingNotificaiton`](crate::vault::BlockingNotification)
34pub static NOTIFICATION_EVENT: OnceLock<BlockingNotification> = OnceLock::new();
35
36/// Holds blocking methods to notify when a notification has bee closed.
37#[derive(Default)]
38pub struct BlockingNotification;
39
40impl BlockingNotification {
41    /// Method to ask the server to emit a signal for closing a particular notificaiton.
42    pub fn call_close(
43        &self,
44        id: u32,
45        reason: CloseReason,
46    ) -> Result<(), Box<dyn std::error::Error>> {
47        let conn = Connection::session()?;
48        let proxy = Proxy::new(
49            &conn,
50            "org.freedesktop.Notifications",
51            "/org/freedesktop/Notifications",
52            "org.freedesktop.Notifications",
53        )?;
54        proxy.call_noreply("NotificationClosed", &(id, reason as u32))?;
55        Ok(())
56    }
57}
58
59/// This trait's implementation is necessary for passing spell's generated widget
60/// into the notification field of [`cast_spell`](crate::cast_spell) macro. It is important to note that
61/// implementation of this trait is not on the spell generated widget/window but on the
62/// **slint generated window**. For example, a window with name `TopBar` will have a
63/// spell implemetation `TopBarSpell`. This trait will be implemented over `TopBar`.
64pub trait NotificationManager {
65    /// This method is called when a new notification is sent.
66    fn new_notification(&self, notification: Notification) -> Result<(), NotiError>;
67    /// This method is called when CloseNotification Server method is invoked.
68    /// It requres the implementation of the trait to close the notification with
69    /// the provided id if it is open.
70    fn close_notification(&self, id: u32) -> Result<(), NotiError>;
71}
72
73/// Reason to close a notification in [`NOTIFICATION_EVENT`].
74#[derive(Debug)]
75pub enum CloseReason {
76    /// The notification expired
77    Expired = 1,
78    /// The notification was dismissed by the user
79    Dismissed = 2,
80    /// The notification was closed by a call to CloseNotification Server method.
81    ByCall = 3,
82    /// Undefined/reserved reasons
83    Undefined = 4,
84}
85
86/// Error type used by [`NotificationManager`].
87#[derive(Debug)]
88pub enum NotiError {
89    /// Returned when a new notification can't be handled by the custom implementation.
90    MessageUnprocessed,
91    /// Returned when a message close request has been failed and the notification
92    /// is not closed.
93    MessageCloseFailed,
94}
95
96/// Object representing a notification.
97#[derive(Debug, Clone)]
98pub struct Notification {
99    /// id of the notification.
100    pub id: u32,
101    /// Name of app invoking the notification.
102    pub appname: String,
103    /// Summary (generally main title) of the notification.
104    pub summary: String,
105    /// Optionaly sub-title of the notification.
106    pub subtitle: Option<String>,
107    /// Body of the notificaiton.
108    pub body: String,
109    /// Icon path of the notification.
110    pub icon: String,
111    /// Hints of the notification. Refer [here](https://specifications.freedesktop.org/notification/1.3/hints.html)
112    ///  for more details.
113    pub hints: Vec<Hint>,
114    /// Specified actions by the notification. Currently partially implemented.
115    pub actions: Vec<String>,
116    /// Specified timeout in which the notification expects to expire itself.
117    pub timeout: Timeout,
118}
119
120/// Hints provided by a notification. Refer [here](https://specifications.freedesktop.org/notification/1.3/hints.html)
121/// for more details. Currently "image-data" and "image_data" hints are not supported.
122#[derive(Debug, Clone)]
123pub enum Hint {
124    /// When set, a server that has the "action-icons" capability will attempt to
125    /// interpret any action identifier as a named icon. The localized display name
126    ///  will be used to annotate the icon for accessibility purposes. The icon name
127    ///  should be compliant with the Freedesktop.org Icon Naming Specification.
128    ActionIcons(bool),
129    /// The type of notification this is.
130    Category(String),
131    /// This specifies the name of the desktop filename representing the  calling
132    /// program. This should be the same as the prefix used for the application's
133    /// .desktop file. An example would be "rhythmbox" from "rhythmbox.desktop".
134    ///  This can be used by the daemon to retrieve the correct icon for the application,
135    ///  for logging purposes, etc.
136    DesktopEntry(String),
137    /// Alternative way to define the notification image. See [Icons and Images](https://specifications.freedesktop.org/notification/1.3/icons-and-images.html).
138    ImagePath(String),
139    /// When set the server will not automatically remove the notification when
140    ///  an action has been invoked. The notification will remain resident in the
141    ///  server until it is explicitly removed by the user or by the sender. This
142    ///  hint is likely only useful when the server has the "persistence" capability.
143    Resident(bool),
144    /// The path to a sound file to play when the notification pops up.
145    SoundFile(String),
146    /// A themeable named sound from the freedesktop.org [sound naming specification](https://0pointer.de/public/sound-naming-spec.html)
147    /// to play when the notification pops up. Similar to icon-name, only for sounds. An example would be "message-new-instant".
148    SoundName(String),
149    /// Causes the server to suppress playing any sounds, if it has that ability.
150    /// This is usually set when the client itself is going to play its own sound.
151    SuppressSound(bool),
152    /// When set the server will treat the notification as transient and by-pass
153    ///  the server's persistence capability, if it should exist.
154    Transient(bool),
155    /// Specifies the X location on the screen that the notification should point to. The "y" hint must also be specified.
156    X(i32),
157    /// Specifies the Y location on the screen that the notification should point to. The "x" hint must also be specified.
158    Y(i32),
159    /// The urgency level.
160    Urgency(Urgency),
161    // Custom(String, String),
162    // CustomInt(String, i32),
163    /// Invalid hint passed and not processed.
164    Invalid,
165}
166
167/// The proposed urgency level by the notification, implementations of trait [`NotificationManager`]
168/// can mark the accent color of their notifications based on this.
169#[derive(Debug, Clone)]
170pub enum Urgency {
171    /// The urgency of the notification is low. Like completion of some unimportant task
172    /// by some application.
173    Low = 0,
174    /// The urgency of the notification is normal. Used by most notifications.
175    Normal = 1,
176    /// The urgency of the notification is critical. This urgency level is used by
177    /// low battery, shutdown related etc notification types.
178    Critical = 2,
179}
180
181/// Timeout duration for a notification.
182#[derive(Debug, Clone)]
183pub enum Timeout {
184    /// Use server's default duration to close a notification.
185    Default,
186    /// Don't close the notification until closed by the end user.
187    Never,
188    /// Close the notification after specified milliseconds.
189    Milliseconds(i32),
190}
191
192/// AppSelector stores the data for each application with possible actions. Known bugs
193/// include failing to open flatpak apps in certain cases and failing to find icons
194/// of apps in certain cases both of which will be fixed in coming releases.
195#[derive(Debug, Clone)]
196pub struct AppSelector {
197    /// Storing [`AppData`] in a vector.
198    pub app_list: Vec<AppData>,
199}
200
201impl Default for AppSelector {
202    fn default() -> Self {
203        let data_dirs: String =
204            env::var("XDG_DATA_DIRS").expect("XDG_DATA_DIRS couldn't be fetched");
205        let mut app_line_data: Vec<AppData> = Vec::new();
206        let mut data_dirs_vec = data_dirs.split(':').collect::<Vec<_>>();
207        // Adding some other directories.
208        data_dirs_vec.push("/home/ramayen/.local/share/");
209        for dir in data_dirs_vec.iter() {
210            // To check if the directory mentioned in var actually exists.
211            if Path::new(dir).is_dir() {
212                for inner_dir in Path::new(dir)
213                    .read_dir()
214                    .expect("Couldn't read the directory")
215                    .flatten()
216                {
217                    // if let Ok(inner_dir_present) = inner_dir {
218                    if *inner_dir
219                        .path()
220                        .components()
221                        .collect::<Vec<_>>()
222                        .last()
223                        .unwrap()
224                        == Component::Normal(OsStr::new("applications"))
225                    {
226                        let app_dir: PathBuf = inner_dir.path();
227                        for entry_or_dir in
228                            app_dir.read_dir().expect("Couldn't read app dir").flatten()
229                        {
230                            if entry_or_dir.path().is_dir() {
231                                println!("Encountered a directory");
232                            } else if entry_or_dir.path().extension() == Some(OsStr::new("desktop"))
233                            {
234                                let new_data: Vec<Option<AppData>> =
235                                    desktop_entry_extracter(entry_or_dir.path());
236                                let filtered_data: Vec<AppData> = new_data
237                                    .iter()
238                                    .filter_map(|val| val.to_owned())
239                                    .filter(|new| {
240                                        !app_line_data.iter().any(|existing| {
241                                            existing.desktop_file_id == new.desktop_file_id
242                                        })
243                                    })
244                                    .collect();
245                                app_line_data.extend(filtered_data);
246                            } else if entry_or_dir.path().is_symlink() {
247                                println!("GOt the symlink");
248                            } else {
249                                // println!("Found something else");
250                            }
251                        }
252                    }
253                }
254            }
255        }
256
257        AppSelector {
258            app_list: app_line_data,
259        }
260    }
261}
262
263impl AppSelector {
264    /// Returns an iterator over primary enteries of applications.
265    pub fn get_primary(&self) -> impl Iterator<Item = &AppData> {
266        self.app_list.iter().filter(|val| val.is_primary)
267    }
268
269    /// Returns an iterator of all enteries of all applications.
270    pub fn get_all(&self) -> impl Iterator<Item = &AppData> {
271        self.app_list.iter()
272    }
273
274    /// Returns an iterator over the most relevent result of applications' primary enteries
275    /// for a given string query. `size` determines the number of enteries to
276    /// yield.
277    pub fn query_primary(&self, query_val: &str, size: usize) -> Vec<&AppData> {
278        let query_val = query_val.to_lowercase();
279        let query_list = self
280            .app_list
281            .iter()
282            .filter(|val| val.is_primary)
283            .map(|val| val.name.to_lowercase())
284            .collect::<Vec<String>>();
285        let query_list: Vec<&str> = query_list.iter().map(|v| v.as_str()).collect();
286        let best_match_names: Vec<&str> =
287            fuzzy_search_best_n(query_val.as_str(), &query_list, size)
288                .iter()
289                .map(|val| val.0)
290                .collect();
291        best_match_names
292            .iter()
293            .map(|app_name| {
294                self.app_list
295                    .iter()
296                    .find(|val| val.name.to_lowercase().as_str() == *app_name)
297                    .unwrap()
298            })
299            .collect::<Vec<&AppData>>()
300    }
301
302    /// Returns an iterator over the most relevent result of all applications' enteries
303    /// for a given string query. `size` determines the number of enteries to
304    /// yield.
305    pub fn query_all(&self, query_val: &str, size: usize) -> Vec<&AppData> {
306        let query_val = query_val.to_lowercase();
307        let query_list = self
308            .app_list
309            .iter()
310            .map(|val| val.name.to_lowercase())
311            .collect::<Vec<String>>();
312        let query_list: Vec<&str> = query_list.iter().map(|v| v.as_ref()).collect();
313        let best_match_names: Vec<&str> =
314            fuzzy_search_best_n(query_val.as_str(), &query_list, size)
315                .iter()
316                .map(|val| val.0)
317                .collect();
318
319        best_match_names
320            .iter()
321            .map(|app_name| {
322                self.app_list
323                    .iter()
324                    .find(|val| val.name.to_lowercase().as_str() == *app_name)
325                    .unwrap()
326            })
327            .collect::<Vec<&AppData>>()
328    }
329}
330
331// TODO add representation for GenericName and comments for better searching
332/// Stores the relevent data for an application. Used internally by [`AppSelector`].
333#[derive(Debug, Clone)]
334pub struct AppData {
335    /// Unique ID of an application desktop file according to
336    /// [spec](https://specifications.freedesktop.org/desktop-entry-spec/latest/file-naming.html#desktop-file-id).
337    pub desktop_file_id: String,
338    /// Determines if the entry is primary or an action of an application.
339    pub is_primary: bool,
340    /// Image path of the application if could be fetched.
341    pub image_path: Option<String>,
342    /// Name of application
343    pub name: String,
344    /// Execute command which runs in an spaned thread when an application is asked to run.
345    pub exec_comm: Option<String>,
346}
347
348// TODO have to replace fuzzy search with a custom implementation to avoid dependency.
349// There needs to be performance improvements in AppSelector's default implementation
350// TODO add an example section in this module with pseudocode for trait implementations.