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.