// 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 ( "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() }