DEV Community

David Rickard
David Rickard

Posted on

Scheduling Windows Notifications from Rust

The windows crate exposes the whole WinRT API surface in Rust and makes it quite easy to send native Windows notifications. One particular handy feature is scheduling notifications for the future, that Windows will deliver even when your app is closed: ScheduledToastNotification::CreateScheduledToastNotification .

One little sticking point is that it requires a Foundation::DateTime to specify the start time, and there don't seem to be any helpers to create one from a chrono or any native Rust date struct. I was able to find in the documentation:

A 64-bit signed integer that represents a point in time as the number of 100-nanosecond intervals prior to or after midnight on January 1, 1601 (according to the Gregorian Calendar).

So the job is to figure out how many 100ns intervals are between your desired time and the WinRT epoch. Then create a DateTime from scratch, passing in this tick count to the UniversalTime property. The full demonstration code is below:

use chrono::{Local, NaiveDateTime, Utc, Duration, TimeZone};
use windows::Foundation::DateTime;
use windows::UI::Notifications::ScheduledToastNotification;
use windows::{Data::Xml::Dom::XmlDocument, UI::Notifications::ToastNotificationManager};

use windows::core::{Result, HSTRING};

fn main() {
    let app_id = "my.app"; // AUMID
    let toast_notifier = ToastNotificationManager::CreateToastNotifierWithId(&HSTRING::from(app_id)).unwrap();

    let toast_xml = create_toast_xml().unwrap();

    let local_time_string = "2023-03-19T16:01:27";
    let winrt_start_time = local_time_to_win_datetime(local_time_string);

    let scheduled_notification = ScheduledToastNotification::CreateScheduledToastNotification(&toast_xml, winrt_start_time).unwrap();
    toast_notifier.AddToSchedule(&scheduled_notification).unwrap();
}

fn create_toast_xml() -> Result<XmlDocument> {
    let toast_xml = XmlDocument::new()?;
    toast_xml.LoadXml(&HSTRING::from("<toast>
        <visual>
            <binding template=\"ToastText01\">
                <text>Testing</text>
            </binding>
        </visual>
    </toast>"))?;

    Ok(toast_xml)
}

fn local_time_to_win_datetime(local_date_time: &str) -> DateTime {
    // First parse the time into a timezone-agnostic local time.
    let naive_time: NaiveDateTime = local_date_time.parse().unwrap();

    // Convert the naive local time to the system time zone, preferring earlier if ambiguous.
    // If there was no local time there, go back an hour and try again.
    let date_time = Local.from_local_datetime(&naive_time)
        .earliest()
        .unwrap_or_else(|| Local.from_local_datetime(&naive_time.checked_sub_signed(Duration::hours(1)).unwrap()).earliest().unwrap());

    let winrt_epoch = Utc.with_ymd_and_hms(1601, 1, 1, 0, 0, 0).unwrap();

    let microsecond_timestamp = date_time.timestamp_micros() - winrt_epoch.timestamp_micros();

    DateTime {
        UniversalTime: microsecond_timestamp * 10
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (3)

Collapse
 
liftoffstudios profile image
Liftoff Studios

Nice Article!
You could just use an inbuilt crate for it (like notify_rust)

Collapse
 
randomengy profile image
David Rickard • Edited

Thanks. But notify-rust uses winrt-notification under the hood, which does not support scheduling notifications in the future.

Collapse
 
liftoffstudios profile image
Liftoff Studios

Ah, okay. I didn't know that :)