diff options
Diffstat (limited to 'internal/notify')
| -rw-r--r-- | internal/notify/policy.go | 95 | ||||
| -rw-r--r-- | internal/notify/policy_test.go | 89 |
2 files changed, 184 insertions, 0 deletions
diff --git a/internal/notify/policy.go b/internal/notify/policy.go new file mode 100644 index 0000000..c1dc647 --- /dev/null +++ b/internal/notify/policy.go @@ -0,0 +1,95 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +package notify + +import "github.com/godbus/dbus/v5" + +// Urgency is the freedesktop urgency level. +type Urgency string + +const ( + Low Urgency = "low" + Normal Urgency = "normal" + Critical Urgency = "critical" +) + +// DefaultTimeoutMS is the balloon lifetime for an urgency when the client sends +// expire_timeout 0. Critical never expires, which is 0 here. +func DefaultTimeoutMS(u Urgency) int64 { + switch u { + case Low, Normal: + return 10_000 + default: + return 0 + } +} + +// EffectiveTimeoutMS resolves the client's expire_timeout: 0 means the urgency +// default, -1 means never, anything positive is milliseconds and wins. +func EffectiveTimeoutMS(expire int32, u Urgency) int64 { + switch { + case expire == -1: + return 0 + case expire == 0: + return DefaultTimeoutMS(u) + default: + return int64(expire) + } +} + +// UrgencyFromHints reads the urgency byte: 0 low, 1 normal, 2 critical. A +// missing or malformed value is normal, the same default libnotify uses. +func UrgencyFromHints(hints map[string]dbus.Variant) Urgency { + v, ok := hints["urgency"] + if !ok { + return Normal + } + switch n := v.Value().(type) { + case uint8: + switch n { + case 0: + return Low + case 2: + return Critical + } + case int32: + switch n { + case 0: + return Low + case 2: + return Critical + } + } + return Normal +} + +// StackTagFromHints reads the dunst stack tag, then the danix spelling. Both +// mean the same thing and dunst wins when a client sends both. +func StackTagFromHints(hints map[string]dbus.Variant) string { + for _, key := range []string{"x-dunst-stack-tag", "x-danix-stack-tag"} { + if v, ok := hints[key]; ok { + if s, ok := v.Value().(string); ok && s != "" { + return s + } + } + } + return "" +} + +// ParseActions turns the spec's flat [key, label, key, label] array into pairs. +func ParseActions(flat []string) [][2]string { + out := make([][2]string, 0, len(flat)/2) + for i := 0; i+1 < len(flat); i += 2 { + out = append(out, [2]string{flat[i], flat[i+1]}) + } + return out +} diff --git a/internal/notify/policy_test.go b/internal/notify/policy_test.go new file mode 100644 index 0000000..256c374 --- /dev/null +++ b/internal/notify/policy_test.go @@ -0,0 +1,89 @@ +package notify + +import ( + "testing" + + "github.com/godbus/dbus/v5" +) + +func TestUrgencyFromHints(t *testing.T) { + cases := []struct { + name string + hints map[string]dbus.Variant + want Urgency + }{ + {"missing is normal", map[string]dbus.Variant{}, Normal}, + {"low byte", map[string]dbus.Variant{"urgency": dbus.MakeVariant(byte(0))}, Low}, + {"normal byte", map[string]dbus.Variant{"urgency": dbus.MakeVariant(byte(1))}, Normal}, + {"critical byte", map[string]dbus.Variant{"urgency": dbus.MakeVariant(byte(2))}, Critical}, + {"critical int32", map[string]dbus.Variant{"urgency": dbus.MakeVariant(int32(2))}, Critical}, + {"wrong type is normal", map[string]dbus.Variant{"urgency": dbus.MakeVariant("2")}, Normal}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if got := UrgencyFromHints(c.hints); got != c.want { + t.Errorf("UrgencyFromHints = %q, want %q", got, c.want) + } + }) + } +} + +func TestEffectiveTimeoutMS(t *testing.T) { + cases := []struct { + name string + expire int32 + u Urgency + want int64 + }{ + {"zero uses low default", 0, Low, 10_000}, + {"zero uses normal default", 0, Normal, 10_000}, + {"zero critical never", 0, Critical, 0}, + {"minus one never", -1, Normal, 0}, + {"explicit wins", 3_000, Normal, 3_000}, + {"sub second exact", 1, Normal, 1}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if got := EffectiveTimeoutMS(c.expire, c.u); got != c.want { + t.Errorf("EffectiveTimeoutMS(%d, %q) = %d, want %d", c.expire, c.u, got, c.want) + } + }) + } +} + +func TestStackTagFromHints(t *testing.T) { + dunst := map[string]dbus.Variant{"x-dunst-stack-tag": dbus.MakeVariant("mail-a")} + danix := map[string]dbus.Variant{"x-danix-stack-tag": dbus.MakeVariant("mail-b")} + both := map[string]dbus.Variant{ + "x-dunst-stack-tag": dbus.MakeVariant("mail-a"), + "x-danix-stack-tag": dbus.MakeVariant("mail-b"), + } + if got := StackTagFromHints(dunst); got != "mail-a" { + t.Errorf("dunst tag = %q, want mail-a", got) + } + if got := StackTagFromHints(danix); got != "mail-b" { + t.Errorf("danix tag = %q, want mail-b", got) + } + if got := StackTagFromHints(both); got != "mail-a" { + t.Errorf("dunst wins when both present = %q, want mail-a", got) + } + if got := StackTagFromHints(map[string]dbus.Variant{}); got != "" { + t.Errorf("empty = %q, want empty", got) + } +} + +func TestParseActions(t *testing.T) { + got := ParseActions([]string{"default", "open", "other", "do the thing"}) + want := [][2]string{{"default", "open"}, {"other", "do the thing"}} + if len(got) != len(want) { + t.Fatalf("len = %d, want %d", len(got), len(want)) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("action %d = %v, want %v", i, got[i], want[i]) + } + } + if got := ParseActions(nil); len(got) != 0 { + t.Errorf("nil actions = %v, want empty", got) + } +} |
