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.