use strum::{Display, EnumString};
use windows::{Data::Xml::Dom::XmlDocument, UI::Notifications::ToastNotificationManager};
use std::path::Path;
mod windows_check;
pub use windows::core::{Error, Result, HSTRING};
pub use windows::UI::Notifications::ToastNotification;
pub struct Toast {
duration: String,
title: String,
line1: String,
line2: String,
images: String,
audio: String,
app_id: String,
scenario: String,
}
#[derive(Clone, Copy)]
pub enum Duration {
Short,
Long,
}
#[derive(Display, Debug, EnumString, Clone, Copy)]
pub enum Sound {
Default,
IM,
Mail,
Reminder,
SMS,
#[strum(disabled)]
Single(LoopableSound),
#[strum(disabled)]
Loop(LoopableSound),
}
#[allow(dead_code)]
#[derive(Display, Debug, Clone, Copy)]
pub enum LoopableSound {
Alarm,
Alarm2,
Alarm3,
Alarm4,
Alarm5,
Alarm6,
Alarm7,
Alarm8,
Alarm9,
Alarm10,
Call,
Call2,
Call3,
Call4,
Call5,
Call6,
Call7,
Call8,
Call9,
Call10,
}
#[allow(dead_code)]
#[derive(Clone, Copy)]
pub enum IconCrop {
Square,
Circular,
}
#[allow(dead_code)]
#[derive(Clone, Copy)]
pub enum Scenario {
Default,
Alarm,
Reminder,
IncomingCall,
}
impl Toast {
pub const POWERSHELL_APP_ID: &'static str = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\
\\WindowsPowerShell\\v1.0\\powershell.exe";
#[allow(dead_code)]
pub fn new(app_id: &str) -> Toast {
Toast {
duration: String::new(),
title: String::new(),
line1: String::new(),
line2: String::new(),
images: String::new(),
audio: String::new(),
app_id: app_id.to_string(),
scenario: String::new(),
}
}
pub fn title(mut self, content: &str) -> Toast {
self.title = format!(r#"<text id="1">{}</text>"#, &escape(content));
self
}
pub fn text1(mut self, content: &str) -> Toast {
self.line1 = format!(r#"<text id="2">{}</text>"#, &escape(content));
self
}
pub fn text2(mut self, content: &str) -> Toast {
self.line2 = format!(r#"<text id="3">{}</text>"#, &escape(content));
self
}
pub fn duration(mut self, duration: Duration) -> Toast {
self.duration = match duration {
Duration::Long => "duration=\"long\"",
Duration::Short => "duration=\"short\"",
}
.to_owned();
self
}
pub fn scenario(mut self, scenario: Scenario) -> Toast {
self.scenario = match scenario {
Scenario::Default => "",
Scenario::Alarm => "scenario=\"alarm\"",
Scenario::Reminder => "scenario=\"reminder\"",
Scenario::IncomingCall => "scenario=\"incomingCall\"",
}
.to_owned();
self
}
pub fn icon(mut self, source: &Path, crop: IconCrop, alt_text: &str) -> Toast {
if windows_check::is_newer_than_windows81() {
let crop_type_attr = match crop {
IconCrop::Square => "".to_string(),
IconCrop::Circular => "hint-crop=\"circle\"".to_string(),
};
self.images = format!(
r#"{}<image placement="appLogoOverride" {} src="file:///{}" alt="{}" />"#,
self.images,
crop_type_attr,
escape(&source.display().to_string()),
escape(alt_text)
);
self
} else {
self.image(source, alt_text)
}
}
pub fn hero(mut self, source: &Path, alt_text: &str) -> Toast {
if windows_check::is_newer_than_windows81() {
self.images = format!(
r#"{}<image placement="Hero" src="file:///{}" alt="{}" />"#,
self.images,
escape(&source.display().to_string()),
escape(alt_text)
);
self
} else {
self.image(source, alt_text)
}
}
pub fn image(mut self, source: &Path, alt_text: &str) -> Toast {
if !windows_check::is_newer_than_windows81() {
self.images = "".to_owned();
}
self.images = format!(
r#"{}<image id="1" src="file:///{}" alt="{}" />"#,
self.images,
escape(&source.display().to_string()),
escape(alt_text)
);
self
}
pub fn sound(mut self, src: Option<Sound>) -> Toast {
self.audio = match src {
None => "<audio silent=\"true\" />".to_owned(),
Some(Sound::Default) => "".to_owned(),
Some(Sound::Loop(sound)) => format!(
r#"<audio loop="true" src="ms-winsoundevent:Notification.Looping.{}" />"#,
sound
),
Some(Sound::Single(sound)) => format!(
r#"<audio src="ms-winsoundevent:Notification.Looping.{}" />"#,
sound
),
Some(sound) => format!(r#"<audio src="ms-winsoundevent:Notification.{}" />"#, sound),
};
self
}
fn create_template(&self) -> windows::core::Result<ToastNotification> {
let toast_xml = XmlDocument::new()?;
let template_binding = if windows_check::is_newer_than_windows81() {
"ToastGeneric"
} else {
if self.images.is_empty() {
"ToastText04"
} else {
"ToastImageAndText04"
}
};
toast_xml.LoadXml(&HSTRING::from(format!(
"<toast {} {}>
<visual>
<binding template=\"{}\">
{}
{}{}{}
</binding>
</visual>
{}
</toast>",
self.duration,
self.scenario,
template_binding,
self.images,
self.title,
self.line1,
self.line2,
self.audio,
)))?;
ToastNotification::CreateToastNotification(&toast_xml)
}
pub fn show(&self) -> windows::core::Result<()> {
let toast_template = self.create_template()?;
let toast_notifier =
ToastNotificationManager::CreateToastNotifierWithId(&HSTRING::from(&self.app_id))?;
let result = toast_notifier.Show(&toast_template);
std::thread::sleep(std::time::Duration::from_millis(10));
result
}
}
fn escape(string: &str) -> String {
let escaped = quick_xml::escape::escape(string.as_bytes()).to_vec();
String::from_utf8(escaped).unwrap()
}
#[cfg(test)]
mod tests {
use crate::*;
use std::path::Path;
#[test]
fn simple_toast() {
let toast = Toast::new(Toast::POWERSHELL_APP_ID);
toast
.hero(
&Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/flower.jpeg"),
"flower",
)
.icon(
&Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/chick.jpeg"),
IconCrop::Circular,
"chicken",
)
.title("title")
.text1("line1")
.text2("line2")
.duration(Duration::Short)
.sound(None)
.show()
.expect("notification failed");
}
}