Files
syncwarden/cmd/icongen/main.go
Axel Meyer cdeae01398
All checks were successful
Release / build (push) Successful in 2m47s
Add shield icon: medieval round shield with Syncthing motif warpaint
Redesign tray/app icon as a wooden round shield with iron rim, rivets,
wood grain, and the Syncthing network motif (ring + 3 nodes + spokes)
painted in the state color. Dark shadow outline for wood contrast.
Inspired by Stammtisch hero icon style (warm, illustrated wood tones).

- Dynamic tray icon changes color per state (green/blue/gray/red)
- Static assets: PNG at 7 sizes + multi-size .ico + preview sheet
- Icon generator utility (cmd/icongen)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 22:35:40 +01:00

166 lines
3.9 KiB
Go

// Generates static icon assets for the SyncWarden project.
// Run: go run ./cmd/icongen/
package main
import (
"bytes"
"encoding/binary"
"fmt"
"image/png"
"os"
"path/filepath"
"github.com/fogleman/gg"
"git.davoryn.de/calic/syncwarden/internal/icons"
)
func main() {
dir := filepath.Join("assets")
os.MkdirAll(dir, 0755)
// Generate PNGs at standard icon sizes (idle/green as canonical)
sizes := []int{16, 32, 48, 64, 128, 256, 512}
pngEntries := make(map[int][]byte) // size → PNG data
for _, sz := range sizes {
data, err := icons.RenderPNG(icons.StateIdle, sz)
if err != nil {
fmt.Fprintf(os.Stderr, "error rendering %d: %v\n", sz, err)
continue
}
pngEntries[sz] = data
fname := fmt.Sprintf("icon-%d.png", sz)
fpath := filepath.Join(dir, fname)
if err := os.WriteFile(fpath, data, 0644); err != nil {
fmt.Fprintf(os.Stderr, "error writing %s: %v\n", fpath, err)
continue
}
fmt.Printf(" %s (%d bytes)\n", fpath, len(data))
}
// Generate multi-size .ico (16, 32, 48, 256)
icoSizes := []int{16, 32, 48, 256}
icoData := buildMultiICO(icoSizes, pngEntries)
icoPath := filepath.Join(dir, "syncwarden.ico")
if err := os.WriteFile(icoPath, icoData, 0644); err != nil {
fmt.Fprintf(os.Stderr, "error writing ico: %v\n", err)
} else {
fmt.Printf(" %s (%d bytes)\n", icoPath, len(icoData))
}
// Generate a large composited preview showing all states
previewPath := filepath.Join(dir, "icon-preview.png")
generatePreview(previewPath)
fmt.Println("Done.")
}
func buildMultiICO(sizes []int, pngs map[int][]byte) []byte {
count := 0
for _, sz := range sizes {
if _, ok := pngs[sz]; ok {
count++
}
}
const headerSize = 6
const entrySize = 16
dataOffset := headerSize + entrySize*count
buf := new(bytes.Buffer)
// ICONDIR header
binary.Write(buf, binary.LittleEndian, uint16(0)) // Reserved
binary.Write(buf, binary.LittleEndian, uint16(1)) // Type: icon
binary.Write(buf, binary.LittleEndian, uint16(count)) // Count
// Calculate offsets
offset := dataOffset
type entry struct {
w, h int
data []byte
}
entries := make([]entry, 0, count)
for _, sz := range sizes {
data, ok := pngs[sz]
if !ok {
continue
}
entries = append(entries, entry{sz, sz, data})
}
// Write ICONDIRENTRY records
for _, e := range entries {
w := byte(e.w)
if e.w >= 256 {
w = 0
}
h := byte(e.h)
if e.h >= 256 {
h = 0
}
buf.WriteByte(w)
buf.WriteByte(h)
buf.WriteByte(0) // ColorCount
buf.WriteByte(0) // Reserved
binary.Write(buf, binary.LittleEndian, uint16(1)) // Planes
binary.Write(buf, binary.LittleEndian, uint16(32)) // BitCount
binary.Write(buf, binary.LittleEndian, uint32(len(e.data))) // BytesInRes
binary.Write(buf, binary.LittleEndian, uint32(offset)) // ImageOffset
offset += len(e.data)
}
// Write image data
for _, e := range entries {
buf.Write(e.data)
}
return buf.Bytes()
}
func generatePreview(path string) {
states := []struct {
s icons.State
name string
}{
{icons.StateIdle, "Idle"},
{icons.StateSyncing, "Syncing"},
{icons.StatePaused, "Paused"},
{icons.StateError, "Error"},
{icons.StateDisconnected, "Disconnected"},
}
sz := 128
pad := 20
w := len(states)*(sz+pad) + pad
h := sz + pad*2 + 20
dc := gg.NewContext(w, h)
dc.SetHexColor("#1a1a2e")
dc.DrawRectangle(0, 0, float64(w), float64(h))
dc.Fill()
for i, st := range states {
// Render at 128px
data, err := icons.RenderPNG(st.s, sz)
if err != nil {
continue
}
img, err := png.Decode(bytes.NewReader(data))
if err != nil {
continue
}
x := pad + i*(sz+pad)
dc.DrawImage(img, x, pad)
// Label
dc.SetHexColor("#cccccc")
dc.DrawStringAnchored(st.name, float64(x)+float64(sz)/2, float64(pad+sz+12), 0.5, 0.5)
}
dc.SavePNG(path)
fmt.Printf(" %s\n", path)
}