diff --git a/.gitignore b/.gitignore index 6985cf1..196e176 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,8 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + + +# Added by cargo + +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..fa6e9b3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "wnotify" +version = "0.1.0" +edition = "2021" +description = "Windows CLI tool for easy, customizable toast notifications." + +[dependencies] +seahorse = "2.2" + +[dependencies.windows] +version = "0.52.0" +features = ["Win32_UI_Shell", "Data_Xml_Dom", "UI_Notifications"] + +[profile.release] +strip = true +opt-level = "z" +lto = true diff --git a/README.md b/README.md index faa7709..bb06bb7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,10 @@ # windows-notify Windows CLI tool for easy, customizable toast notifications. + +## Usage + +```sh +$ wnotify.exe hello world +``` + +![sample](./docs/sample_notify.png) diff --git a/docs/sample_notify.png b/docs/sample_notify.png new file mode 100644 index 0000000..d443ef8 Binary files /dev/null and b/docs/sample_notify.png differ diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..331043e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,50 @@ +mod toast; + +use seahorse::{ActionError, ActionResult, App, Context, Flag, FlagType}; + +use std::env; +use std::process::exit; + +fn main() { + let args = env::args().collect::>(); + + let app = App::new(env!("CARGO_PKG_NAME")) + .version(env!("CARGO_PKG_VERSION")) + .usage(format!("{} []", env!("CARGO_PKG_NAME"))) + .flag( + Flag::new("app-id", FlagType::String) + .description("The App ID to use for the notification") + .alias("a"), + ) + .action_with_result(do_action); + + match app.run_with_result(args) { + Ok(_) => {} + Err(ActionError { message }) => { + eprintln!("Error: {}", message); + exit(1) + } + } +} + +fn do_action(c: &Context) -> ActionResult { + if c.args.is_empty() { + return Err(ActionError { + message: "No arguments provided".to_string(), + }); + } + + let app_id = if let Ok(app_id) = c.string_flag("app-id") { + app_id + } else { + "wnotify".to_string() + }; + + let text1 = &c.args[0]; + let text2 = c.args.get(1).map(|s| s.as_str()); + + let toast = toast::Toast::new(&app_id, text1, text2); + toast.notify(); + + Ok(()) +} diff --git a/src/toast.rs b/src/toast.rs new file mode 100644 index 0000000..6519cf5 --- /dev/null +++ b/src/toast.rs @@ -0,0 +1,53 @@ +use windows::{ + core::HSTRING, + Data::Xml::Dom::XmlDocument, + UI::Notifications::{ToastNotification, ToastNotificationManager}, +}; + +pub struct Toast { + app_id: String, + text1: String, + text2: Option, +} + +impl Toast { + pub fn new(app_id: &str, text1: &str, text2: Option<&str>) -> Toast { + Toast { + app_id: app_id.to_string(), + text1: text1.to_string(), + text2: text2.map(|s| s.to_string()), + } + } + + pub fn notify(&self) { + let toast_xml = format!( + r#" + + + + {} + + + "#, + self.app_id, self.text1 + ); + + let xml = XmlDocument::new().unwrap(); + xml.LoadXml(&HSTRING::from(toast_xml)).unwrap(); + + if let Some(text2) = &self.text2 { + let text = xml.CreateElement(&HSTRING::from("text")).unwrap(); + text.SetInnerText(&HSTRING::from(text2)).unwrap(); + xml.SelectSingleNode(&HSTRING::from("/toast/visual/binding")) + .unwrap() + .AppendChild(&text) + .unwrap(); + } + let template = ToastNotification::CreateToastNotification(&xml).unwrap(); + + let notifier = + ToastNotificationManager::CreateToastNotifierWithId(&HSTRING::from(&self.app_id)) + .unwrap(); + notifier.Show(&template).unwrap(); + } +}