func (n *notifier) SendNotification(note Notification) (uint32, error) {
	return SendNotification(n.conn, note)
}
SendNotification sends a notification to the notification server and returns the ID or an error.

Implements dbus call:

UINT32 org.freedesktop.Notifications.Notify (
STRING app_name,
UINT32 replaces_id,
STRING app_icon,
STRING summary,
STRING body,
ARRAY  actions,
DICT   hints,
INT32  expire_timeout
);

Name	    	Type	Description
app_name		STRING	The optional name of the application sending the notification. Can be blank.
replaces_id	    UINT32	The optional notification ID that this notification replaces. The server must atomically (ie with no flicker or other visual cues) replace the given notification with this one. This allows clients to effectively modify the notification while it's active. A value of value of 0 means that this notification won't replace any existing notifications.
app_icon		STRING	The optional program icon of the calling application. Can be an empty string, indicating no icon.
summary		    STRING	The summary text briefly describing the notification.
body			STRING	The optional detailed body text. Can be empty.
actions		    ARRAY	Actions are sent over as a list of pairs. Each even element in the list (starting at index 0) represents the identifier for the action. Each odd element in the list is the localized string that will be displayed to the user.
hints	        DICT	Optional hints that can be passed to the server from the client program. Although clients and servers should never assume each other supports any specific hints, they can be used to pass along information, such as the process PID or window ID, that the server may be able to make use of. See Hints. Can be empty.
expire_timeout  INT32   The timeout time in milliseconds since the display of the notification at which the notification should automatically close.
If -1, the notification's expiration time is dependent on the notification server's settings, and may vary for the type of notification. If 0, never expire.

If replaces_id is 0, the return value is a UINT32 that represent the notification.
It is unique, and will not be reused unless a MAXINT number of notifications have been generated.
An acceptable implementation may just use an incrementing counter for the ID.
The returned ID is always greater than zero. Servers must make sure not to return zero as an ID.

If replaces_id is not 0, the returned value is the same value as replaces_id.
package notify
import (
	"errors"
	"fmt"
	"log"
	"sync"
	"time"
	"github.com/godbus/dbus/v5"
)
const (
	dbusRemoveMatch            = "org.freedesktop.DBus.RemoveMatch"
	dbusAddMatch               = "org.freedesktop.DBus.AddMatch"
	dbusObjectPath             = "/org/freedesktop/Notifications"  
	dbusNotificationsInterface = "org.freedesktop.Notifications"   
	signalNotificationClosed   = "org.freedesktop.Notifications.NotificationClosed"
	signalActionInvoked        = "org.freedesktop.Notifications.ActionInvoked"
	callGetCapabilities        = "org.freedesktop.Notifications.GetCapabilities"
	callCloseNotification      = "org.freedesktop.Notifications.CloseNotification"
	callNotify                 = "org.freedesktop.Notifications.Notify"
	callGetServerInformation   = "org.freedesktop.Notifications.GetServerInformation"
	channelBufferSize = 10
)
type Notification struct {
	AppName string
	ReplacesID uint32
	AppIcon string
	Summary string
	Body    string
	Actions []Action
	Hints   map[string]dbus.Variant
	ExpireTimeout time.Duration
}
type Action struct {
	Key   string
	Label string
}
func SendNotification(conn *dbus.Conn, note Notification) (uint32, error) {
	actions := []string{}
	for i := range note.Actions {
		actions = append(actions, note.Actions[i].Key, note.Actions[i].Label)
	}
	durationMs := int32(note.ExpireTimeout.Milliseconds())
	obj := conn.Object(dbusNotificationsInterface, dbusObjectPath)
	call := obj.Call(callNotify, 0,
		note.AppName,
		note.ReplacesID,
		note.AppIcon,
		note.Summary,
		note.Body,
		actions,
		note.Hints,
		durationMs)
	if call.Err != nil {
		return 0, fmt.Errorf("error sending notification: %w", call.Err)
	}
	var ret uint32
	err := call.Store(&ret)
	if err != nil {
		return ret, fmt.Errorf("error getting uint32 ret value: %w", err)
	}
	return ret, nil
}
type ServerInformation struct {
	Name        string
	Vendor      string
	Version     string
	SpecVersion string
}
func GetServerInformation(conn *dbus.Conn) (ServerInformation, error) {
	obj := conn.Object(dbusNotificationsInterface, dbusObjectPath)
	if obj == nil {
		return ServerInformation{}, errors.New("error creating dbus call object")
	}
	call := obj.Call(callGetServerInformation, 0)
	if call.Err != nil {
		return ServerInformation{}, fmt.Errorf("error calling %v: %v", callGetServerInformation, call.Err)
	}
	ret := ServerInformation{}
	err := call.Store(&ret.Name, &ret.Vendor, &ret.Version, &ret.SpecVersion)
	if err != nil {
		return ret, fmt.Errorf("error reading %v return values: %v", callGetServerInformation, err)
	}
	return ret, nil
}
func GetCapabilities(conn *dbus.Conn) ([]string, error) {
	obj := conn.Object(dbusNotificationsInterface, dbusObjectPath)
	call := obj.Call(callGetCapabilities, 0)
	if call.Err != nil {
		return []string{}, call.Err
	}
	var ret []string
	err := call.Store(&ret)
	if err != nil {
		return ret, err
	}
	return ret, nil
}
type Notifier interface {
	SendNotification(n Notification) (uint32, error)
	GetCapabilities() ([]string, error)
	GetServerInformation() (ServerInformation, error)
	CloseNotification(id uint32) (bool, error)
	Close() error
}
type NotificationClosedHandler func(*NotificationClosedSignal)
type ActionInvokedHandler func(*ActionInvokedSignal)
type ActionInvokedSignal struct {
	ID uint32
	ActionKey string
}
type notifier struct {
	conn     *dbus.Conn
	signal   chan *dbus.Signal
	done     chan bool
	onClosed NotificationClosedHandler
	onAction ActionInvokedHandler
	wg       *sync.WaitGroup
	log      logger
}
type logger interface {
	Printf(format string, v ...interface{})
}
type option func(*notifier)
func WithLogger(logz logger) option {
	return func(n *notifier) {
		n.log = logz
	}
}
func WithOnAction(h ActionInvokedHandler) option {
	return func(n *notifier) {
		n.onAction = h
	}
}
func WithOnClosed(h NotificationClosedHandler) option {
	return func(n *notifier) {
		n.onClosed = h
	}
}
func New(conn *dbus.Conn, opts ...option) (Notifier, error) {
	n := &notifier{
		conn:     conn,
		signal:   make(chan *dbus.Signal, channelBufferSize),
		done:     make(chan bool),
		wg:       &sync.WaitGroup{},
		onClosed: func(s *NotificationClosedSignal) {},
		onAction: func(s *ActionInvokedSignal) {},
		log:      &loggerWrapper{"notify: "},
	}
	for _, val := range opts {
		val(n)
	}
	err := n.conn.AddMatchSignal(
		dbus.WithMatchObjectPath(dbusObjectPath),
		dbus.WithMatchInterface(dbusNotificationsInterface),
	)
	if err != nil {
		return nil, fmt.Errorf("error registering for signals in dbus: %w", err)
	}
	n.conn.Signal(n.signal)
	go n.eventLoop()
	return n, nil
}
func (n notifier) eventLoop() {
	n.wg.Add(1)
	defer n.wg.Done()
	for {
		select {
		case signal := <-n.signal:
			n.handleSignal(signal)
		case <-n.done:
			n.log.Printf("Got Close() signal, shutting down...")
			return
		}
	}
}
func (n notifier) handleSignal(signal *dbus.Signal) {
	switch signal.Name {
	case signalNotificationClosed:
		nc := &NotificationClosedSignal{
			ID:     signal.Body[0].(uint32),
			Reason: Reason(signal.Body[1].(uint32)),
		}
		n.onClosed(nc)
	case signalActionInvoked:
		is := &ActionInvokedSignal{
			ID:        signal.Body[0].(uint32),
			ActionKey: signal.Body[1].(string),
		}
		n.onAction(is)
	default:
		n.log.Printf("Received unknown signal: %+v", signal)
	}
}
func (n *notifier) GetCapabilities() ([]string, error) {
	return GetCapabilities(n.conn)
}
func (n *notifier) GetServerInformation() (ServerInformation, error) {
	return GetServerInformation(n.conn)
}