// Command icon-gen renders the theta-agent tray icon set (Red/Yellow/Green/ // Blue state badges) and writes: // // - /icons.go — the Go source with the embedded 256x256 PNG byte // arrays the tray binary compiles in (gofmt-formatted). // - /icon-*.png — a preview of each color, for eyeballing. // - — the multi-size Windows .ico (16..256) of the Blue badge, // used as the installer's own icon, the Start menu shortcut icon, and the // uninstaller display icon. // // Design: a rounded-square badge in the state color with a subtle vertical // gradient, and a white theta (ring + horizontal bar) glyph knocked out of it. // Rendered at 256px with 4x4 supersampling so the edges are crisp at every // downscaled size. Pure stdlib; run with: // // go run ./cmd/icon-gen cmd/theta-agent-tray installer/windows/theta-agent.ico // // The generated icons.go and theta-agent.ico must not be edited by hand. package main import ( "bytes" "encoding/binary" "fmt" "go/format" "image" "image/color" "image/png" "math" "os" "path/filepath" "strings" ) const size = 256 type badgeColor struct { name string base color.RGBA } var palette = []badgeColor{ {"Red", color.RGBA{0xEF, 0x44, 0x44, 0xFF}}, {"Yellow", color.RGBA{0xEA, 0xB3, 0x08, 0xFF}}, {"Green", color.RGBA{0x22, 0xC5, 0x5E, 0xFF}}, {"Blue", color.RGBA{0x3B, 0x82, 0xF6, 0xFF}}, } func main() { if len(os.Args) != 3 { fmt.Fprintln(os.Stderr, "usage: go run ./cmd/icon-gen ") os.Exit(2) } dir := os.Args[1] icoPath := os.Args[2] // The Blue badge doubles as the product/app icon (Start menu shortcut, // installer, uninstaller). var blue *image.RGBA var src strings.Builder src.WriteString("// Code generated by cmd/icon-gen; DO NOT EDIT.\n") src.WriteString("\npackage main\n\n") src.WriteString("// Theta Agent tray state badges, 256x256 PNG.\n") src.WriteString("var (\n") for _, c := range palette { img := renderBadge(c.base) preview := filepath.Join(dir, "icon-"+strings.ToLower(c.name)+".png") writePNG(img, preview) if c.name == "Blue" { blue = img } var buf bytes.Buffer if err := png.Encode(&buf, img); err != nil { fmt.Fprintln(os.Stderr, "encode:", err) os.Exit(1) } src.WriteString(fmt.Sprintf("\ticon%s = %s\n", c.name, goBytes(buf.Bytes()))) } src.WriteString(")\n") out := filepath.Join(dir, "icons.go") formatted, err := format.Source([]byte(src.String())) if err != nil { fmt.Fprintln(os.Stderr, "format:", err) os.Exit(1) } if err := os.WriteFile(out, formatted, 0644); err != nil { fmt.Fprintln(os.Stderr, "write:", err) os.Exit(1) } fmt.Println("wrote", out) if err := writeICO(blue, icoPath); err != nil { fmt.Fprintln(os.Stderr, "ico:", err) os.Exit(1) } fmt.Println("wrote", icoPath) } // renderBadge draws the state badge at `size` with 4x4 supersampling. func renderBadge(base color.RGBA) *image.RGBA { img := image.NewRGBA(image.Rect(0, 0, size, size)) white := color.RGBA{0xFF, 0xFF, 0xFF, 0xFF} top := mix(base, white, 0.16) bottom := mix(base, color.RGBA{0, 0, 0, 0xFF}, 0.20) const margin = 8.0 const corner = 52.0 cx, cy := size/2.0, size/2.0 half := (float64(size) - 2*margin) / 2 // Theta glyph geometry (ring + horizontal bar, bar extends past the ring). const ringOuter = 80.0 const ringInner = 56.0 const barHalf = 12.0 const barLen = 86.0 const ss = 4 // supersample factor for py := 0; py < size; py++ { for px := 0; px < size; px++ { var accR, accG, accB, accA float64 for sy := 0; sy < ss; sy++ { for sx := 0; sx < ss; sx++ { x := float64(px) + (float64(sx)+0.5)/ss y := float64(py) + (float64(sy)+0.5)/ss if !inRoundedRect(x, y, cx, cy, half, corner) { continue // transparent outside the badge } t := clamp01((y - (cy - half)) / (2 * half)) col := lerp(top, bottom, t) if inTheta(x, y, cx, cy, ringOuter, ringInner, barHalf, barLen) { col = white } accR += float64(col.R) accG += float64(col.G) accB += float64(col.B) accA += 255 } } n := float64(ss * ss) img.SetRGBA(px, py, color.RGBA{ R: uint8(accR / n), G: uint8(accG / n), B: uint8(accB / n), A: uint8(accA / n), }) } } return img } func inRoundedRect(x, y, cx, cy, half, r float64) bool { dx := math.Abs(x-cx) - (half - r) dy := math.Abs(y-cy) - (half - r) if dx <= 0 && dy <= 0 { return true } return dx*dx+dy*dy <= r*r } func inTheta(x, y, cx, cy, ringOuter, ringInner, barHalf, barLen float64) bool { d := math.Hypot(x-cx, y-cy) inRing := d >= ringInner && d <= ringOuter inBar := math.Abs(y-cy) <= barHalf && math.Abs(x-cx) <= barLen return inRing || inBar } func mix(a, b color.RGBA, t float64) color.RGBA { return color.RGBA{ R: uint8(float64(a.R) + (float64(b.R)-float64(a.R))*t), G: uint8(float64(a.G) + (float64(b.G)-float64(a.G))*t), B: uint8(float64(a.B) + (float64(b.B)-float64(a.B))*t), A: 0xFF, } } func lerp(a, b color.RGBA, t float64) color.RGBA { return mix(a, b, t) } func clamp01(v float64) float64 { if v < 0 { return 0 } if v > 1 { return 1 } return v } func writePNG(img *image.RGBA, path string) { f, err := os.Create(path) if err != nil { fmt.Fprintln(os.Stderr, "create:", err) return } defer f.Close() if err := png.Encode(f, img); err != nil { fmt.Fprintln(os.Stderr, "encode:", err) } } // icoSizes are the entries embedded in the .ico. The 256px source is an // integer multiple of each, so every entry is an exact box-filter downscale. var icoSizes = []int{16, 24, 32, 48, 64, 128, 256} // writeICO writes a multi-size Windows .ico (classic BMP XOR+AND entries, the // format LoadImage has always supported) from the 256px source image. func writeICO(src *image.RGBA, path string) error { sizes := icoSizes var dir bytes.Buffer var payload bytes.Buffer offset := 6 + len(sizes)*16 dir.Write([]byte{0, 0, 1, 0, byte(len(sizes)), 0}) // ICONDIR for _, s := range sizes { bmp := toDIB(scaleBox(src, s)) w, h := byte(s), byte(s) if s >= 256 { w, h = 0, 0 } dir.Write([]byte{w, h, 0, 0, 1, 0, 32, 0}) dir.Write(u32le(len(bmp))) dir.Write(u32le(offset + payload.Len())) payload.Write(bmp) } f, err := os.Create(path) if err != nil { return err } defer f.Close() if _, err := f.Write(dir.Bytes()); err != nil { return err } _, err = f.Write(payload.Bytes()) return err } // scaleBox resizes src to w x w with an exact box (area-average) filter. Only // correct when src's dimensions are an integer multiple of w — 256 is for // every icoSizes entry — so no interpolation blur is introduced. func scaleBox(src *image.RGBA, w int) *image.RGBA { b := src.Bounds() sw, sh := b.Dx(), b.Dy() fx := sw / w fy := sh / w dst := image.NewRGBA(image.Rect(0, 0, w, w)) for y := 0; y < w; y++ { for x := 0; x < w; x++ { var r, g, bl, a int64 for sy := 0; sy < fy; sy++ { yy := b.Min.Y + y*fy + sy for sx := 0; sx < fx; sx++ { xx := b.Min.X + x*fx + sx cr, cg, cb, ca := src.At(xx, yy).RGBA() r += int64(cr >> 8) g += int64(cg >> 8) bl += int64(cb >> 8) a += int64(ca >> 8) } } n := int64(fx * fy) dst.SetRGBA(x, y, color.RGBA{ R: uint8(r / n), G: uint8(g / n), B: uint8(bl / n), A: uint8(a / n), }) } } return dst } // toDIB encodes an image as a 32-bit bottom-up DIB with an all-transparent // AND mask — the classic icon bitmap entry. func toDIB(img *image.RGBA) []byte { b := img.Bounds() w, h := b.Dx(), b.Dy() andRow := ((w + 31) / 32) * 4 dib := make([]byte, 0, 40+w*h*4+andRow*h) dib = append(dib, u32le(40)...) // biSize dib = append(dib, u32le(w)...) // biWidth dib = append(dib, u32le(h*2)...) // biHeight (XOR + AND) dib = append(dib, u16le(1)...) // biPlanes dib = append(dib, u16le(32)...) // biBitCount dib = append(dib, 0, 0, 0, 0) // biCompression = 0 (BI_RGB) dib = append(dib, u32le(w*h*4+andRow*h)...) dib = append(dib, make([]byte, 16)...) // remaining header fields for y := 0; y < h; y++ { srcY := b.Min.Y + (h - 1 - y) // DIB rows are bottom-up for x := 0; x < w; x++ { r, g, bl, a := img.At(b.Min.X+x, srcY).RGBA() dib = append(dib, byte(bl>>8), byte(g>>8), byte(r>>8), byte(a>>8)) } } dib = append(dib, make([]byte, andRow*h)...) // AND mask: all zero return dib } func u16le(v int) []byte { return []byte{byte(v), byte(v >> 8)} } func u32le(v int) []byte { var b [4]byte binary.LittleEndian.PutUint32(b[:], uint32(v)) return b[:] } func goBytes(b []byte) string { var sb strings.Builder sb.WriteString("[]byte{") for i, x := range b { if i%12 == 0 { sb.WriteString("\n\t\t") } sb.WriteString(fmt.Sprintf("0x%02x, ", x)) } sb.WriteString("\n\t}") return sb.String() }