Files

152 lines
3.3 KiB
Go

package notifyactions
import (
"os/exec"
"path/filepath"
"sync"
"syscall"
"github.com/godbus/dbus/v5"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
const (
notifyPath = "/org/freedesktop/Notifications"
notifyInterface = "org.freedesktop.Notifications"
actionInvoked = notifyInterface + ".ActionInvoked"
closed = notifyInterface + ".NotificationClosed"
dbusInterface = "org.freedesktop.DBus"
nameOwnerChanged = "NameOwnerChanged"
ownerChanged = dbusInterface + "." + nameOwnerChanged
)
type watched struct {
path string
command []string
}
type Manager struct {
conn *dbus.Conn
signals chan *dbus.Signal
mu sync.Mutex
watched map[uint32]watched
}
func NewManager() (*Manager, error) {
conn, err := dbus.SessionBus()
if err != nil {
return nil, err
}
if err := conn.AddMatchSignal(dbus.WithMatchObjectPath(notifyPath), dbus.WithMatchInterface(notifyInterface)); err != nil {
return nil, err
}
// A restarted notification server reuses ids without sending NotificationClosed.
if err := conn.AddMatchSignal(dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(nameOwnerChanged), dbus.WithMatchArg(0, notifyInterface)); err != nil {
return nil, err
}
m := &Manager{
conn: conn,
signals: make(chan *dbus.Signal, 32),
watched: make(map[uint32]watched),
}
conn.Signal(m.signals)
go m.loop()
return m, nil
}
func (m *Manager) Watch(id uint32, path string) {
m.mu.Lock()
m.watched[id] = watched{path: path}
m.mu.Unlock()
}
func (m *Manager) WatchCommand(id uint32, argv []string) {
m.mu.Lock()
m.watched[id] = watched{command: argv}
m.mu.Unlock()
}
func (m *Manager) Close() {
m.conn.RemoveSignal(m.signals)
_ = m.conn.RemoveMatchSignal(dbus.WithMatchObjectPath(notifyPath), dbus.WithMatchInterface(notifyInterface))
_ = m.conn.RemoveMatchSignal(dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(nameOwnerChanged), dbus.WithMatchArg(0, notifyInterface))
close(m.signals)
}
func (m *Manager) loop() {
for sig := range m.signals {
switch sig.Name {
case actionInvoked:
m.handleAction(sig)
case closed:
m.forget(sig)
case ownerChanged:
// The shared bus connection also delivers other managers' NameOwnerChanged matches.
if len(sig.Body) < 1 || sig.Body[0] != notifyInterface {
continue
}
m.mu.Lock()
clear(m.watched)
m.mu.Unlock()
}
}
}
func (m *Manager) take(sig *dbus.Signal) (watched, bool) {
if len(sig.Body) < 1 {
return watched{}, false
}
id, ok := sig.Body[0].(uint32)
if !ok {
return watched{}, false
}
m.mu.Lock()
defer m.mu.Unlock()
w, ok := m.watched[id]
if !ok {
return watched{}, false
}
delete(m.watched, id)
return w, true
}
func (m *Manager) forget(sig *dbus.Signal) {
m.take(sig)
}
func (m *Manager) handleAction(sig *dbus.Signal) {
if len(sig.Body) < 2 {
return
}
action, ok := sig.Body[1].(string)
if !ok {
return
}
w, ok := m.take(sig)
if !ok {
return
}
if len(w.command) > 0 {
if action == "default" {
run(w.command)
}
return
}
path := w.path
if action == "folder" {
path = filepath.Dir(path)
}
run([]string{"xdg-open", path})
}
func run(argv []string) {
cmd := exec.Command(argv[0], argv[1:]...)
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
if err := cmd.Start(); err != nil {
log.Warnf("notifyactions: %v: %v", argv, err)
return
}
go func() { _ = cmd.Wait() }()
}