refactor: restructure to standard Go project layout (cmd/ + internal/)
- Move entry point to cmd/streamdeck-lets-go/ - Split package main into internal packages: - internal/deck: hardware control (Deck, events, brightness) - internal/render: key rendering, icons, fonts, PageManager - internal/web: HTTP API + embedded SPA - internal/daemon: event loop, autoswitch, screensaver, actions - Add ConfigDir() to internal/config - Export cross-package API (Deck, PageManager, WebServer, etc.) - Move dist/arch/ → packaging/ with updated PKGBUILD/.SRCINFO - Add Makefile (build, test, vet, fmt, install) - Update README with new build commands and project structure - Delete unused media.go stub
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
package render
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user