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