From 1e70e552db6790f2fd07fdd1721bf6cf76e0c6b3 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 19 Oct 2026 09:15:53 +0200 Subject: feat(notify): take the per-urgency default timeout from config Co-Authored-By: Claude Opus 5.5 --- internal/notify/policy.go | 26 ++++++++++++++------------ internal/notify/policy_test.go | 19 ++++++++++++------- internal/notify/service.go | 8 ++++++-- 3 files changed, 32 insertions(+), 21 deletions(-) (limited to 'internal/notify') diff --git a/internal/notify/policy.go b/internal/notify/policy.go index 5c70390..8161ea3 100644 --- a/internal/notify/policy.go +++ b/internal/notify/policy.go @@ -23,26 +23,28 @@ const ( ) // DefaultTimeoutMS is the balloon lifetime for an urgency when the client -// leaves the choice to the server (expire_timeout -1). Critical never expires, -// which is 0 here. -func DefaultTimeoutMS(u Urgency) int64 { +// leaves the choice to the server (expire_timeout -1), from the config's +// seconds. 0 is never. +func DefaultTimeoutMS(u Urgency, t Timeouts) int64 { switch u { - case Low, Normal: - return 10_000 + case Low: + return int64(t.Low) * 1000 + case Critical: + return int64(t.Critical) * 1000 default: - return 0 + return int64(t.Normal) * 1000 } } // EffectiveTimeoutMS resolves the client's expire_timeout per the freedesktop -// spec: -1 means the server decides (the urgency default), 0 means never, and -// anything positive is milliseconds and wins. The spec is precise about the -// direction and libnotify's default is -1, so getting it backwards would make -// every plain notify-send immortal. -func EffectiveTimeoutMS(expire int32, u Urgency) int64 { +// spec: -1 means the server decides (the configured default), 0 means never, +// and anything positive is milliseconds and wins. The spec is precise about +// the direction and libnotify's default is -1, so getting it backwards would +// make every plain notify-send immortal. +func EffectiveTimeoutMS(expire int32, u Urgency, t Timeouts) int64 { switch { case expire == -1: - return DefaultTimeoutMS(u) + return DefaultTimeoutMS(u, t) case expire == 0: return 0 default: diff --git a/internal/notify/policy_test.go b/internal/notify/policy_test.go index 378c573..a6ca5a5 100644 --- a/internal/notify/policy_test.go +++ b/internal/notify/policy_test.go @@ -40,22 +40,27 @@ func TestUrgencyFromHints(t *testing.T) { } func TestEffectiveTimeoutMS(t *testing.T) { + def := DefaultConfig().Timeouts + custom := Timeouts{Low: 3, Normal: 7, Critical: 60} cases := []struct { name string expire int32 u Urgency + t Timeouts want int64 }{ - {"minus one uses low default", -1, Low, 10_000}, - {"minus one uses normal default", -1, Normal, 10_000}, - {"minus one critical never", -1, Critical, 0}, - {"zero never", 0, Normal, 0}, - {"explicit wins", 3_000, Normal, 3_000}, - {"sub second exact", 1, Normal, 1}, + {"minus one uses low default", -1, Low, def, 10_000}, + {"minus one uses normal default", -1, Normal, def, 10_000}, + {"minus one critical never", -1, Critical, def, 0}, + {"minus one uses configured low", -1, Low, custom, 3_000}, + {"minus one uses configured critical", -1, Critical, custom, 60_000}, + {"zero never", 0, Normal, custom, 0}, + {"explicit wins", 3_000, Normal, custom, 3_000}, + {"sub second exact", 1, Normal, def, 1}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { - if got := EffectiveTimeoutMS(c.expire, c.u); got != c.want { + if got := EffectiveTimeoutMS(c.expire, c.u, c.t); got != c.want { t.Errorf("EffectiveTimeoutMS(%d, %q) = %d, want %d", c.expire, c.u, got, c.want) } }) diff --git a/internal/notify/service.go b/internal/notify/service.go index a2bf103..b29a783 100644 --- a/internal/notify/service.go +++ b/internal/notify/service.go @@ -39,10 +39,11 @@ type Service struct { mu sync.Mutex timers map[uint32]*time.Timer + cfg Config } func NewService(conn *dbus.Conn, dir string) *Service { - s := &Service{conn: conn, dir: dir, timers: map[uint32]*time.Timer{}} + s := &Service{conn: conn, dir: dir, timers: map[uint32]*time.Timer{}, cfg: DefaultConfig()} s.store = NewStore(s.emitClosed, func(live, history []Popup) { if err := Publish(s.dir, live, history); err != nil { log.Printf("notifyd: publish: %v", err) @@ -102,7 +103,10 @@ func (s *Service) Notify(appName string, replacesID uint32, appIcon, summary, bo u := UrgencyFromHints(hints) tag := StackTagFromHints(hints) now := time.Now().UnixMilli() - ms := EffectiveTimeoutMS(expireTimeout, u) + s.mu.Lock() + timeouts := s.cfg.Timeouts + s.mu.Unlock() + ms := EffectiveTimeoutMS(expireTimeout, u, timeouts) // Spec priority is image-data > image-path > the deprecated icon_data. A // raw blob cannot be written until Add has assigned the id the filename is // derived from, so any raw tier is held in pendingImage. -- cgit v1.2.3