Hung-Yi's LogoHung-Yi’s Journal

Windows Toasts From Emacs Within WSL

With some PowerShell and Emacs Lisp, you can easily pop up a native Windows toast straight from Emacs, even within WSL.

I wanted my Org Mode scheduled reminders and deadlines to interrupt my attention while I’m working on my laptop. There is the wonderful org-alert package, which uses the equally fantastic alert Emacs package, but unfortunately libnotify toasts wouldn’t work for me on Windows Subsystem for Linux (WSL).

Thankfully, WSL has built-in command line capability to reach back into the host Windows system, which lets me run this PowerShell1 script:

param($Title)
$ErrorActionPreference = "Stop"
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
$Template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)

$RawXml = [xml] $Template.GetXml()
($RawXml.toast.visual.binding.text | Where-Object { $_.id -eq "1" }).AppendChild($RawXml.CreateTextNode($Title)) > $null
($RawXml.toast.visual.binding.text | Where-Object { $_.id -eq "2" }).AppendChild($RawXml.CreateTextNode($args[0])) > $null

$SerializedXml = New-Object Windows.Data.Xml.Dom.XmlDocument
$SerializedXml.LoadXml($RawXml.OuterXml)

$Toast = [Windows.UI.Notifications.ToastNotification]::new($SerializedXml)
$Toast.ExpirationTime = [DateTimeOffset]::Now.AddMinutes(30)

$Notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Emacs")
$Notifier.Show($Toast);

…via an Emacs Lisp function (in Emacs running within WSL):

(defun toast (title message)
  (with-temp-buffer
    (shell-command
     (concat
      ;; replace the path to the powershell script to wherever you saved it
      "powershell.exe -ExecutionPolicy Bypass -File ~/path/to/toast.ps1 -Title"
      " \""
      title
      "\" \""
      message
      "\" > /dev/null 2>&1")
     t)))

…which lets me script a toast by calling it like this:

(toast "This Is a Title" "This is some longer text content below the title")

For a basic toast, that’s all you need! You can stop here if all you were interested in was toasting from within WSL.2 Read on for some additional tips in setting up org-alert and alert.

Integration with org-alert and alert

By following alert’s documentation, I learned to create my own simplified alert style which called into my toast function.

(alert-define-style 'wsl :title "WSL Toast"
                    :notifier
                    (lambda (info)
                      (toast (plist-get info :title) (plist-get info :message))))

;; set the default alert style to the one we just created
(setq alert-default-style 'wsl)

You can test that it’s working properly by executing this, which should pop up a toast as if you were calling the toast function manually:

(alert "Text content" :title "Title")

Now org-alert just needs to be configured to your liking, and you’ll get basic toasts in Windows for all your agenda events.

Footnotes:

1

The old, system-provided PowerShell (a.k.a. powershell.exe) is needed here, as the Notification modules don’t seem to be loadable in PowerShell Core (a.k.a. pwsh.exe)

2

Since we’re just calling powershell.exe with shell-command, this would probably work outside WSL, directly in Windows too.