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)) }