Switch remote deploy to vendored source builds
Move remote deployment to a vendored source bundle built on the target host via Docker so redeploys no longer require local cross-compilation or host Go installation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
89
vendor/git.sr.ht/~jackmordaunt/go-toast/v2/wintoast/bind.go
vendored
Normal file
89
vendor/git.sr.ht/~jackmordaunt/go-toast/v2/wintoast/bind.go
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
// Package wintoast provides a pure-Go implementation of toast notifications on Windows.
|
||||
package wintoast
|
||||
|
||||
import "errors"
|
||||
|
||||
// AppData describes the application to the Windows Runtime.
|
||||
// See toast.Notification for more thorough documentation off these fields.
|
||||
type AppData struct {
|
||||
AppID string
|
||||
GUID string
|
||||
ActivationExe string // optional
|
||||
IconPath string // optional
|
||||
IconBackgroundColor string // optional
|
||||
}
|
||||
|
||||
// UserData contains Key:Value pairs generated within the notification, based
|
||||
// on the XML content of the notification. Specifically, all inputs within
|
||||
// the XML will generate a corresponding UserData struct.
|
||||
type UserData struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
// Callback is a function that gets invoked when the notification is activated.
|
||||
type Callback func(appUserModelId string, invokedArgs string, userData []UserData)
|
||||
|
||||
// SetAppData teaches the Windows Runtime about our application and establishes the activation GUID
|
||||
// so Windows will know how to invoke us back.
|
||||
func SetAppData(data AppData) (err error) {
|
||||
return setAppData(data)
|
||||
}
|
||||
|
||||
// SetActivationCallback establishes the callback `cb` to be invoked when
|
||||
// the toast notification is activated. This callback instance should handle
|
||||
// being activated from any available toast notification.
|
||||
func SetActivationCallback(cb Callback) {
|
||||
callback = cb
|
||||
}
|
||||
|
||||
// Push a notification described by the XML to the Windows Runtime.
|
||||
//
|
||||
// App data should be set first via a call to SetAppData before calling
|
||||
// this function.
|
||||
//
|
||||
// If the powershell fallback is engaged, activation callbacks will not
|
||||
// work as expected and the COM error will still be returned.
|
||||
func Push(appID, xml string, op ...option) error {
|
||||
var opts options
|
||||
for _, opt := range op {
|
||||
opt(&opts)
|
||||
}
|
||||
if opts.PowershellPreferred {
|
||||
return pushPowershell(xml)
|
||||
}
|
||||
if appID == "" {
|
||||
appID = appData.AppID
|
||||
}
|
||||
if err := pushCOM(appID, xml); err != nil {
|
||||
if opts.PowershellFallback {
|
||||
return errors.Join(err, pushPowershell(xml))
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type options struct {
|
||||
PowershellFallback bool
|
||||
PowershellPreferred bool
|
||||
}
|
||||
|
||||
type option func(*options)
|
||||
|
||||
// PreferPowershell indicates to use the powershell method by default.
|
||||
// COM will not be used.
|
||||
func PreferPowershell(opt *options) {
|
||||
opt.PowershellPreferred = true
|
||||
}
|
||||
|
||||
// PowershellFallback specifies to use the powershell method as a fallback
|
||||
// if the COM api fails.
|
||||
func PowershellFallback(opt *options) {
|
||||
opt.PowershellFallback = true
|
||||
}
|
||||
|
||||
// callback is the global callback reference that is invoked by Activate.
|
||||
//
|
||||
// NOTE(jfm): synchronize access to this?
|
||||
var callback Callback = func(model, args string, data []UserData) {}
|
||||
21
vendor/git.sr.ht/~jackmordaunt/go-toast/v2/wintoast/bind_noop.go
vendored
Normal file
21
vendor/git.sr.ht/~jackmordaunt/go-toast/v2/wintoast/bind_noop.go
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
//go:build !windows
|
||||
|
||||
package wintoast
|
||||
|
||||
var appData AppData
|
||||
|
||||
func setAppData(data AppData) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateToast(appID string, xml string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func pushPowershell(xml string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func pushCOM(appID, xml string) error {
|
||||
return nil
|
||||
}
|
||||
211
vendor/git.sr.ht/~jackmordaunt/go-toast/v2/wintoast/bind_windows.go
vendored
Normal file
211
vendor/git.sr.ht/~jackmordaunt/go-toast/v2/wintoast/bind_windows.go
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
//go:build windows
|
||||
|
||||
package wintoast
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"git.sr.ht/~jackmordaunt/go-toast/v2/internal/winrt/data/xml/dom"
|
||||
"git.sr.ht/~jackmordaunt/go-toast/v2/internal/winrt/ui/notifications"
|
||||
"git.sr.ht/~jackmordaunt/go-toast/v2/tmpl"
|
||||
"github.com/go-ole/go-ole"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func 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 := 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
|
||||
}
|
||||
|
||||
func buildPowershell(xml string, w io.Writer) error {
|
||||
type scriptData struct {
|
||||
AppID string
|
||||
XML string
|
||||
}
|
||||
return tmpl.ScriptTemplate.Execute(w, scriptData{AppID: appData.AppID, XML: xml})
|
||||
}
|
||||
|
||||
// HRESULT E_NOINTERFACE
|
||||
const errNoInterface = 0x80004002
|
||||
|
||||
var comDisabled atomic.Bool
|
||||
|
||||
func pushCOM(appID, xml string) (err error) {
|
||||
if comDisabled.Load() {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// On Windows 7 WinRT interfaces can be stubbed out, and fail to produce
|
||||
// error values. This leads to a panic when trying to use the interface.
|
||||
// This recover transforms such panics back into an error value for the
|
||||
// caller.
|
||||
//
|
||||
// If the error is "interface not supported" we will permanently disable
|
||||
// this API henceforth.
|
||||
if v := recover(); v != nil {
|
||||
if verr, ok := v.(error); ok {
|
||||
err = verr
|
||||
}
|
||||
if oleErr, ok := v.(*ole.OleError); ok {
|
||||
if oleErr.Code() == errNoInterface {
|
||||
comDisabled.Store(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := registerClassFactory(ClassFactory); err != nil {
|
||||
return fmt.Errorf("registering class factory: %w", err)
|
||||
}
|
||||
|
||||
doc, err := dom.NewXmlDocument()
|
||||
if err != nil {
|
||||
return fmt.Errorf("dom.NewXmlDocument(): %w", err)
|
||||
}
|
||||
|
||||
defer doc.Release()
|
||||
|
||||
if err := doc.LoadXml(xml); err != nil {
|
||||
return fmt.Errorf("doc.LoadXml(tmpl): %w", err)
|
||||
}
|
||||
|
||||
manager, err := notifications.GetDefault()
|
||||
if err != nil {
|
||||
return fmt.Errorf("notifications.GetDefault(): %w", err)
|
||||
}
|
||||
|
||||
defer manager.Release()
|
||||
|
||||
notifier, err := manager.CreateToastNotifierWithId(appID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("manager.CreateToastNotifier(%q): %w", appID, err)
|
||||
}
|
||||
|
||||
defer notifier.Release()
|
||||
|
||||
toast, err := notifications.CreateToastNotification(doc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("notifications.CreateToastNotification(doc): %w", err)
|
||||
}
|
||||
|
||||
defer toast.Release()
|
||||
|
||||
if err := notifier.Show(toast); err != nil {
|
||||
return fmt.Errorf("notifier.Show(): %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setAppData(data AppData) (err error) {
|
||||
appDataMu.Lock()
|
||||
defer appDataMu.Unlock()
|
||||
|
||||
// Early out if we have already set this data.
|
||||
//
|
||||
// In the case the data is empty, we don't want to overrite
|
||||
// all of the registry entries to empty.
|
||||
//
|
||||
// This allows the caller to either globally set the app data
|
||||
// or provide it per notification.
|
||||
if appData == data || data.AppID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if data.GUID != "" {
|
||||
GUID_ImplNotificationActivationCallback = ole.NewGUID(data.GUID)
|
||||
}
|
||||
|
||||
// Keep a copy of the saved data for later.
|
||||
defer func() {
|
||||
if err == nil {
|
||||
appData = data
|
||||
}
|
||||
}()
|
||||
|
||||
if err := setAppDataFunc(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var initialized atomic.Bool
|
||||
|
||||
// initialize attempts to initialize the Windows Runtime.
|
||||
// Each invocation will retry RoInitialize until a successful initialization
|
||||
// is achieved. Once initialized, we avoid invoking RoInitialize since subsequent
|
||||
// reinitialization generates errors.
|
||||
func initialize() (err error) {
|
||||
if initialized.CompareAndSwap(false, true) {
|
||||
if err := ole.RoInitialize(1); err != nil {
|
||||
return fmt.Errorf("RoInitialize: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sliceUserDataFromUnsafe builds a slice of UserData out of an unsafe pointer.
|
||||
func sliceUserDataFromUnsafe(ptr unsafe.Pointer, count int) []UserData {
|
||||
// Layout mirrors the memory layout of the C struct that contains this data.
|
||||
// I'm not sure if there's special alignment or packing - though I don't notice
|
||||
// anything in the definition to indicate as such.
|
||||
type layout struct {
|
||||
Key unsafe.Pointer
|
||||
Value unsafe.Pointer
|
||||
}
|
||||
|
||||
// Create a new slice with the appropriate length
|
||||
out := make([]UserData, count)
|
||||
|
||||
// Create a slice with the unsafe data layout.
|
||||
tmp := unsafe.Slice((*layout)(ptr), count)
|
||||
|
||||
// Convert the unsafe layout to safe strings.
|
||||
for ii, it := range tmp {
|
||||
out[ii] = UserData{
|
||||
Key: windows.UTF16PtrToString((*uint16)(it.Key)),
|
||||
Value: windows.UTF16PtrToString((*uint16)(it.Value)),
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
179
vendor/git.sr.ht/~jackmordaunt/go-toast/v2/wintoast/impl.go
vendored
Normal file
179
vendor/git.sr.ht/~jackmordaunt/go-toast/v2/wintoast/impl.go
vendored
Normal file
@@ -0,0 +1,179 @@
|
||||
//go:build windows
|
||||
|
||||
// This file contains our pure-Go implementations of two COM objects that we need
|
||||
// to render toast notifications: IClassFactory and INotificationActivationCallback.
|
||||
//
|
||||
// More specifically we allocate the C callable functions that can be used to populate
|
||||
// the vtable at runtime.
|
||||
//
|
||||
// Unfortunately these functions have to be declared as var not const because the callbacks
|
||||
// are built at runtime. They are declared globally because `syscall.NewCallback` never
|
||||
// releases the memory it allocates for the functions thus causing an unsolvable memory
|
||||
// leak if we were to allocate these per-notification.
|
||||
//
|
||||
// The other COM interfaces we are interacting with are auto-generated from metadata.
|
||||
// However the INotificationActivationCallback is undocumented, so we have to define
|
||||
// it entirely ourselves.
|
||||
//
|
||||
// The definitions are derived from:
|
||||
// - <combase.h>
|
||||
// - <NotificationActivationCallback.h>
|
||||
package wintoast
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-ole/go-ole"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// Interface GUIDS. These GUIDS are predefined by the Windows Runtime, identifying the various
|
||||
// interfaces we want to make use of.
|
||||
var (
|
||||
IID_IClassFactory = ole.NewGUID("{00000001-0000-0000-C000-000000000046}")
|
||||
IID_INotificationActivationCallback = ole.NewGUID("{53E31837-6600-4A81-9395-75CFFE746F94}")
|
||||
)
|
||||
|
||||
// This default GUID is for our implementation.
|
||||
// This was generated and should not collide with any other GUID.
|
||||
// It's preferable for the application to override this value with its own generated GUID.
|
||||
var GUID_ImplNotificationActivationCallback = ole.NewGUID("{0F82E845-CB89-4039-BDBF-67CA33254C76}")
|
||||
|
||||
type (
|
||||
// IClassFactory defines the factory that builds our INotificationActivationCallback instance.
|
||||
// Windows Runtime loves factories.
|
||||
IClassFactory struct {
|
||||
VTable *IClassFactoryVtbl
|
||||
}
|
||||
|
||||
IClassFactoryVtbl struct {
|
||||
ole.IUnknownVtbl
|
||||
CreateInstance uintptr
|
||||
LockServer uintptr
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
// INotificationActivationCallback receives activations from toast notifications.
|
||||
INotificationActivationCallback struct {
|
||||
VTable *INotificationActivationCallbackVtbl
|
||||
}
|
||||
|
||||
INotificationActivationCallbackVtbl struct {
|
||||
ole.IUnknownVtbl
|
||||
Activate uintptr
|
||||
}
|
||||
)
|
||||
|
||||
/*
|
||||
Strictly speaking we shouldn't need to pin the static objects. They
|
||||
are package-globals and wont be garabge collected. No harm in being
|
||||
extra careful, though.
|
||||
*/
|
||||
|
||||
var pinner runtime.Pinner
|
||||
|
||||
func init() {
|
||||
pinner.Pin(ClassFactory)
|
||||
pinner.Pin(ClassFactory.VTable)
|
||||
pinner.Pin(NotificationActivationCallback)
|
||||
pinner.Pin(NotificationActivationCallback.VTable)
|
||||
}
|
||||
|
||||
// Static implementations for the IClassFactory.
|
||||
var (
|
||||
ClassFactory = &IClassFactory{
|
||||
VTable: &IClassFactoryVtbl{
|
||||
IUnknownVtbl: ole.IUnknownVtbl{
|
||||
QueryInterface: IClassFactory_QueryInterface,
|
||||
AddRef: IClassFactory_AddRef,
|
||||
Release: IClassFactory_Release,
|
||||
},
|
||||
LockServer: IClassFactory_LockServer,
|
||||
CreateInstance: IClassFactory_CreateInstance,
|
||||
},
|
||||
}
|
||||
|
||||
IClassFactory_AddRef = syscall.NewCallback(func(this *IClassFactory) (re uintptr) {
|
||||
return uintptr(1)
|
||||
})
|
||||
|
||||
IClassFactory_Release = syscall.NewCallback(func(this *IClassFactory) (re uintptr) {
|
||||
return uintptr(1)
|
||||
})
|
||||
|
||||
IClassFactory_QueryInterface = syscall.NewCallback(func(this *IClassFactory, riid *ole.GUID, out unsafe.Pointer) (re uintptr) {
|
||||
if !ole.IsEqualGUID(riid, IID_IClassFactory) &&
|
||||
!ole.IsEqualGUID(riid, ole.IID_IUnknown) {
|
||||
return ole.E_NOINTERFACE
|
||||
}
|
||||
*(**IClassFactory)(out) = this
|
||||
return ole.S_OK
|
||||
})
|
||||
|
||||
IClassFactory_LockServer = syscall.NewCallback(func(this *IClassFactory, flock uintptr) (ret uintptr) {
|
||||
return ole.S_OK
|
||||
})
|
||||
|
||||
IClassFactory_CreateInstance = syscall.NewCallback(func(this *IClassFactory, punkOuter *ole.IUnknown, riid *ole.GUID, out unsafe.Pointer) (re uintptr) {
|
||||
if punkOuter != nil {
|
||||
// Should be CLASS_E_NOAGGREGATION but ole doesn't define this.
|
||||
return ole.E_NOINTERFACE
|
||||
}
|
||||
if !ole.IsEqualGUID(riid, IID_INotificationActivationCallback) &&
|
||||
!ole.IsEqualGUID(riid, ole.IID_IUnknown) {
|
||||
return ole.E_NOINTERFACE
|
||||
}
|
||||
*(**INotificationActivationCallback)(out) = NotificationActivationCallback
|
||||
return ole.S_OK
|
||||
})
|
||||
)
|
||||
|
||||
// Static implementations for the INotificationActivationCallback.
|
||||
var (
|
||||
NotificationActivationCallback = &INotificationActivationCallback{
|
||||
VTable: &INotificationActivationCallbackVtbl{
|
||||
IUnknownVtbl: ole.IUnknownVtbl{
|
||||
QueryInterface: INotificationActivationCallback_QueryInterface,
|
||||
AddRef: INotificationActivationCallback_AddRef,
|
||||
Release: INotificationActivationCallback_Release,
|
||||
},
|
||||
Activate: INotificationActivationCallback_Activate,
|
||||
},
|
||||
}
|
||||
|
||||
INotificationActivationCallback_AddRef = syscall.NewCallback(func(this *INotificationActivationCallback) (re uintptr) {
|
||||
return uintptr(1)
|
||||
})
|
||||
|
||||
INotificationActivationCallback_Release = syscall.NewCallback(func(this *INotificationActivationCallback) (re uintptr) {
|
||||
return uintptr(1)
|
||||
})
|
||||
|
||||
INotificationActivationCallback_QueryInterface = syscall.NewCallback(func(this *INotificationActivationCallback, riid *ole.GUID, out unsafe.Pointer) (re uintptr) {
|
||||
if !ole.IsEqualGUID(riid, IID_INotificationActivationCallback) &&
|
||||
!ole.IsEqualGUID(riid, ole.IID_IUnknown) {
|
||||
return ole.E_NOINTERFACE
|
||||
}
|
||||
*(**INotificationActivationCallback)(out) = this
|
||||
return ole.S_OK
|
||||
})
|
||||
|
||||
// Activate is our re-entrance into Go from Windows. This is the magic.
|
||||
INotificationActivationCallback_Activate = syscall.NewCallback(func(
|
||||
this unsafe.Pointer,
|
||||
appUserModelId unsafe.Pointer,
|
||||
invokedArgs unsafe.Pointer,
|
||||
data unsafe.Pointer,
|
||||
count uint32,
|
||||
) (ret uintptr) {
|
||||
callback(
|
||||
windows.UTF16PtrToString((*uint16)(appUserModelId)),
|
||||
windows.UTF16PtrToString((*uint16)(invokedArgs)),
|
||||
sliceUserDataFromUnsafe(data, int(count)),
|
||||
)
|
||||
return
|
||||
})
|
||||
)
|
||||
37
vendor/git.sr.ht/~jackmordaunt/go-toast/v2/wintoast/procs.go
vendored
Normal file
37
vendor/git.sr.ht/~jackmordaunt/go-toast/v2/wintoast/procs.go
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
//go:build windows
|
||||
|
||||
package wintoast
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-ole/go-ole"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
// Define procs that go-ole doesn't provide. This is how we register our Go-implemented
|
||||
// COM objects.
|
||||
modcombase = windows.NewLazySystemDLL("combase.dll")
|
||||
procRegisterClassObject = modcombase.NewProc("CoRegisterClassObject")
|
||||
)
|
||||
|
||||
// registerClassFactory teaches the Windows Runtime about our factory that can allocate
|
||||
// instances of our ActivationCallback.
|
||||
func registerClassFactory(factory *IClassFactory) error {
|
||||
// cookie is used as a handle to this class. It is used when calling CoRevokeClassObject
|
||||
// which unregisters the class. We don't need it until we plan to revoke this registration
|
||||
// for some reason.
|
||||
var cookie int64
|
||||
hr, _, _ := procRegisterClassObject.Call(
|
||||
uintptr(unsafe.Pointer(GUID_ImplNotificationActivationCallback)),
|
||||
uintptr(unsafe.Pointer(factory)),
|
||||
uintptr(ole.CLSCTX_LOCAL_SERVER),
|
||||
uintptr(1), /* REGCLS_MULTIPLEUSE */
|
||||
uintptr(unsafe.Pointer(&cookie)),
|
||||
)
|
||||
if hr != ole.S_OK {
|
||||
return ole.NewError(hr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
110
vendor/git.sr.ht/~jackmordaunt/go-toast/v2/wintoast/registry.go
vendored
Normal file
110
vendor/git.sr.ht/~jackmordaunt/go-toast/v2/wintoast/registry.go
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
//go:build windows
|
||||
|
||||
// This file contains registry manipulation code.
|
||||
// This logic is orthogonal to, but works in tandem with the COM code; since the
|
||||
// Windows Runtime uses the registry as it's primary source of state.
|
||||
package wintoast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
var (
|
||||
// allows diffing the new call from the previous so that we can early-out,
|
||||
// and avoid touching the registry more than necessary.
|
||||
// It also allows empty app data to be supplied to the Notifcation type,
|
||||
// without erasing the data that has been set via the global function.
|
||||
appData AppData
|
||||
appDataMu sync.Mutex
|
||||
)
|
||||
|
||||
// Overridden in testing.
|
||||
var (
|
||||
writeStringValue = writeStringValueImpl
|
||||
setAppDataFunc = setAppDataImpl
|
||||
)
|
||||
|
||||
var (
|
||||
// appKeyRoot is the root path for app metadata.
|
||||
appKeyRoot = filepath.Join("SOFTWARE", "Classes", "AppUserModelId")
|
||||
// activationKey is the root path to the activation executable.
|
||||
activationKey = filepath.Join("SOFTWARE", "Classes", "CLSID", GUID_ImplNotificationActivationCallback.String(), "LocalServer32")
|
||||
)
|
||||
|
||||
// The Windows registry package uses empty string for the "(Default)" key.
|
||||
const registryDefaultKey string = ""
|
||||
|
||||
func setAppDataImpl(data AppData) error {
|
||||
if data.AppID == "" {
|
||||
return fmt.Errorf("empty app ID")
|
||||
}
|
||||
|
||||
appKey := filepath.Join(appKeyRoot, data.AppID)
|
||||
|
||||
if err := writeStringValue(appKey, "DisplayName", data.AppID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// CustomActivator teaches Window what COM class to use as the callback when
|
||||
// a toast notification is activated.
|
||||
if err := writeStringValue(appKey, "CustomActivator", GUID_ImplNotificationActivationCallback.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if data.IconPath != "" {
|
||||
if err := writeStringValue(appKey, "IconUri", data.IconPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if data.IconBackgroundColor != "" {
|
||||
if err := writeStringValue(appKey, "IconBackgroundColor", data.IconBackgroundColor); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if data.ActivationExe != "" {
|
||||
if err := writeStringValue(activationKey, registryDefaultKey, data.ActivationExe); err != nil {
|
||||
return fmt.Errorf("setting activation executable: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeStringValue writes a string value to the path, where name is the subkey and
|
||||
// value is the literal value.
|
||||
func writeStringValueImpl(path, name, value string) error {
|
||||
if keyExists(path, name) {
|
||||
return nil
|
||||
}
|
||||
key, _, err := registry.CreateKey(registry.CURRENT_USER, path, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening registry key: %s: %w", path, err)
|
||||
}
|
||||
if err := key.SetStringValue(name, value); err != nil {
|
||||
return fmt.Errorf("setting string value: (%s) %s=%s: %w", path, name, value, err)
|
||||
}
|
||||
if err := key.Close(); err != nil {
|
||||
return fmt.Errorf("closing key: %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// keyExists returns true if the key exists.
|
||||
func keyExists(path, name string) bool {
|
||||
key, err := registry.OpenKey(registry.CURRENT_USER, path, registry.READ)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer key.Close()
|
||||
v, _, err := key.GetStringValue(name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return v != ""
|
||||
}
|
||||
Reference in New Issue
Block a user