diff --git a/Cargo.toml b/Cargo.toml index cc90b6d..5e6d566 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ zbus = "4.4.0" zvariant = "4.2.0" tokio = { version = "1.40.0", features = ["full"] } futures-util = "0.3.30" -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0.128" -rson_rs = "0.2.1" \ No newline at end of file +rson_rs = "0.2.1" +argh = "0.1.12" diff --git a/README.md b/README.md index 6899526..4f4cf6f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,20 @@ Inspired by [`tiramisu`](https://github.com/Sweets/tiramisu) - Do one thing and do it well([DOTADIW](https://en.wikipedia.org/w/index.php?title=Unix_philosophy&useskin=vector#Do_One_Thing_and_Do_It_Well)) & [KISS](https://en.wikipedia.org/wiki/KISS_Principle) principle: no extra complicated features - (Not really a feature) Written in [`rust`](https://www.rust-lang.org/) using the [`zbus`](https://docs.rs/zbus/latest/zbus/) crate +## Usage + +```bash +snot --help +Usage: snot [-f ] [-v] + +Print desktop notifications + +Options: + -f, --format select output format: r(rson), j(json, default), p(plain) + -v, --verbose verbose mode + --help display usage information +``` + ## Supported formats - Plain text - Print the output text. (✓ Just print it) @@ -19,12 +33,6 @@ Inspired by [`tiramisu`](https://github.com/Sweets/tiramisu) - Better handling of `json` and `rson` data - Better ways to work with other programs -## Usage - -```bash -snot [r|j|p] [v] -``` - ## Why this project? - Something simple to work with [`EWW`](https://github.com/elkowar/eww) widgets diff --git a/src/formats/json.rs b/src/formats/json.rs index c662594..839bb0f 100644 --- a/src/formats/json.rs +++ b/src/formats/json.rs @@ -3,68 +3,7 @@ use crate::notification::Notification; use serde_json::{json, Value}; // json!() macro and Value type impl Notification { - // Actions, as json - pub fn actions_json(&self) -> Value { - if self.actions().is_empty() { - json!({}) // Empty JSON object if no actions - } else { - serde_json::Value::Object( - // Wrap the map into a serde_json::Value - self.actions() - .iter() - .map(|(id, label)| { - (id.clone(), json!(label)) // Create key-value pairs: id -> label - }) - .collect::>(), // Collect into a serde_json::Map - ) - } - } - - // Default action, as json string - pub fn default_action_json(&self) -> Value { - match self.default_action() { - None => serde_json::Value::from("None"), - Some(actn) => serde_json::Value::String(actn.0), - } - } - - // Hints, as json - pub fn hints_json(&self) -> Value { - if self.hints().is_empty() { - json!({}) // Empty JSON object if no hints - } else { - serde_json::Value::Object( - self.hints() - .iter() - .map(|(key, value)| { - (key.clone(), json!(value.to_string())) // Convert hint value to string - }) - .collect::>(), // Collect into a serde_json::Map - ) - } - } - - // The notification as a json object pub fn json(&self) -> Value { - // Initialize - let mut notif_json: Value = json!({ - "app_name": &self.app_name(), - "replace_id": &self.replace_id(), - "icon": &self.icon(), - "summary": &self.summary(), - "body": &self.body(), - "hints": &self.hints_json(), - "expiration_timeout": self.expir_timeout(), - "urgency": self.urgency(), - }); - - // Conditionally add the Actions fields - if let Value::Object(ref mut map) = notif_json { - if !self.actions().is_empty() { - map.insert("actions".to_string(), self.actions_json()); - map.insert("default_action".to_string(), self.default_action_json()); - } - } - notif_json + json!(self) } } diff --git a/src/formats/rson.rs b/src/formats/rson.rs index d9420e0..fd4027d 100644 --- a/src/formats/rson.rs +++ b/src/formats/rson.rs @@ -1,19 +1,10 @@ // This module deals with converting the notification object into rson format, which can be used instead of json if preferred use crate::notification::Notification; use rson_rs::ser::to_string as rson_string; +use rson_rs::de::Error as RsonError; impl Notification { - pub fn actions_rson(&self) -> String { - if self.actions().is_empty() { - String::new() - } else { - rson_string(&self.actions()).unwrap() - } - } - pub fn hints_rson(&self) -> String { - rson_string(&self.hints()).unwrap() - } - pub fn rson(&self) -> String { - rson_string(&self).unwrap() + pub fn rson(&self) -> Result { + rson_string(self).map_err(|e| RsonError::Message(format!("RSON serialization error: {}", e))) } } diff --git a/src/formats/serde.rs b/src/formats/serde.rs new file mode 100644 index 0000000..da73200 --- /dev/null +++ b/src/formats/serde.rs @@ -0,0 +1,41 @@ +use serde::{Serialize, Serializer}; +/* +use std::collections::HashMap; +use std::hash::BuildHasher; +use zvariant::OwnedValue; */ + +pub fn serialize_actions(actions: &[String], serializer: S) -> Result +where + S: Serializer, +{ + let mut map = serde_json::Map::new(); + + // Assuming actions are in pairs: [id, label, id, label, ...] + for chunk in actions.chunks(2) { + if let [id, label] = chunk { + map.insert(id.clone(), serde_json::Value::String(label.clone())); + } + } + + map.serialize(serializer) +} + +/* +pub fn serialize_hints( + hints: &HashMap, + serializer: S, +) -> Result +where + S: Serializer, + H: BuildHasher, +{ + let mut map = serde_json::Map::new(); + + for (key, value) in hints { + // Customize OwnedValue serialization as needed + map.insert(key.clone(), serde_json::Value::String(value.to_string())); + } + + map.serialize(serializer) +} +*/ diff --git a/src/main.rs b/src/main.rs index 753bf17..bdf9847 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,18 +2,19 @@ pub mod formats { pub mod json; pub mod plain; pub mod rson; + pub mod serde; } mod notification; use notification::{to_notif, Notification}; use std::collections::HashMap; -use std::env; +pub mod optparse; use futures_util::stream::TryStreamExt; use zbus::{message::Body, Connection, Result}; const SERVER_NAME: &str = "SNot"; // Server software name const VENDOR: &str = "candifloss.cc"; // Server software vendor -const VERSION: &str = "0.1.0"; // Server software version +const VERSION: &str = "0.1.2"; // Server software version const SPEC_VERSION: &str = "0.42"; // DBus specification version const NOTIF_INTERFACE: &str = "org.freedesktop.Notifications"; // DBus interface name @@ -31,18 +32,28 @@ fn server_properties() -> HashMap { #[tokio::main] async fn main() -> Result<()> { - let args: Vec = env::args().collect(); - let op_format: &str = if args.is_empty() || args[1] == "j" { - "j" // Default value, json format - } else if args[1] == "p" { - "p" // Plain format - } else if args[1] == "r" { - "r" // rson format - } else { - "j" + // Options + let args: optparse::Cli = argh::from_env(); + + // Format: r|j|p + let op_format = match args.format { + Some(value) => { + // Reject invalid format option + if ["r", "j", "p"].contains(&value.as_str()) { + value.clone() + } else { + // Exit with error code + return Err(zbus::Error::from(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Invalid output format", + ))); + } + } + None => "j".to_string(), }; - let verbose: bool = (args.len() > 2) && (args[2] == "v"); + // Verbose mode. Off/false by default + let verbose = args.verbose; let connection = Connection::session().await?; connection @@ -74,9 +85,6 @@ async fn main() -> Result<()> { println!("GetAll request received for interface: {interface_name}"); } } else { - if verbose { - println!("Unknown interface requested: {interface_name}"); - } // Reply with an error connection .reply_error( @@ -85,6 +93,9 @@ async fn main() -> Result<()> { &"Unknown interface".to_string(), ) .await?; + if verbose { + println!("Unknown interface requested: {interface_name}"); + } } } "GetServerInformation" => { @@ -116,23 +127,27 @@ async fn main() -> Result<()> { // Convert the msg body to a Notification object let notif: Notification = to_notif(&msg_body)?; // Handle the notif - match op_format { + match op_format.as_str() { "j" => { println!("{}", ¬if.json()); // Print the json version } "r" => { - println!("{}", ¬if.rson()); // Print the plain version + // Print the rson version + match notif.rson() { + Ok(rson_string) => println!("{}", rson_string), + Err(e) => eprintln!("Failed to convert to RSON: {}", e), + } } "p" => { println!("{}\n", ¬if.plain()); // Print the plain version } _ => { - println!("Onkown output format."); + println!("Onkown output format."); // This is probably unreachable } } } "CloseNotification" => { - // Client sent a close signal. Extract notification ID of the notif to be closed from the message body + // Client sent a 'close' signal. Extract notification ID of the notif to be closed from the message body let notification_id: u32 = msg.body().deserialize()?; // This method has only one parameter, the id // Tracking notifications by their IDs, closing them, and other features may be implemented later diff --git a/src/notification.rs b/src/notification.rs index 95d6d5f..538faa6 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -1,3 +1,4 @@ +use crate::formats::serde::serialize_actions; use serde::Serialize; use std::collections::HashMap; use zbus::{message::Body, Result}; @@ -17,8 +18,10 @@ pub struct Notification { // Notification content/body body: String, // Action requests that can be sent back to the client - "Reply," "Mark as Read," "Play/Pause/Next," "Snooze/Dismiss," etc. + #[serde(serialize_with = "serialize_actions")] actions: Vec, // Useful extra data - notif type, urgency, notif sound, icon data, etc. + //#[serde(serialize_with = "serialize_hints")] hints: HashMap, // Seconds till this notif expires. Optional expir_timeout: i32, @@ -48,30 +51,11 @@ impl Notification { // Key:Val pairs of actions pub fn actions(&self) -> Vec<(String, String)> { - let mut actions: Vec<(String, String)> = vec![]; - let acts = &self.actions; - let act_len = acts.len(); - let mut i = 0; - while i < act_len { - // Action ID, used by the sender to id the clicked action - let action_id = &acts[i]; - // Localised human-readable string that describes the action - let action_label = &acts[i + 1]; - // Pair of (id, label) - let action_pair = (action_id.to_owned(), action_label.to_owned()); - // Add it to the Vec - actions.push(action_pair); - i += 2; - } - actions + self.actions + .chunks(2) + .map(|chunk| (chunk[0].clone(), chunk[1].clone())) + .collect() } - /* - pub fn actions(&self) -> Vec<(String, String)> { - self.actions - .chunks(2) - .map(|chunk| (chunk[0].clone(), chunk[1].clone())) - .collect() - } */ // Hints pub fn hints(&self) -> &HashMap { diff --git a/src/optparse.rs b/src/optparse.rs new file mode 100644 index 0000000..e2a4d67 --- /dev/null +++ b/src/optparse.rs @@ -0,0 +1,13 @@ +use argh::FromArgs; + +#[derive(FromArgs)] +/// Print desktop notifications +pub struct Cli { + /// select output format: r(rson), j(json, default), p(plain) + #[argh(option, short = 'f')] + pub format: Option, + + /// verbose mode + #[argh(switch, short = 'v')] + pub verbose: bool, +}