diff options
Diffstat (limited to 'internal/notify/icons.go')
| -rw-r--r-- | internal/notify/icons.go | 158 |
1 files changed, 158 insertions, 0 deletions
diff --git a/internal/notify/icons.go b/internal/notify/icons.go new file mode 100644 index 0000000..b874581 --- /dev/null +++ b/internal/notify/icons.go @@ -0,0 +1,158 @@ +// 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 ( + "os" + "path/filepath" + "sort" + "strconv" + "strings" +) + +// IconThemeName reads the desktop's current icon theme: qt6ct is the truth on +// this desktop, then the GTK3 setting, then hicolor. A missing or empty value +// falls through. +func IconThemeName() string { + if v := iniValue(filepath.Join(homeDir(), ".config", "qt6ct", "qt6ct.conf"), "icon_theme"); v != "" { + return v + } + if v := iniValue(filepath.Join(homeDir(), ".config", "gtk-3.0", "settings.ini"), "gtk-icon-theme-name"); v != "" { + return v + } + return "hicolor" +} + +// ResolveIcon turns an app_icon or image-path value into a file path. A URI is +// trimmed, a path is returned unchanged, and a bare name is looked up in the +// icon theme. Nothing found is an empty string, which renders no image. +func ResolveIcon(value string) string { + if value == "" { + return "" + } + if strings.HasPrefix(value, "file://") { + return strings.TrimPrefix(value, "file://") + } + if strings.Contains(value, "/") { + return value + } + return lookupThemeIcon(value, IconThemeName()) +} + +// XDGIconDirs is the icon search path: the user's dir then each data dir. +func XDGIconDirs() []string { + home := os.Getenv("XDG_DATA_HOME") + if home == "" { + home = filepath.Join(homeDir(), ".local", "share") + } + dirs := []string{filepath.Join(home, "icons")} + for _, d := range filepath.SplitList(os.Getenv("XDG_DATA_DIRS")) { + if d != "" { + dirs = append(dirs, filepath.Join(d, "icons")) + } + } + if len(dirs) == 1 { + dirs = append(dirs, "/usr/local/share/icons", "/usr/share/icons") + } + return dirs +} + +// lookupThemeIcon searches the theme, then its Inherits chain, then hicolor. +func lookupThemeIcon(name, theme string) string { + seen := map[string]bool{} + for theme != "" && !seen[theme] { + seen[theme] = true + found, next := "", "" + for _, root := range XDGIconDirs() { + base := filepath.Join(root, theme) + if p := findInTheme(base, name); p != "" { + found = p + break + } + if next == "" { + next = inheritsOf(base) + } + } + if found != "" { + return found + } + theme = next + } + for _, root := range XDGIconDirs() { + if p := findInTheme(filepath.Join(root, "hicolor"), name); p != "" { + return p + } + } + return "" +} + +// findInTheme prefers scalable then the largest raster under apps/. +func findInTheme(base, name string) string { + sizes := []string{"scalable"} + matches, _ := filepath.Glob(filepath.Join(base, "apps", "[0-9]*")) + for _, m := range matches { + sizes = append(sizes, filepath.Base(m)) + } + numeric := sizes[1:] + sort.Slice(numeric, func(i, j int) bool { + a, _ := strconv.Atoi(strings.TrimSuffix(numeric[i], "@2x")) + b, _ := strconv.Atoi(strings.TrimSuffix(numeric[j], "@2x")) + return a > b + }) + for _, size := range sizes { + for _, ext := range []string{"svg", "png", "xpm"} { + p := filepath.Join(base, "apps", size, name+"."+ext) + if fileExists(p) { + return p + } + } + } + return "" +} + +func inheritsOf(base string) string { + v := iniValue(filepath.Join(base, "index.theme"), "Inherits") + if i := strings.IndexByte(v, ','); i >= 0 { + v = v[:i] + } + return strings.TrimSpace(v) +} + +func iniValue(path, key string) string { + data, err := os.ReadFile(path) + if err != nil { + return "" + } + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "#") || !strings.Contains(line, "=") { + continue + } + k, v, _ := strings.Cut(line, "=") + if strings.TrimSpace(k) == key { + return strings.TrimSpace(v) + } + } + return "" +} + +func homeDir() string { + if h := os.Getenv("HOME"); h != "" { + return h + } + return os.TempDir() +} + +func fileExists(path string) bool { + info, err := os.Stat(path) + return err == nil && !info.IsDir() +} |
