master sewn/kohai / ui / dbus_linux.go
  1//go:build linux
  2
  3package ui
  4
  5import (
  6	"fmt"
  7	"net/url"
  8	"sync"
  9
 10	"github.com/godbus/dbus/v5"
 11)
 12
 13var dbusConn *dbus.Conn
 14var dbusLock sync.Mutex
 15
 16var notifications = make(map[int]*NotifyEvent)
 17
 18func notifyDBus(title, content string) int {
 19	conn, err := dbus.SessionBus()
 20	if err != nil {
 21		return -1
 22	}
 23	var r uint32
 24	obj := conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
 25	err = obj.Call("org.freedesktop.Notifications.Notify", 0, "senpai", uint32(0), "senpai", title, content, []string{
 26		"default", "Open",
 27	}, map[string]dbus.Variant{
 28		"category":      dbus.MakeVariant("im.received"),
 29		"desktop-entry": dbus.MakeVariant("senpai"),
 30		"image-path":    dbus.MakeVariant("senpai"),
 31		"urgency":       dbus.MakeVariant(uint8(1)), // Normal
 32	}, int32(-1)).Store(&r)
 33	if err != nil {
 34		return -1
 35	}
 36	return int(r)
 37}
 38
 39func (ui *UI) notify(target NotifyEvent, title, content string) int {
 40	if ui.config.LocalIntegrations {
 41		id := notifyDBus(title, content)
 42		if id > 0 {
 43			dbusLock.Lock()
 44			notifications[id] = &target
 45			dbusLock.Unlock()
 46			return id
 47		}
 48	}
 49
 50	ui.vx.Notify(title, content)
 51	return -1
 52}
 53
 54func notifyClose(id int) {
 55	conn, err := dbus.SessionBus()
 56	if err != nil {
 57		return
 58	}
 59	obj := conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
 60	obj.Call("org.freedesktop.Notifications.CloseNotification", 0, uint32(id))
 61}
 62
 63func Screenshot() error {
 64	conn, err := dbus.SessionBus()
 65	if err != nil {
 66		return fmt.Errorf("failed to connect to D-Bus: %v", err)
 67	}
 68	obj := conn.Object("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop")
 69	c := obj.Call("org.freedesktop.portal.Screenshot.Screenshot", 0, "", map[string]dbus.Variant{
 70		"modal":       dbus.MakeVariant(true),
 71		"interactive": dbus.MakeVariant(true),
 72	})
 73	if c.Err != nil {
 74		return fmt.Errorf("failed to take screenshot: %v", c.Err)
 75	}
 76	return nil
 77}
 78
 79func DBusStart(callback func(any)) {
 80	conn, err := dbus.SessionBus()
 81	if err != nil {
 82		return
 83	}
 84	if err := conn.AddMatchSignal(
 85		dbus.WithMatchObjectPath("/org/freedesktop/Notifications"),
 86		dbus.WithMatchInterface("org.freedesktop.Notifications"),
 87		dbus.WithMatchSender("org.freedesktop.Notifications.ActionInvoked"),
 88	); err != nil {
 89		return
 90	}
 91	if err := conn.AddMatchSignal(
 92		dbus.WithMatchObjectPath("/org/freedesktop/portal/desktop"),
 93		dbus.WithMatchInterface("org.freedesktop.portal.Request"),
 94		dbus.WithMatchMember("Response"),
 95	); err != nil {
 96		return
 97	}
 98	c := make(chan *dbus.Signal, 64)
 99	conn.Signal(c)
100	dbusLock.Lock()
101	dbusConn = conn
102	dbusLock.Unlock()
103	go func() {
104		for v := range c {
105			switch v.Name {
106			case "org.freedesktop.Notifications.NotificationClosed":
107				id := int(v.Body[0].(uint32))
108				dbusLock.Lock()
109				delete(notifications, id)
110				dbusLock.Unlock()
111			case "org.freedesktop.Notifications.ActionInvoked":
112				id := int(v.Body[0].(uint32))
113				dbusLock.Lock()
114				target, ok := notifications[id]
115				dbusLock.Unlock()
116				if ok {
117					callback(target)
118				}
119			case "org.freedesktop.portal.Request.Response":
120				status := v.Body[0].(uint32)
121				results := v.Body[1].(map[string]dbus.Variant)
122				if status == 0 /* success */ {
123					uri := results["uri"].Value().(string)
124					if u, err := url.Parse(uri); err == nil && u.Scheme == "file" {
125						callback(&ScreenshotEvent{
126							Path: u.Path,
127						})
128					}
129				}
130			}
131		}
132	}()
133}
134
135func DBusStop() {
136	dbusLock.Lock()
137	c := dbusConn
138	dbusConn = nil
139	dbusLock.Unlock()
140	if c != nil {
141		c.Close()
142	}
143}