228 lines
4.9 KiB
Go
228 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
"image/draw"
|
|
_ "image/png"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
func iconThemeDirs() []string {
|
|
dirs := []string{}
|
|
|
|
if home := os.Getenv("HOME"); home != "" {
|
|
dirs = append(dirs, filepath.Join(home, ".local/share/icons"))
|
|
}
|
|
|
|
xdgDirs := os.Getenv("XDG_DATA_DIRS")
|
|
if xdgDirs == "" {
|
|
xdgDirs = "/usr/local/share:/usr/share"
|
|
}
|
|
for _, d := range filepath.SplitList(xdgDirs) {
|
|
dirs = append(dirs, filepath.Join(d, "icons"))
|
|
}
|
|
|
|
return uniquePaths(dirs)
|
|
}
|
|
|
|
func uniquePaths(paths []string) []string {
|
|
seen := make(map[string]bool)
|
|
res := make([]string, 0, len(paths))
|
|
for _, p := range paths {
|
|
if !seen[p] {
|
|
seen[p] = true
|
|
res = append(res, p)
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
func preferredThemes() []string {
|
|
theme := detectGtkTheme()
|
|
if theme != "" {
|
|
return []string{theme, "hicolor", "Adwaita", "Papirus", "Humanity", "breeze", "gnome"}
|
|
}
|
|
return []string{"hicolor", "Adwaita", "Papirus", "Humanity", "breeze", "gnome"}
|
|
}
|
|
|
|
func detectGtkTheme() string {
|
|
data, err := os.ReadFile(filepath.Join(os.Getenv("HOME"), ".config/gtk-4.0/settings.ini"))
|
|
if err != nil {
|
|
data, err = os.ReadFile(filepath.Join(os.Getenv("HOME"), ".config/gtk-3.0/settings.ini"))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
}
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "gtk-icon-theme-name=") {
|
|
return strings.TrimSpace(line[len("gtk-icon-theme-name="):])
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func sizeDirs(target int) []string {
|
|
sizes := []int{}
|
|
|
|
for _, base := range []int{16, 22, 24, 32, 48, 64, 72, 96, 128, 192, 256} {
|
|
sizes = append(sizes, base)
|
|
}
|
|
|
|
sort.Slice(sizes, func(i, j int) bool {
|
|
di := abs(sizes[i] - target)
|
|
dj := abs(sizes[j] - target)
|
|
if di != dj {
|
|
return di < dj
|
|
}
|
|
return sizes[i] > sizes[j]
|
|
})
|
|
|
|
seen := make(map[int]bool)
|
|
res := make([]string, 0, len(sizes))
|
|
for _, s := range sizes {
|
|
if seen[s] {
|
|
continue
|
|
}
|
|
seen[s] = true
|
|
res = append(res, fmt.Sprintf("%dx%d", s, s))
|
|
}
|
|
res = append(res, "scalable")
|
|
return res
|
|
}
|
|
|
|
func abs(x int) int {
|
|
if x < 0 {
|
|
return -x
|
|
}
|
|
return x
|
|
}
|
|
|
|
func iconCategories() []string {
|
|
return []string{"actions", "apps", "categories", "devices", "emblems", "mimetypes", "places", "status"}
|
|
}
|
|
|
|
func findSystemIcon(name string, targetSize int) (string, error) {
|
|
themes := preferredThemes()
|
|
dirs := iconThemeDirs()
|
|
sDirs := sizeDirs(targetSize)
|
|
cats := iconCategories()
|
|
|
|
for _, base := range dirs {
|
|
for _, theme := range themes {
|
|
themeDir := filepath.Join(base, theme)
|
|
if _, err := os.Stat(themeDir); os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
|
|
for _, sd := range sDirs {
|
|
for _, cat := range cats {
|
|
for _, ext := range []string{"png", "xpm"} {
|
|
p := filepath.Join(themeDir, sd, cat, name+"."+ext)
|
|
if fi, err := os.Stat(p); err == nil && !fi.IsDir() {
|
|
return p, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, sd := range sDirs {
|
|
for _, cat := range cats {
|
|
p := filepath.Join(themeDir, sd, cat, name+".svg")
|
|
if fi, err := os.Stat(p); err == nil && !fi.IsDir() {
|
|
return p, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("system icon %q not found", name)
|
|
}
|
|
|
|
func svgToPNG(svgPath string, targetSize int, scale float64) (image.Image, error) {
|
|
if scale <= 0 {
|
|
scale = 0.55
|
|
}
|
|
|
|
renderSize := int(float64(targetSize) * scale)
|
|
if renderSize > targetSize {
|
|
renderSize = targetSize
|
|
}
|
|
if renderSize < 1 {
|
|
renderSize = 1
|
|
}
|
|
|
|
cmd := exec.Command("rsvg-convert",
|
|
"-w", strconv.Itoa(renderSize),
|
|
"-h", strconv.Itoa(renderSize),
|
|
"-f", "png",
|
|
svgPath,
|
|
)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("rsvg-convert: %w", err)
|
|
}
|
|
img, _, err := image.Decode(bytes.NewReader(out))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode converted svg: %w", err)
|
|
}
|
|
|
|
if renderSize < targetSize {
|
|
canvas := image.NewRGBA(image.Rect(0, 0, targetSize, targetSize))
|
|
offX := (targetSize - renderSize) / 2
|
|
offY := (targetSize - renderSize) / 2
|
|
draw.Draw(canvas, image.Rect(offX, offY, offX+renderSize, offY+renderSize), img, image.Point{}, draw.Over)
|
|
img = canvas
|
|
}
|
|
|
|
return img, nil
|
|
}
|
|
|
|
var iconSizeCache sync.Map
|
|
|
|
func loadSystemIcon(name string, targetSize int) (string, error) {
|
|
type cacheKey struct {
|
|
name string
|
|
size int
|
|
}
|
|
key := cacheKey{name, targetSize}
|
|
if cached, ok := iconSizeCache.Load(key); ok {
|
|
return cached.(string), nil
|
|
}
|
|
path, err := findSystemIcon(name, targetSize)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
iconSizeCache.Store(key, path)
|
|
return path, nil
|
|
}
|
|
|
|
func isSystemIconRef(path string) bool {
|
|
return strings.HasPrefix(path, "@")
|
|
}
|
|
|
|
func systemIconName(path string) string {
|
|
return strings.TrimPrefix(path, "@")
|
|
}
|
|
|
|
func parseSizeDir(dirName string) (int, bool) {
|
|
parts := strings.SplitN(dirName, "x", 2)
|
|
if len(parts) != 2 {
|
|
return 0, false
|
|
}
|
|
s, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return s, true
|
|
}
|