extern crate windows;
extern crate xml;
#[macro_use]
extern crate strum;
#[allow(dead_code)]
mod bindings {
::windows::include_bindings!();
}
use bindings::{
windows::data::xml::dom::XmlDocument,
windows::ui::notifications::ToastNotification,
windows::ui::notifications::ToastNotificationManager,
windows::HString,
};
use std::fmt;
use std::path::Path;
use xml::escape::escape_str_attribute;
mod windows_check;
pub use windows::Error;
pub struct Toast {
duration: String,
title: String,
line1: String,
line2: String,
images: String,
audio: String,
app_id: String,
}
pub enum Duration {
Short,
Long,
}
#[derive(Debug, EnumString)]
pub enum Sound {
Default,
IM,
Mail,
Reminder,
SMS,
#[strum(disabled)]
Single(LoopableSound),
#[strum(disabled)]
Loop(LoopableSound),
}
#[derive(Debug)]
#[allow(dead_code)]
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)]
pub enum IconCrop {
Square,
Circular,
}
#[doc(hidden)]
impl fmt::Display for Sound {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
#[doc(hidden)]
impl fmt::Display for LoopableSound {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
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(),
}
}
pub fn title(mut self, content: &str) -> Toast {
self.title = format!(r#"<text id="1">{}</text>"#, escape_str_attribute(content));
self
}
pub fn text1(mut self, content: &str) -> Toast {
self.line1 = format!(r#"<text id="2">{}</text>"#, escape_str_attribute(content));
self
}
pub fn text2(mut self, content: &str) -> Toast {
self.line2 = format!(r#"<text id="3">{}</text>"#, escape_str_attribute(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 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_str_attribute(&source.display().to_string()),
escape_str_attribute(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_str_attribute(&source.display().to_string()),
escape_str_attribute(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_str_attribute(&source.display().to_string()),
escape_str_attribute(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
}
pub fn show(&self) -> windows::Result<()> {
let toast_xml = XmlDocument::new()?;
let template_binding = if windows_check::is_newer_than_windows81() {
"ToastGeneric"
} else
{
if self.images == "" {
"ToastText04"
} else {
"ToastImageAndText04"
}
};
toast_xml.load_xml(HString::from(format!(
"<toast {}>
<visual>
<binding template=\"{}\">
{}
{}{}{}
</binding>
</visual>
{}
</toast>",
self.duration,
template_binding,
self.images,
self.title,
self.line1,
self.line2,
self.audio,
)))?;
let toast_template = ToastNotification::create_toast_notification(toast_xml)?;
let toast_notifier =
ToastNotificationManager::create_toast_notifier_with_id(HString::from(&self.app_id))?;
let result = toast_notifier.show(&toast_template);
std::thread::sleep(std::time::Duration::from_millis(10));
result
}
}
#[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");
}
#[test]
fn text_toast() {
Toast::new(Toast::POWERSHELL_APP_ID)
.title("just text in this toast")
.text1("line1")
.text2("line2")
.duration(Duration::Short)
.sound(Some(Sound::SMS))
.show()
.expect("notification failed");
}
}