From 9dd8b12fafe7f975896a4701f1da9b83c851d1ed Mon Sep 17 00:00:00 2001 From: candifloss Date: Mon, 23 Sep 2024 20:27:43 +0530 Subject: [PATCH 01/10] Changed json and rson format generation --- Cargo.toml | 2 +- src/formats/json.rs | 63 +------------------------------------------- src/formats/rson.rs | 10 ------- src/formats/serde.rs | 43 ++++++++++++++++++++++++++++++ src/main.rs | 5 ++-- src/notification.rs | 3 +++ 6 files changed, 51 insertions(+), 75 deletions(-) create mode 100644 src/formats/serde.rs diff --git a/Cargo.toml b/Cargo.toml index cc90b6d..8da4a6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,6 @@ 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 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..ea14252 100644 --- a/src/formats/rson.rs +++ b/src/formats/rson.rs @@ -3,16 +3,6 @@ use crate::notification::Notification; use rson_rs::ser::to_string as rson_string; 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() } diff --git a/src/formats/serde.rs b/src/formats/serde.rs new file mode 100644 index 0000000..3a2d9ff --- /dev/null +++ b/src/formats/serde.rs @@ -0,0 +1,43 @@ +use serde::{Serialize, Serializer}; +/* +use std::collections::HashMap; +use std::hash::BuildHasher; +use zvariant::OwnedValue; */ + +pub fn serialize_actions( + actions: &[String], // Changed to a slice + 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..29bd456 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ pub mod formats { pub mod json; pub mod plain; pub mod rson; + pub mod serde; } mod notification; use notification::{to_notif, Notification}; @@ -13,7 +14,7 @@ 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 @@ -32,7 +33,7 @@ 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" { + let op_format: &str = if args.len() == 1 || args[1] == "j" { "j" // Default value, json format } else if args[1] == "p" { "p" // Plain format diff --git a/src/notification.rs b/src/notification.rs index 95d6d5f..e734e5e 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, From b572fa1cc2a9e920b8bb4858a490185c7228b54e Mon Sep 17 00:00:00 2001 From: candifloss Date: Tue, 24 Sep 2024 14:01:11 +0530 Subject: [PATCH 02/10] commit --- src/formats/serde.rs | 6 ++---- src/notification.rs | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/formats/serde.rs b/src/formats/serde.rs index 3a2d9ff..da73200 100644 --- a/src/formats/serde.rs +++ b/src/formats/serde.rs @@ -4,10 +4,7 @@ use std::collections::HashMap; use std::hash::BuildHasher; use zvariant::OwnedValue; */ -pub fn serialize_actions( - actions: &[String], // Changed to a slice - serializer: S, -) -> Result +pub fn serialize_actions(actions: &[String], serializer: S) -> Result where S: Serializer, { @@ -22,6 +19,7 @@ where map.serialize(serializer) } + /* pub fn serialize_hints( hints: &HashMap, diff --git a/src/notification.rs b/src/notification.rs index e734e5e..7b632b0 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -50,7 +50,7 @@ impl Notification { } // Key:Val pairs of actions - pub fn actions(&self) -> Vec<(String, String)> { + /*pub fn actions(&self) -> Vec<(String, String)> { let mut actions: Vec<(String, String)> = vec![]; let acts = &self.actions; let act_len = acts.len(); @@ -68,13 +68,13 @@ impl Notification { } actions } - /* + */ 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 { From 8c373462aa4714ea73b7bf7c53d3db6d43ccf42e Mon Sep 17 00:00:00 2001 From: candifloss Date: Wed, 25 Sep 2024 13:18:52 +0530 Subject: [PATCH 03/10] removed comment --- src/notification.rs | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/notification.rs b/src/notification.rs index 7b632b0..2e92fbe 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -50,29 +50,13 @@ 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 - } - */ pub fn actions(&self) -> Vec<(String, String)> { self.actions .chunks(2) - .map(|chunk| (chunk[0].clone(), chunk[1].clone())) + .map(|chunk| + (chunk[0].clone(), + chunk[1].clone()) + ) .collect() } From 05833c5d99e565f4f0d7b34b8f717d146c5550c4 Mon Sep 17 00:00:00 2001 From: candifloss Date: Thu, 26 Sep 2024 13:37:39 +0530 Subject: [PATCH 04/10] options parsing --- Cargo.toml | 3 ++- src/main.rs | 20 +++++++++++++++----- src/notification.rs | 13 +++++-------- src/optparse.rs | 13 +++++++++++++ 4 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 src/optparse.rs diff --git a/Cargo.toml b/Cargo.toml index 8da4a6d..5e6d566 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,5 @@ tokio = { version = "1.40.0", features = ["full"] } futures-util = "0.3.30" 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/src/main.rs b/src/main.rs index 29bd456..ae4a3a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,8 @@ pub mod formats { mod notification; use notification::{to_notif, Notification}; use std::collections::HashMap; -use std::env; +// use std::env; +pub mod optparse; use futures_util::stream::TryStreamExt; use zbus::{message::Body, Connection, Result}; @@ -32,6 +33,7 @@ fn server_properties() -> HashMap { #[tokio::main] async fn main() -> Result<()> { + /* let args: Vec = env::args().collect(); let op_format: &str = if args.len() == 1 || args[1] == "j" { "j" // Default value, json format @@ -41,9 +43,17 @@ async fn main() -> Result<()> { "r" // rson format } else { "j" - }; + }; */ - let verbose: bool = (args.len() > 2) && (args[2] == "v"); + let args: optparse::Cli = argh::from_env(); + let op_format = match args.format { + Some(value) => value.clone(), // Cloning the owned String + None => "j".to_string(), // Using the default value as a String + }; + + let verbose = args.verbose; + + //let verbose: bool = (args.len() > 2) && (args[2] == "v"); let connection = Connection::session().await?; connection @@ -117,12 +127,12 @@ 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 + println!("{}", ¬if.rson()); // Print the rson version } "p" => { println!("{}\n", ¬if.plain()); // Print the plain version diff --git a/src/notification.rs b/src/notification.rs index 2e92fbe..538faa6 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -51,14 +51,11 @@ impl Notification { // Key:Val pairs of actions pub fn actions(&self) -> Vec<(String, String)> { - self.actions - .chunks(2) - .map(|chunk| - (chunk[0].clone(), - chunk[1].clone()) - ) - .collect() - } + 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..f3ed7a1 --- /dev/null +++ b/src/optparse.rs @@ -0,0 +1,13 @@ +use argh::FromArgs; + +#[derive(FromArgs)] +/// Print desktop notifications +pub struct Cli { + /// select output format: j(json), r(rson), p(plain) + #[argh(option, short = 'f')] + pub format: Option, + + /// verbose mode + #[argh(switch, short = 'v')] + pub verbose: bool, +} \ No newline at end of file From 4a708bb6a0283952f6002b793c949f389bdb3165 Mon Sep 17 00:00:00 2001 From: candifloss Date: Thu, 26 Sep 2024 16:16:46 +0530 Subject: [PATCH 05/10] fixed option parsing --- src/main.rs | 33 +++++++++++++-------------------- src/optparse.rs | 4 ++-- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/main.rs b/src/main.rs index ae4a3a5..198b8e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,27 +33,20 @@ fn server_properties() -> HashMap { #[tokio::main] async fn main() -> Result<()> { - /* - let args: Vec = env::args().collect(); - let op_format: &str = if args.len() == 1 || 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" - }; */ - let args: optparse::Cli = argh::from_env(); let op_format = match args.format { - Some(value) => value.clone(), // Cloning the owned String - None => "j".to_string(), // Using the default value as a String + Some(value) => value.clone(), + None => "j".to_string(), }; - - let verbose = args.verbose; - //let verbose: bool = (args.len() > 2) && (args[2] == "v"); + if !["r", "j", "p"].contains(&op_format.as_str()) { + return Err(zbus::Error::from(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Invalid output format", + ))); + } + + let verbose = args.verbose; let connection = Connection::session().await?; connection @@ -85,9 +78,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( @@ -96,6 +86,9 @@ async fn main() -> Result<()> { &"Unknown interface".to_string(), ) .await?; + if verbose { + println!("Unknown interface requested: {interface_name}"); + } } } "GetServerInformation" => { diff --git a/src/optparse.rs b/src/optparse.rs index f3ed7a1..e2a4d67 100644 --- a/src/optparse.rs +++ b/src/optparse.rs @@ -3,11 +3,11 @@ use argh::FromArgs; #[derive(FromArgs)] /// Print desktop notifications pub struct Cli { - /// select output format: j(json), r(rson), p(plain) + /// 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, -} \ No newline at end of file +} From e83a14dac4081dd10284b03dd31b4a80b95fa3fc Mon Sep 17 00:00:00 2001 From: candifloss Date: Thu, 26 Sep 2024 16:26:07 +0530 Subject: [PATCH 06/10] edited README --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6899526..6b3f397 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,15 @@ Inspired by [`tiramisu`](https://github.com/Sweets/tiramisu) ## Usage ```bash -snot [r|j|p] [v] +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 ``` ## Why this project? From 5bcddfbad94aa89c5ea661de50ffea62c78bd6b5 Mon Sep 17 00:00:00 2001 From: candifloss Date: Thu, 26 Sep 2024 16:27:48 +0530 Subject: [PATCH 07/10] edit README --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6b3f397..4f4cf6f 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,6 @@ 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 -## Supported formats - -- Plain text - Print the output text. (✓ Just print it) -- [`json`](https://json.org) - This output can be parsed by other programs -- [`rson`](https://github.com/rson-rs/rson) - A more sensible alternative to json - -## Upcoming feature - -- Better handling of `json` and `rson` data -- Better ways to work with other programs - ## Usage ```bash @@ -33,6 +22,17 @@ Options: --help display usage information ``` +## Supported formats + +- Plain text - Print the output text. (✓ Just print it) +- [`json`](https://json.org) - This output can be parsed by other programs +- [`rson`](https://github.com/rson-rs/rson) - A more sensible alternative to json + +## Upcoming feature + +- Better handling of `json` and `rson` data +- Better ways to work with other programs + ## Why this project? - Something simple to work with [`EWW`](https://github.com/elkowar/eww) widgets From a6b95bad577953aab6ed7fc243caa79ce01174ad Mon Sep 17 00:00:00 2001 From: candifloss Date: Fri, 27 Sep 2024 00:51:37 +0530 Subject: [PATCH 08/10] option fixes --- src/main.rs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index 198b8e0..5a8b329 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,6 @@ pub mod formats { mod notification; use notification::{to_notif, Notification}; use std::collections::HashMap; -// use std::env; pub mod optparse; use futures_util::stream::TryStreamExt; @@ -33,19 +32,27 @@ fn server_properties() -> HashMap { #[tokio::main] async fn main() -> Result<()> { + // Options let args: optparse::Cli = argh::from_env(); + + // Format: r|j|p let op_format = match args.format { - Some(value) => value.clone(), + 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(), }; - if !["r", "j", "p"].contains(&op_format.as_str()) { - return Err(zbus::Error::from(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "Invalid output format", - ))); - } - + // Verbose mode. Off/false by default let verbose = args.verbose; let connection = Connection::session().await?; @@ -131,12 +138,12 @@ async fn main() -> Result<()> { 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 From 4dfa6e4d3da11ce2740745ef86767f59d513b2d9 Mon Sep 17 00:00:00 2001 From: candifloss Date: Fri, 27 Sep 2024 12:16:31 +0530 Subject: [PATCH 09/10] error handling --- src/formats/rson.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/formats/rson.rs b/src/formats/rson.rs index ea14252..4609941 100644 --- a/src/formats/rson.rs +++ b/src/formats/rson.rs @@ -3,7 +3,7 @@ use crate::notification::Notification; use rson_rs::ser::to_string as rson_string; impl Notification { - pub fn rson(&self) -> String { - rson_string(&self).unwrap() + pub fn rson(&self) -> Result { + rson_string(self).map_err(|e| format!("Failed to serialize to RSON: {}", e)) } } From f568ac111db1b1dfe3896a2b8474e0d0edae491e Mon Sep 17 00:00:00 2001 From: candifloss Date: Fri, 27 Sep 2024 15:29:14 +0530 Subject: [PATCH 10/10] error handling --- src/formats/rson.rs | 5 +++-- src/main.rs | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/formats/rson.rs b/src/formats/rson.rs index 4609941..fd4027d 100644 --- a/src/formats/rson.rs +++ b/src/formats/rson.rs @@ -1,9 +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 rson(&self) -> Result { - rson_string(self).map_err(|e| format!("Failed to serialize to RSON: {}", e)) + pub fn rson(&self) -> Result { + rson_string(self).map_err(|e| RsonError::Message(format!("RSON serialization error: {}", e))) } } diff --git a/src/main.rs b/src/main.rs index 5a8b329..bdf9847 100644 --- a/src/main.rs +++ b/src/main.rs @@ -132,7 +132,11 @@ async fn main() -> Result<()> { println!("{}", ¬if.json()); // Print the json version } "r" => { - println!("{}", ¬if.rson()); // Print the rson 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