From 7a4d76c5db3e7668796486a0a4d9db583f89b8e5 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 19 Oct 2026 09:52:43 +0200 Subject: feat(notify): resolve sound-file, sound-name and suppress-sound hints sound-name is looked up in a sound theme, its Inherits chain and freedesktop, with the naming spec's trailing-component fallback. A sound-file must be an absolute path to an existing file, else it is treated as absent so a sound-name in the same notification still applies. Co-Authored-By: Claude Opus 5.5 --- internal/notify/sounds.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 internal/notify/sounds.go (limited to 'internal/notify/sounds.go') diff --git a/internal/notify/sounds.go b/internal/notify/sounds.go new file mode 100644 index 0000000..85f1ecc --- /dev/null +++ b/internal/notify/sounds.go @@ -0,0 +1,98 @@ +// Copyright (C) 2026 Danilo M. +// +// 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 ( + "log" + "path/filepath" + "strings" + + "github.com/godbus/dbus/v5" +) + +// XDGSoundDirs is the sound theme search path: the user's dir then each data +// dir, the same roots as XDGIconDirs. +func XDGSoundDirs() []string { + dirs := XDGIconDirs() + for i, d := range dirs { + dirs[i] = filepath.Join(filepath.Dir(d), "sounds") + } + return dirs +} + +var soundExts = []string{".oga", ".ogg", ".wav"} + +// LookupSound resolves a sound naming spec name: the theme, its Inherits +// chain, then freedesktop, each searched across every root; within that, the +// full name first and then with trailing -components dropped, as the spec's +// fallback (message-new-email, message-new, message). "" when nothing matches. +func LookupSound(name, theme string) string { + var chain []string + seen := map[string]bool{} + for t := theme; t != "" && !seen[t]; { + seen[t] = true + chain = append(chain, t) + next := "" + for _, root := range XDGSoundDirs() { + if v := inheritsOf(filepath.Join(root, t)); v != "" { + next = v + break + } + } + t = next + } + if !seen["freedesktop"] { + chain = append(chain, "freedesktop") + } + for n := name; n != ""; { + for _, t := range chain { + for _, root := range XDGSoundDirs() { + for _, ext := range soundExts { + p := filepath.Join(root, t, "stereo", n+ext) + if fileExists(p) { + return p + } + } + } + } + i := strings.LastIndexByte(n, '-') + if i < 0 { + break + } + n = n[:i] + } + return "" +} + +// SoundFromHints reads the three freedesktop sound hints. sound-file wins over +// sound-name, but only as an absolute path to an existing file: anything else +// is logged and treated as absent, so the name still gets its turn. silent is +// suppress-sound, published as is; the renderer decides what it means. +func SoundFromHints(hints map[string]dbus.Variant, theme string) (sound string, silent bool) { + if v, ok := hints["suppress-sound"]; ok { + silent, _ = v.Value().(bool) + } + if v, ok := hints["sound-file"]; ok { + if p, _ := v.Value().(string); p != "" { + if filepath.IsAbs(p) && fileExists(p) { + return p, silent + } + log.Printf("notifyd: sound-file %q is not an absolute path to a file", p) + } + } + if v, ok := hints["sound-name"]; ok { + if n, _ := v.Value().(string); n != "" { + return LookupSound(n, theme), silent + } + } + return "", silent +} -- cgit v1.2.3