From 3b8a0e0f4a5c28394ac578e8559bf57ae82d3fd2 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 15 Sep 2026 17:59:35 +0200 Subject: feat(notify): add the image field and its cleanup Popup gains image, and the store calls an injected removeImage when an entry is dismissed, evicted, replaced or expired, so a daemon-written PNG does not outlive its balloon. The service decides what is daemon-owned; the store only names the path. NewStore now takes the callback as a third parameter, so the service wires its removeImage in (unlinking only under ImagesDir) and the existing call sites pass nil. --- internal/notify/store.go | 48 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 12 deletions(-) (limited to 'internal/notify/store.go') diff --git a/internal/notify/store.go b/internal/notify/store.go index f1e9fe4..7a1a745 100644 --- a/internal/notify/store.go +++ b/internal/notify/store.go @@ -22,6 +22,7 @@ type Popup struct { Body string `json:"body"` Urgency Urgency `json:"urgency"` Icon string `json:"icon"` + Image string `json:"image"` Actions [][2]string `json:"actions"` Created int64 `json:"created"` Expires int64 `json:"expires"` @@ -43,21 +44,23 @@ const ( // Store holds the live queue and the history ring. Time, signals and file // writes are injected, so the whole thing is tested without a bus or a clock. type Store struct { - mu sync.Mutex - nextID uint32 - order []uint32 - entries map[uint32]*live - history []*live - emit func(id, reason uint32) - publish func(live, history []Popup) + mu sync.Mutex + nextID uint32 + order []uint32 + entries map[uint32]*live + history []*live + emit func(id, reason uint32) + publish func(live, history []Popup) + removeImage func(string) } -func NewStore(emit func(id, reason uint32), publish func(live, history []Popup)) *Store { +func NewStore(emit func(id, reason uint32), publish func(live, history []Popup), removeImage func(string)) *Store { return &Store{ - nextID: 1, - entries: map[uint32]*live{}, - emit: emit, - publish: publish, + nextID: 1, + entries: map[uint32]*live{}, + emit: emit, + publish: publish, + removeImage: removeImage, } } @@ -82,6 +85,9 @@ func (s *Store) Add(n *Popup, stack string, replacesID uint32) (uint32, bool) { } add := &live{Popup: *n, Stack: stack} if old != nil { + if s.removeImage != nil { + s.removeImage(old.Image) + } add.ID = old.ID s.entries[add.ID] = add s.moveToFrontLocked(add.ID) @@ -107,9 +113,24 @@ func (s *Store) Expire(id uint32) { return } n.Closed = true + if s.removeImage != nil { + s.removeImage(n.Image) + } s.emit(id, 1) } +// SetImage attaches a materialised image path to a live entry and republishes. +func (s *Store) SetImage(id uint32, path string) { + s.mu.Lock() + defer s.mu.Unlock() + n, ok := s.entries[id] + if !ok { + return + } + n.Image = path + s.publishLocked() +} + // Dismiss is an explicit close from either renderer. The client is told only // if expiry has not already told it, then the entry is filed. func (s *Store) Dismiss(id, reason uint32) { @@ -174,6 +195,9 @@ func (s *Store) historySnapshot() []*live { } func (s *Store) removeLocked(id uint32) { + if n, ok := s.entries[id]; ok && s.removeImage != nil { + s.removeImage(n.Image) + } delete(s.entries, id) for i, v := range s.order { if v == id { -- cgit v1.2.3