winrt_notification/
lib.rs

1//! An incomplete wrapper over the WinRT toast api
2//!
3//! Tested in Windows 10 and 8.1. Untested in Windows 8, might work.
4//!
5//! Todo:
6//!
7//! * Add support for Adaptive Content
8//! * Add support for Actions
9//!
10//! Known Issues:
11//!
12//! * Will not work for Windows 7.
13//!
14//! Limitations:
15//!
16//! * Windows 8.1 only supports a single image, the last image (icon, hero, image) will be the one on the toast
17
18/// for xml schema details check out:
19///
20/// * https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/root-elements
21/// * https://docs.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-toast-xml-schema
22/// * https://docs.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-adaptive-interactive-toasts
23/// * https://msdn.microsoft.com/library/14a07fce-d631-4bad-ab99-305b703713e6#Sending_toast_notifications_from_desktop_apps
24
25/// for Windows 7 and older support look into Shell_NotifyIcon
26/// https://msdn.microsoft.com/en-us/library/windows/desktop/ee330740(v=vs.85).aspx
27/// https://softwareengineering.stackexchange.com/questions/222339/using-the-system-tray-notification-area-app-in-windows-7
28
29/// For actions look at https://docs.microsoft.com/en-us/dotnet/api/microsoft.toolkit.uwp.notifications.toastactionscustom?view=win-comm-toolkit-dotnet-7.0
30extern 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    /// 7 seconds
63    Short,
64
65    /// 25 seconds
66    Long,
67}
68
69#[derive(Display, Debug, EnumString, Clone, Copy)]
70pub enum Sound {
71    Default,
72    IM,
73    Mail,
74    Reminder,
75    SMS,
76    /// Play the loopable sound only once
77    #[strum(disabled)]
78    Single(LoopableSound),
79    /// Loop the loopable sound for the entire duration of the toast
80    #[strum(disabled)]
81    Loop(LoopableSound),
82}
83
84/// Sounds suitable for Looping
85#[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    /// The normal toast behavior.
121    Default,
122    /// This will be displayed pre-expanded and stay on the user's screen till dismissed. Audio will loop by default and will use alarm audio.
123    Alarm,
124    /// This will be displayed pre-expanded and stay on the user's screen till dismissed..
125    Reminder,
126    /// This will be displayed pre-expanded in a special call format and stay on the user's screen till dismissed. Audio will loop by default and will use ringtone audio.
127    IncomingCall,
128}
129
130impl Toast {
131    /// This can be used if you do not have a AppUserModelID.
132    ///
133    /// However, the toast will erroniously report its origin as powershell.
134    pub const POWERSHELL_APP_ID: &'static str = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\
135                                                 \\WindowsPowerShell\\v1.0\\powershell.exe";
136    /// Constructor for the toast builder.
137    ///
138    /// app_id is the running application's [AppUserModelID][1].
139    ///
140    /// [1]: https://msdn.microsoft.com/en-us/library/windows/desktop/dd378459(v=vs.85).aspx
141    ///
142    /// If the program you are using this in was not installed, use Toast::POWERSHELL_APP_ID for now
143    #[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    /// Sets the title of the toast.
158    ///
159    /// Will be white.
160    /// Supports Unicode ✓
161    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    /// Add/Sets the first line of text below title.
167    ///
168    /// Will be grey.
169    /// Supports Unicode ✓
170    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    /// Add/Sets the second line of text below title.
176    ///
177    /// Will be grey.
178    /// Supports Unicode ✓
179    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    /// Set the length of time to show the toast
185    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    /// Set the scenario of the toast
195    ///
196    /// The system keeps the notification on screen until the user acts upon/dismisses it.
197    /// The system also plays the suitable notification sound as well.
198    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    /// Set the icon shown in the upper left of the toast
211    ///
212    /// The default is determined by your app id.
213    /// If you are using the powershell workaround, it will be the powershell icon
214    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            // Win81 rejects the above xml so we fallback to a simpler call
231            self.image(source, alt_text)
232        }
233    }
234
235    /// Add/Set a Hero image for the toast.
236    ///
237    /// This will be above the toast text and the icon.
238    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            // win81 rejects the above xml so we fallback to a simpler call
249            self.image(source, alt_text)
250        }
251    }
252
253    /// Add an image to the toast
254    ///
255    /// May be done many times.
256    /// Will appear below text.
257    pub fn image(mut self, source: &Path, alt_text: &str) -> Toast {
258        if !windows_check::is_newer_than_windows81() {
259            // win81 cannot have more than 1 image and shows nothing if there is more than that
260            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    /// Set the sound for the toast or silence it
272    ///
273    /// Default is [Sound::IM](enum.Sound.html)
274    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        //using this to get an instance of XmlDocument
288        let toast_xml = XmlDocument::new()?;
289
290        let template_binding = if windows_check::is_newer_than_windows81() {
291            "ToastGeneric"
292        } else
293        //win8 or win81
294        {
295            // Need to do this or an empty placeholder will be shown if no image is set
296            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        // Create the toast
324        ToastNotification::CreateToastNotification(toast_xml)
325    }
326
327    /// Display the toast on the screen
328    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        // Show the toast.
334        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(Some(Sound::Loop(LoopableSound::Call)))
360            //.sound(Some(Sound::SMS))
361            .sound(None)
362            .show()
363            // silently consume errors
364            .expect("notification failed");
365    }
366}