1extern crate windows;
31extern crate xml;
32
33#[macro_use]
34extern crate strum;
35
36use windows::{
37 Data::Xml::Dom::XmlDocument,
38 UI::Notifications::ToastNotificationManager,
39};
40
41use std::path::Path;
42
43use xml::escape::escape_str_attribute;
44mod windows_check;
45
46pub use windows::runtime::{Error, HSTRING, Result};
47pub use windows::UI::Notifications::ToastNotification;
48
49pub struct Toast {
50 duration: String,
51 title: String,
52 line1: String,
53 line2: String,
54 images: String,
55 audio: String,
56 app_id: String,
57 scenario: String,
58}
59
60#[derive(Clone, Copy)]
61pub enum Duration {
62 Short,
64
65 Long,
67}
68
69#[derive(Display, Debug, EnumString, Clone, Copy)]
70pub enum Sound {
71 Default,
72 IM,
73 Mail,
74 Reminder,
75 SMS,
76 #[strum(disabled)]
78 Single(LoopableSound),
79 #[strum(disabled)]
81 Loop(LoopableSound),
82}
83
84#[allow(dead_code)]
86#[derive(Display, Debug, Clone, Copy)]
87pub enum LoopableSound {
88 Alarm,
89 Alarm2,
90 Alarm3,
91 Alarm4,
92 Alarm5,
93 Alarm6,
94 Alarm7,
95 Alarm8,
96 Alarm9,
97 Alarm10,
98 Call,
99 Call2,
100 Call3,
101 Call4,
102 Call5,
103 Call6,
104 Call7,
105 Call8,
106 Call9,
107 Call10,
108}
109
110#[allow(dead_code)]
111#[derive(Clone, Copy)]
112pub enum IconCrop {
113 Square,
114 Circular,
115}
116
117#[allow(dead_code)]
118#[derive(Clone, Copy)]
119pub enum Scenario {
120 Default,
122 Alarm,
124 Reminder,
126 IncomingCall,
128}
129
130impl Toast {
131 pub const POWERSHELL_APP_ID: &'static str = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\
135 \\WindowsPowerShell\\v1.0\\powershell.exe";
136 #[allow(dead_code)]
144 pub fn new(app_id: &str) -> Toast {
145 Toast {
146 duration: String::new(),
147 title: String::new(),
148 line1: String::new(),
149 line2: String::new(),
150 images: String::new(),
151 audio: String::new(),
152 app_id: app_id.to_string(),
153 scenario: String::new(),
154 }
155 }
156
157 pub fn title(mut self, content: &str) -> Toast {
162 self.title = format!(r#"<text id="1">{}</text>"#, escape_str_attribute(content));
163 self
164 }
165
166 pub fn text1(mut self, content: &str) -> Toast {
171 self.line1 = format!(r#"<text id="2">{}</text>"#, escape_str_attribute(content));
172 self
173 }
174
175 pub fn text2(mut self, content: &str) -> Toast {
180 self.line2 = format!(r#"<text id="3">{}</text>"#, escape_str_attribute(content));
181 self
182 }
183
184 pub fn duration(mut self, duration: Duration) -> Toast {
186 self.duration = match duration {
187 Duration::Long => "duration=\"long\"",
188 Duration::Short => "duration=\"short\"",
189 }
190 .to_owned();
191 self
192 }
193
194 pub fn scenario(mut self, scenario: Scenario) -> Toast {
199 self.scenario = match scenario {
200 Scenario::Default => "",
201 Scenario::Alarm => "scenario=\"alarm\"",
202 Scenario::Reminder => "scenario=\"reminder\"",
203 Scenario::IncomingCall => "scenario=\"incomingCall\"",
204 }
205 .to_owned();
206 self
207 }
208
209
210 pub fn icon(mut self, source: &Path, crop: IconCrop, alt_text: &str) -> Toast {
215 if windows_check::is_newer_than_windows81() {
216 let crop_type_attr = match crop {
217 IconCrop::Square => "".to_string(),
218 IconCrop::Circular => "hint-crop=\"circle\"".to_string(),
219 };
220
221 self.images = format!(
222 r#"{}<image placement="appLogoOverride" {} src="file:///{}" alt="{}" />"#,
223 self.images,
224 crop_type_attr,
225 escape_str_attribute(&source.display().to_string()),
226 escape_str_attribute(alt_text)
227 );
228 self
229 } else {
230 self.image(source, alt_text)
232 }
233 }
234
235 pub fn hero(mut self, source: &Path, alt_text: &str) -> Toast {
239 if windows_check::is_newer_than_windows81() {
240 self.images = format!(
241 r#"{}<image placement="Hero" src="file:///{}" alt="{}" />"#,
242 self.images,
243 escape_str_attribute(&source.display().to_string()),
244 escape_str_attribute(alt_text)
245 );
246 self
247 } else {
248 self.image(source, alt_text)
250 }
251 }
252
253 pub fn image(mut self, source: &Path, alt_text: &str) -> Toast {
258 if !windows_check::is_newer_than_windows81() {
259 self.images = "".to_owned();
261 }
262 self.images = format!(
263 r#"{}<image id="1" src="file:///{}" alt="{}" />"#,
264 self.images,
265 escape_str_attribute(&source.display().to_string()),
266 escape_str_attribute(alt_text)
267 );
268 self
269 }
270
271 pub fn sound(mut self, src: Option<Sound>) -> Toast {
275 self.audio = match src {
276 None => "<audio silent=\"true\" />".to_owned(),
277 Some(Sound::Default) => "".to_owned(),
278 Some(Sound::Loop(sound)) => format!(r#"<audio loop="true" src="ms-winsoundevent:Notification.Looping.{}" />"#, sound),
279 Some(Sound::Single(sound)) => format!(r#"<audio src="ms-winsoundevent:Notification.Looping.{}" />"#, sound),
280 Some(sound) => format!(r#"<audio src="ms-winsoundevent:Notification.{}" />"#, sound),
281 };
282
283 self
284 }
285
286 fn create_template(&self) -> windows::runtime::Result<ToastNotification> {
287 let toast_xml = XmlDocument::new()?;
289
290 let template_binding = if windows_check::is_newer_than_windows81() {
291 "ToastGeneric"
292 } else
293 {
295 if self.images == "" {
297 "ToastText04"
298 } else {
299 "ToastImageAndText04"
300 }
301 };
302
303 toast_xml.LoadXml(HSTRING::from(format!(
304 "<toast {} {}>
305 <visual>
306 <binding template=\"{}\">
307 {}
308 {}{}{}
309 </binding>
310 </visual>
311 {}
312 </toast>",
313 self.duration,
314 self.scenario,
315 template_binding,
316 self.images,
317 self.title,
318 self.line1,
319 self.line2,
320 self.audio,
321 )))?;
322
323 ToastNotification::CreateToastNotification(toast_xml)
325 }
326
327 pub fn show(&self) -> windows::runtime::Result<()> {
329 let toast_template = self.create_template()?;
330
331 let toast_notifier = ToastNotificationManager::CreateToastNotifierWithId(HSTRING::from(&self.app_id))?;
332
333 let result = toast_notifier.Show(&toast_template);
335 std::thread::sleep(std::time::Duration::from_millis(10));
336 result
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use crate::*;
343 use std::path::Path;
344
345 #[test]
346 fn simple_toast() {
347 let toast = Toast::new(Toast::POWERSHELL_APP_ID);
348 toast
349 .hero(&Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/flower.jpeg"), "flower")
350 .icon(
351 &Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/chick.jpeg"),
352 IconCrop::Circular,
353 "chicken",
354 )
355 .title("title")
356 .text1("line1")
357 .text2("line2")
358 .duration(Duration::Short)
359 .sound(None)
362 .show()
363 .expect("notification failed");
365 }
366}