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:
| M | toast.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),