diff --git a/Cargo.toml b/Cargo.toml index 9f5ad03..fe7ffbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ pickledb = { version = "0.5.1", features = ["bincode"] } # Directories dirs = "6.0.0" +# Notifications +notify-rust = "4.18.0" # WebSockets tokio-tungstenite = { version = "0.30.0", features = ["native-tls"] } futures-util = "0.3.31" diff --git a/src/main.rs b/src/main.rs index 108cc07..bc95419 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,14 +4,18 @@ mod app; mod cache; mod error; mod input; +mod notification; mod ui; use std::{fs, path::PathBuf}; use app::App; +use notify_rust::{CloseReason, NotificationResponse}; use log::debug; use ratatui::crossterm::event::{self, Event}; +use crate::notification::NotifyHandler; + pub const LOG_FILE: &str = "logs"; pub type Result = anyhow::Result; @@ -54,6 +58,32 @@ async fn main() -> anyhow::Result<(), Box> { app.authenticate_ws(&app.api_client.clone_token()).await?; + /* This is an example, for now we have no use for notifications */ + { + let n = NotifyHandler::new().await?; + let icon = n.clone_icon_path(); + + n.send_notification( + "This is a title".to_string(), + "This is a body".to_string(), + icon, + vec![ + ("reply".to_string(), "Reply".to_string()), + ("read".to_string(), "Mark as read".to_string()), + ], + |response: &NotificationResponse| match response { + NotificationResponse::Default => log::info!("body clicked"), + NotificationResponse::Action(key) => log::info!("button {key:?} clicked"), + NotificationResponse::Reply(text) => log::info!("user replied: {text}"), + NotificationResponse::Closed(CloseReason::Dismissed) => { + log::info!("dismissed by the user") + } + NotificationResponse::Closed(reason) => log::info!("closed: {reason:?}"), + }, + ) + .await; + } + loop { terminal.draw(|f| ui::render(f, &app))?; diff --git a/src/notification.rs b/src/notification.rs new file mode 100644 index 0000000..0d1fcd0 --- /dev/null +++ b/src/notification.rs @@ -0,0 +1,79 @@ +use log::{error, warn}; +use notify_rust::{Notification, ResponseHandler, Timeout}; +use std::{fs, path::PathBuf, thread}; + +use crate::Result; + +pub const ICON_FILE: &str = "icon"; // since idk which file extention we'll be using, I'm leaving it without +pub const NOTIFICATION_TIMEOUT: u32 = 10_000; + +fn get_icon_path() -> Option { + let mut icon_path = dirs::data_dir()?; + icon_path.push(env!("CARGO_PKG_NAME")); + + if let Err(e) = fs::create_dir_all(&icon_path) { + error!("Error creating data directory: {e:?}"); + return None; + }; + + icon_path.push(ICON_FILE); + Some(icon_path) +} + +pub struct NotifyHandler { + icon_path: Option, +} + +impl NotifyHandler { + pub async fn new() -> Result { + let icon_path = get_icon_path(); + if icon_path.is_none() { + warn!("Failed to find icon path!"); + } + + Ok(Self { icon_path }) + } + + pub fn clone_icon_path(&self) -> String { + self.icon_path + .clone() + .unwrap_or_default() + .to_string_lossy() + .to_string() + } + + pub async fn send_notification( + &self, + title: String, + body: String, + icon: String, + actions: Vec<(String, String)>, + response: impl ResponseHandler + Send + 'static, + ) { + thread::spawn(move || { + let mut notification = Notification::new() + .appname(env!("CARGO_PKG_NAME")) + .summary(&title) + .body(&body) + .timeout(Timeout::Milliseconds(NOTIFICATION_TIMEOUT)) + .icon(&icon) + .clone(); + + for action in actions { + notification.action(&action.0, &action.1); + } + + let res = notification.show(); + match res { + Ok(handle) => { + if let Err(e) = handle.wait_for_response(response) { + error!("Error fetching notification response: {e:?}"); + }; + } + Err(e) => { + error!("Error sending notification: {e:?}"); + } + } + }); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index db8968f..4904799 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,8 +1,7 @@ mod error; mod input_token; +mod render; mod server_list; mod validating_token; -mod render; - pub use render::render;