go-toast

Send toast notifications in Windows
Log | Files | Refs | README | LICENSE

commit 08918e35ac6788d52ac4c5c2b0a579ddae837c53
parent 194b232c28cedca42432d94adf098ebcee704729
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Wed, 15 Mar 2023 19:19:15 +0800

toast: add powershell fallback

If the dll is not available, we use a temporary powershell
script to gracefully degrade behaviour. This is hacky and
doesn't support the ActivationCallback.

Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>

Diffstat:
Mtoast.go | 102++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
1 file changed, 78 insertions(+), 24 deletions(-)

diff --git a/toast.go b/toast.go @@ -2,7 +2,12 @@ package toast import ( "bytes" + "errors" "fmt" + "io" + "os" + "os/exec" + "syscall" "toast/internal/bind" "toast/tmpl" ) @@ -129,30 +134,6 @@ type Action struct { Arguments string } -func (n *Notification) applyDefaults() { - if n.ActivationType == "" { - n.ActivationType = Protocol - } - if n.Duration == "" { - n.Duration = Short - } - if n.Audio == "" { - n.Audio = Default - } - if n.IconBackgroundColor == "" { - n.IconBackgroundColor = "FFFFFFFF" - } -} - -func (n *Notification) buildXML() (string, error) { - var out bytes.Buffer - err := tmpl.XMLTemplate.Execute(&out, n) - if err != nil { - return "", err - } - return out.String(), nil -} - // Push the notification to the Windows Runtime via the COM API. // // notification := toast.Notification{ @@ -184,9 +165,82 @@ func (n *Notification) Push() error { }); err != nil { return fmt.Errorf("configuring registry: %w", err) } + if bind.IsDLLAvailable() { + return n.pushCOM(xml) + } + return n.pushPowershell(xml) +} + +func (n *Notification) applyDefaults() { + if n.ActivationType == "" { + n.ActivationType = Protocol + } + if n.Duration == "" { + n.Duration = Short + } + if n.Audio == "" { + n.Audio = Default + } + if n.IconBackgroundColor == "" { + n.IconBackgroundColor = "FFFFFFFF" + } +} + +func (n *Notification) buildXML() (string, error) { + var out bytes.Buffer + err := tmpl.XMLTemplate.Execute(&out, n) + if err != nil { + return "", err + } + return out.String(), nil +} + +func (n *Notification) buildPowerShell(xml string, w io.Writer) error { + type scriptData struct { + AppID string + XML string + } + return tmpl.ScriptTemplate.Execute(w, scriptData{AppID: n.AppID, XML: xml}) +} + +// pushCOM pushes the notification using the COM interface. +func (n *Notification) pushCOM(xml string) error { return bind.GenerateToast(n.AppID, xml) } +// pushPowershell pushes the notification using a temporary powershell script. +// This method does not support the in-process activation callback. +func (n *Notification) pushPowershell(xml string) error { + f, err := os.CreateTemp("", "*.ps1") + if err != nil { + return fmt.Errorf("creating temporary script file: %w", err) + } + + defer func() { err = errors.Join(err, os.Remove(f.Name())) }() + + // This BOM ensures we can support non-ascii characters in the toast content. + bomUtf8 := []byte{0xef, 0xbb, 0xbf} + if _, err := f.Write(bomUtf8); err != nil { + return fmt.Errorf("writing utf8 byte marker: %w", err) + } + + if err := n.buildPowerShell(xml, f); err != nil { + return fmt.Errorf("generating powershell script: %w", err) + } + + if err := f.Close(); err != nil { + return fmt.Errorf("closing script file: %w", err) + } + + cmd := exec.Command("PowerShell", "-ExecutionPolicy", "Bypass", "-File", f.Name()) + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("executing powershell: %q: %w", string(out), err) + } + + return nil +} + // SetActivationCallback sets the global activation callback. // // The first argument contains application defined data (embedded within the xml),