aboutsummaryrefslogtreecommitdiffstats
path: root/internal
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-15 18:03:29 +0200
committerDanilo M. <danix@danix.xyz>2026-09-15 18:03:29 +0200
commit03784b2024436827de25515b1d85d9ffdde40745 (patch)
treebbd1d9264a83a5ea6ad359fe699b622ec15386bb /internal
parent7a6b1c1ada3ba6ca2ca4ed70cf51f2f5f30d7b1e (diff)
downloadnotifyd-03784b2024436827de25515b1d85d9ffdde40745.tar.gz
notifyd-03784b2024436827de25515b1d85d9ffdde40745.zip
feat(notify): materialise notification images
Notify resolves app_icon and image-path theme names, decodes image-data to a PNG under the image directory, and re-publishes the entry with its image path. The daemon advertises body-images, and removeImage refuses to touch a path outside its own directory so a client's screenshot file is never deleted.
Diffstat (limited to 'internal')
-rw-r--r--internal/notify/service.go27
-rw-r--r--internal/notify/service_test.go29
2 files changed, 53 insertions, 3 deletions
diff --git a/internal/notify/service.go b/internal/notify/service.go
index 2442127..7bfad6e 100644
--- a/internal/notify/service.go
+++ b/internal/notify/service.go
@@ -73,6 +73,9 @@ func (s *Service) Start() error {
if err := s.conn.Export(&control{s}, dbus.ObjectPath(ctrlPath), ctrlIface); err != nil {
return err
}
+ if err := os.RemoveAll(ImagesDir(s.dir)); err != nil {
+ log.Printf("notifyd: clear images: %v", err)
+ }
s.store.Reset()
return nil
}
@@ -83,9 +86,10 @@ func (s *Service) emitClosed(id, reason uint32) {
// GetCapabilities tells clients what the daemon understands. actions and
// body-markup are load-bearing: mail-notify sends actions and escapes its body
-// because the running dunst advertises markup.
+// because the running dunst advertises markup. body-images says the daemon
+// decodes image-data and image-path, not just a static app icon.
func (s *Service) GetCapabilities() ([]string, *dbus.Error) {
- return []string{"actions", "body-markup", "icon-static", "persistence"}, nil
+ return []string{"actions", "body-markup", "body-images", "icon-static", "persistence"}, nil
}
func (s *Service) GetServerInformation() (string, string, string, string, *dbus.Error) {
@@ -99,19 +103,36 @@ func (s *Service) Notify(appName string, replacesID uint32, appIcon, summary, bo
tag := StackTagFromHints(hints)
now := time.Now().UnixMilli()
ms := EffectiveTimeoutMS(expireTimeout, u)
+ // image-data wins over image-path, and its blob cannot be written until
+ // Add has assigned the id the filename is derived from.
+ var pendingImage []byte
n := &Popup{
App: appName,
Summary: summary,
Body: body,
Urgency: u,
- Icon: appIcon,
+ Icon: ResolveIcon(appIcon),
Actions: ParseActions(actions),
Created: now,
}
+ if raw, ok := ImageDataFromHints(hints); ok {
+ if data, err := raw.PNG(); err == nil {
+ pendingImage = data
+ }
+ } else if path, ok := ImagePathFromHints(hints); ok {
+ n.Image = ResolveIcon(path)
+ }
if ms > 0 {
n.Expires = now + ms
}
id, _ := s.store.Add(n, tag, replacesID)
+ if pendingImage != nil {
+ if path, err := WriteImage(s.dir, id, pendingImage); err == nil {
+ s.store.SetImage(id, path)
+ } else {
+ log.Printf("notifyd: write image: %v", err)
+ }
+ }
s.arm(id, ms)
return id, nil
}
diff --git a/internal/notify/service_test.go b/internal/notify/service_test.go
index 871a0c7..735d01c 100644
--- a/internal/notify/service_test.go
+++ b/internal/notify/service_test.go
@@ -67,6 +67,35 @@ func TestIdentityAndCapabilities(t *testing.T) {
}
}
+func TestCapabilitiesIncludeBodyImages(t *testing.T) {
+ caps, err := NewService(nil, t.TempDir()).GetCapabilities()
+ if err != nil {
+ t.Fatal(err)
+ }
+ found := false
+ for _, c := range caps {
+ if c == "body-images" {
+ found = true
+ }
+ }
+ if !found {
+ t.Fatalf("body-images missing from %v", caps)
+ }
+}
+
+func TestNotifyPublishesImagePath(t *testing.T) {
+ var live []Popup
+ s := &Service{dir: t.TempDir(), timers: map[uint32]*time.Timer{}}
+ s.store = NewStore(s.emitClosed, func(l, _ []Popup) { live = l }, s.removeImage)
+ hints := map[string]dbus.Variant{"image-path": dbus.MakeVariant("/tmp/shot.png")}
+ if _, err := s.Notify("t", 0, "", "s", "b", nil, hints, -1); err != nil {
+ t.Fatal(err)
+ }
+ if len(live) != 1 || live[0].Image != "/tmp/shot.png" {
+ t.Fatalf("published %+v", live)
+ }
+}
+
func TestNotifyReturnsAnIDAndPublishes(t *testing.T) {
conn := busOrSkip(t)
dir := t.TempDir()