package notify
import (
"context"
"encoding/base64"
"fmt"
"os/exec"
)
// toastAppID is the AppUserModelID the toasts are shown under. Unpackaged
// programs need a registered one; PowerShell's own is present on every
// Windows and lets the toast show reliably (it then appears as coming from
// Windows PowerShell). A talk-specific identity would need a Start Menu
// shortcut; left for later.
const toastAppID = `{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe`
// toastGroup groups all talk toasts so they can be removed by tag.
const toastGroup = "talk"
// announceWindows shows a toast with the text and a Cancel button that opens
// a.CancelURI (protocol activation; Windows then starts `talk uri `,
// which tells the daemon to cancel). The returned close removes the toast by
// its tag when the announcement ends.
func announceWindows(ctx context.Context, a Announcement) (func(), error) {
tag := "talk-" + a.Tag
script := fmt.Sprintf(`$ErrorActionPreference = 'Stop'
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
$dec = { param($b) [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($b)) }
$esc = { param($s) [System.Security.SecurityElement]::Escape($s) }
$body = & $esc (& $dec '%s')
$label = & $esc (& $dec '%s')
$uri = & $esc (& $dec '%s')
$xmlText = "talk$body"
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($xmlText)
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
$toast.Tag = '%s'
$toast.Group = '%s'
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('%s').Show($toast)
`, b64(a.Text), b64(a.CancelLabel), b64(a.CancelURI), tag, toastGroup, toastAppID)
if err := runPowerShell(ctx, script); err != nil {
return func() {}, err
}
return func() {
remove := fmt.Sprintf(`[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.UI.Notifications.ToastNotificationManager]::History.Remove('%s', '%s', '%s')
`, tag, toastGroup, toastAppID)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
_ = runPowerShell(ctx, remove)
}, nil
}
// runPowerShell runs a script via -EncodedCommand.
func runPowerShell(ctx context.Context, script string) error {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
cmd := exec.CommandContext(ctx, "powershell", "-NoProfile", "-NonInteractive", "-EncodedCommand", psEncode(script))
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("notify: powershell failed: %w: %s", err, string(out))
}
return nil
}
func b64(s string) string { return base64.StdEncoding.EncodeToString([]byte(s)) }