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