From 2e77cf0a115b5159153f6db1635df61cb319cf15 Mon Sep 17 00:00:00 2001 From: m3ta-chiron Date: Sun, 25 Oct 2026 12:43:14 +0100 Subject: [PATCH] feat(icon): app icon with embedded PNG, Windows .ico and resource objects One SVG source (assets/icon/talk.svg: speech bubble with level bars). assets/icon/gen.go renders it with inkscape into the embedded PNG (internal/icon, installed next to the config for notifications), the multi-size talk.ico and the COFF .rsrc objects for windows/amd64 and arm64 that give talk.exe its icon. The generator writes the resource objects itself, so no resource compiler is needed; everything it produces is committed and a plain go build picks it up. Co-Authored-By: Claude Fable 5.1 --- assets/icon/gen.go | 299 +++++++++++++++++++++++++++++++ assets/icon/talk.ico | Bin 0 -> 114727 bytes assets/icon/talk.svg | 8 + cmd/talk/rsrc_windows_amd64.syso | Bin 0 -> 115442 bytes cmd/talk/rsrc_windows_arm64.syso | Bin 0 -> 115442 bytes internal/icon/icon.go | 39 ++++ internal/icon/icon_test.go | 49 +++++ internal/icon/talk.png | Bin 0 -> 4065 bytes 8 files changed, 395 insertions(+) create mode 100644 assets/icon/gen.go create mode 100644 assets/icon/talk.ico create mode 100644 assets/icon/talk.svg create mode 100644 cmd/talk/rsrc_windows_amd64.syso create mode 100644 cmd/talk/rsrc_windows_arm64.syso create mode 100644 internal/icon/icon.go create mode 100644 internal/icon/icon_test.go create mode 100644 internal/icon/talk.png diff --git a/assets/icon/gen.go b/assets/icon/gen.go new file mode 100644 index 0000000..3de99d0 --- /dev/null +++ b/assets/icon/gen.go @@ -0,0 +1,299 @@ +//go:build ignore + +// gen renders the talk icon from assets/icon/talk.svg into everything the +// build embeds, so no resource compiler is needed: +// +// internal/icon/talk.png 256 px, embedded in the binary (toast +// and notification icon) +// assets/icon/talk.ico multi-size Windows icon +// cmd/talk/rsrc_windows_.syso COFF resource object with the icon; +// go build links it into talk.exe +// +// Run from the repository root (needs inkscape in PATH): +// +// go run assets/icon/gen.go +package main + +import ( + "bytes" + "encoding/binary" + "fmt" + "image/color" + "image/png" + "os" + "os/exec" + "path/filepath" +) + +// sizes are the icon sizes Windows asks for (Explorer, taskbar, Alt-Tab, +// toast header); 256 is stored as PNG, the rest as plain bitmaps. +var sizes = []int{16, 20, 24, 32, 40, 48, 64, 128, 256} + +const svgPath = "assets/icon/talk.svg" + +// iconImage is one size of the icon in the form ICO files and RT_ICON +// resources store it. +type iconImage struct { + size int + data []byte // PNG (256) or BITMAPINFOHEADER + pixels + mask +} + +func main() { + if _, err := os.Stat("go.mod"); err != nil { + fail("run from the repository root: %v", err) + } + tmp, err := os.MkdirTemp("", "talk-icon-") + if err != nil { + fail("%v", err) + } + defer os.RemoveAll(tmp) + + var images []iconImage + for _, s := range sizes { + out := filepath.Join(tmp, fmt.Sprintf("%d.png", s)) + cmd := exec.Command("inkscape", svgPath, "-w", fmt.Sprint(s), "-h", fmt.Sprint(s), "-o", out) + if b, err := cmd.CombinedOutput(); err != nil { + fail("inkscape %d px: %v: %s", s, err, b) + } + b, err := os.ReadFile(out) + if err != nil { + fail("%v", err) + } + if s == 256 { + write("internal/icon/talk.png", b) + images = append(images, iconImage{s, b}) + continue + } + dib, err := toDIB(b) + if err != nil { + fail("%d px: %v", s, err) + } + images = append(images, iconImage{s, dib}) + } + write("assets/icon/talk.ico", buildICO(images)) + for arch, machine := range map[string]uint16{"amd64": 0x8664, "arm64": 0xAA64} { + write("cmd/talk/rsrc_windows_"+arch+".syso", buildSyso(machine, images)) + } +} + +func write(path string, b []byte) { + if err := os.WriteFile(path, b, 0o644); err != nil { + fail("%v", err) + } + fmt.Printf("wrote %s (%d bytes)\n", path, len(b)) +} + +func fail(format string, a ...any) { + fmt.Fprintf(os.Stderr, "gen: "+format+"\n", a...) + os.Exit(1) +} + +// toDIB converts a PNG into the classic icon bitmap: BITMAPINFOHEADER with +// doubled height, 32-bit BGRA rows bottom-up, then the 1-bit AND mask. +func toDIB(pngData []byte) ([]byte, error) { + img, err := png.Decode(bytes.NewReader(pngData)) + if err != nil { + return nil, err + } + b := img.Bounds() + w, h := b.Dx(), b.Dy() + maskRow := ((w + 31) / 32) * 4 + var buf bytes.Buffer + hdr := []uint32{40, uint32(w), uint32(2 * h)} + binary.Write(&buf, binary.LittleEndian, hdr) + binary.Write(&buf, binary.LittleEndian, []uint16{1, 32}) // planes, bit count + binary.Write(&buf, binary.LittleEndian, []uint32{0, uint32(w*h*4 + maskRow*h), 0, 0, 0, 0}) + for y := h - 1; y >= 0; y-- { + for x := 0; x < w; x++ { + c := color.NRGBAModel.Convert(img.At(b.Min.X+x, b.Min.Y+y)).(color.NRGBA) + buf.Write([]byte{c.B, c.G, c.R, c.A}) + } + } + for y := h - 1; y >= 0; y-- { + row := make([]byte, maskRow) + for x := 0; x < w; x++ { + if _, _, _, a := img.At(b.Min.X+x, b.Min.Y+y).RGBA(); a == 0 { + row[x/8] |= 0x80 >> (x % 8) + } + } + buf.Write(row) + } + return buf.Bytes(), nil +} + +// dirEntry writes the 12 bytes ICONDIRENTRY and GRPICONDIRENTRY share. +func dirEntry(buf *bytes.Buffer, im iconImage) { + dim := byte(im.size) + if im.size >= 256 { + dim = 0 + } + buf.Write([]byte{dim, dim, 0, 0}) + binary.Write(buf, binary.LittleEndian, []uint16{1, 32}) + binary.Write(buf, binary.LittleEndian, uint32(len(im.data))) +} + +// buildICO lays out an .ico file: ICONDIR, one ICONDIRENTRY per image with +// the file offset of its data, then the images. +func buildICO(images []iconImage) []byte { + var buf bytes.Buffer + binary.Write(&buf, binary.LittleEndian, []uint16{0, 1, uint16(len(images))}) + offset := 6 + 16*len(images) + for _, im := range images { + dirEntry(&buf, im) + binary.Write(&buf, binary.LittleEndian, uint32(offset)) + offset += len(im.data) + } + for _, im := range images { + buf.Write(im.data) + } + return buf.Bytes() +} + +// buildGroupIcon is the RT_GROUP_ICON payload: like ICONDIR, but each entry +// ends with the RT_ICON resource id instead of a file offset. +func buildGroupIcon(images []iconImage) []byte { + var buf bytes.Buffer + binary.Write(&buf, binary.LittleEndian, []uint16{0, 1, uint16(len(images))}) + for i, im := range images { + dirEntry(&buf, im) + binary.Write(&buf, binary.LittleEndian, uint16(i+1)) + } + return buf.Bytes() +} + +const ( + rtIcon = 3 + rtGroupIcon = 14 + langNeutral = 0x0409 +) + +// resource is one leaf of the .rsrc tree: type/id/language and its data. +type resource struct { + typ, id uint32 + data []byte +} + +// buildSyso writes a COFF object with a single .rsrc section the Go linker +// merges into the PE resource table: the directory tree (type → id → +// language → data entry), the data, one ADDR32NB relocation per data entry +// (the linker turns the section offsets into image RVAs), and the section +// symbol the relocations refer to. +func buildSyso(machine uint16, images []iconImage) []byte { + var res []resource + for i, im := range images { + res = append(res, resource{rtIcon, uint32(i + 1), im.data}) + } + res = append(res, resource{rtGroupIcon, 1, buildGroupIcon(images)}) + + // Layout: root dir, the two type dirs, one language dir per resource, + // one data entry per resource, then the data. + const dirHdr, dirEnt, dataEnt = 16, 8, 16 + types := []uint32{rtIcon, rtGroupIcon} + byType := map[uint32][]resource{} + for _, r := range res { + byType[r.typ] = append(byType[r.typ], r) + } + rootOff := uint32(0) + typeDirOff := map[uint32]uint32{} + off := uint32(dirHdr + dirEnt*len(types)) + for _, t := range types { + typeDirOff[t] = off + off += uint32(dirHdr + dirEnt*len(byType[t])) + } + langDirOff := make([]uint32, len(res)) + for i := range res { + langDirOff[i] = off + off += dirHdr + dirEnt + } + dataEntOff := make([]uint32, len(res)) + for i := range res { + dataEntOff[i] = off + off += dataEnt + } + dataOff := make([]uint32, len(res)) + for i, r := range res { + off = (off + 7) &^ 7 + dataOff[i] = off + off += uint32(len(r.data)) + } + sectionSize := off + + var sec bytes.Buffer + le := binary.LittleEndian + dir := func(entries int) { + binary.Write(&sec, le, []uint32{0, 0}) + binary.Write(&sec, le, []uint16{0, 0, 0, uint16(entries)}) + } + sub := func(id, target uint32) { binary.Write(&sec, le, []uint32{id, target | 0x80000000}) } + leaf := func(id, target uint32) { binary.Write(&sec, le, []uint32{id, target}) } + index := func(r resource) int { + for i := range res { + if res[i].typ == r.typ && res[i].id == r.id { + return i + } + } + panic("unreachable") + } + _ = rootOff + dir(len(types)) + for _, t := range types { + sub(t, typeDirOff[t]) + } + for _, t := range types { + dir(len(byType[t])) + for _, r := range byType[t] { + sub(r.id, langDirOff[index(r)]) + } + } + for i := range res { + dir(1) + leaf(langNeutral, dataEntOff[i]) + } + var relocs []uint32 + for i, r := range res { + relocs = append(relocs, uint32(sec.Len())) + binary.Write(&sec, le, []uint32{dataOff[i], uint32(len(r.data)), 0, 0}) + } + for i, r := range res { + sec.Write(make([]byte, int(dataOff[i])-sec.Len())) + sec.Write(r.data) + } + if uint32(sec.Len()) != sectionSize { + fail("section layout mismatch: %d != %d", sec.Len(), sectionSize) + } + + relocType := uint16(0x0003) // IMAGE_REL_AMD64_ADDR32NB + if machine == 0xAA64 { + relocType = 0x0002 // IMAGE_REL_ARM64_ADDR32NB + } + const fileHdr, secHdr, relocSize, symSize = 20, 40, 10, 18 + rawOff := uint32(fileHdr + secHdr) + relocOff := rawOff + sectionSize + symOff := relocOff + uint32(relocSize*len(relocs)) + + var out bytes.Buffer + binary.Write(&out, le, machine) + binary.Write(&out, le, uint16(1)) // sections + binary.Write(&out, le, uint32(0)) // timestamp + binary.Write(&out, le, symOff) // symbol table + binary.Write(&out, le, uint32(1)) // symbols + binary.Write(&out, le, uint16(0)) // optional header + binary.Write(&out, le, uint16(0x0104)) // 32-bit machine, line numbers stripped + name := [8]byte{'.', 'r', 's', 'r', 'c'} + out.Write(name[:]) + binary.Write(&out, le, []uint32{0, 0, sectionSize, rawOff, relocOff, 0}) + binary.Write(&out, le, []uint16{uint16(len(relocs)), 0}) + binary.Write(&out, le, uint32(0x40000040)) // initialized data, readable + out.Write(sec.Bytes()) + for _, at := range relocs { + binary.Write(&out, le, []uint32{at, 0}) + binary.Write(&out, le, relocType) + } + out.Write(name[:]) + binary.Write(&out, le, uint32(0)) // value + binary.Write(&out, le, int16(1)) // section number + binary.Write(&out, le, uint16(0)) // type + out.Write([]byte{3, 0}) // static, no aux symbols + binary.Write(&out, le, uint32(4)) // empty string table + return out.Bytes() +} diff --git a/assets/icon/talk.ico b/assets/icon/talk.ico new file mode 100644 index 0000000000000000000000000000000000000000..926feb165e136fc6cb7099b3dcf9a29f11c736a1 GIT binary patch literal 114727 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

M zpY`|L{A0FLj#%A_UsJKuBxKIg6*AroE?t{Nz{zUPQV%gEhKY}KTsH|Ybcm#DPHJUv zSYqKl5tP`DM|M3?U=Xl87o^hO&A{<~lFbsabj>*&dp;g?W!Uh+DOB$E$4zN}?|xq$ z#rNf_cHV_X?z$?P%Q|Fd2X6A;y`nY2BL6+}Z~n5cvVQkYSXuMMOuPGFl~~imJ+-?c zU+1l}cDCJ-fL*XPU3= zvaWmn#cjc%kJHpu{@zLT4e9^^m^|JpaMP0N0@=DzAoPd{$u4I6*Dox6Q~U0#LUD zR{iZT<2=KqQz!Ja*Tij7J(J$@FLl8?=EqU0t-s_Mm-JojZe@Oa>rkD~3&w=`zg|7q z@c5|y$ydh{?YTWz7l#)$AGP1+Uu*j1X|aYq^E|)Di`?B(*}AM%eS4lXI_Pey+C61s z`1iFjQa?_;U)b^H@dDfXJ6{(ZOWCJT*lSV1COgw7%`IedeAkcH2j*&vzu#VZbaezD z!~R1rrXEcCYYGzT|Fm&pz1*JP7q6YWlkm~%W0Js+j;q}}8y{`Cx3yd8zPa#C>xO>Y zAJdkDT03|1Q{RR2II^fTn7X}fP1fy>k^dyQ>ITz;CC^w-FZWDaJoVTSD*=Tl`KXyU z@2;Nq$Kagfk$?5~)`fa+=by4ESjE!zIgiSXWkH(OmhVpLt1e)+2nDrxrY$u+_QL$J z9QT8HH`Q0)K78+Pc;^l4hTzh*`@Y|=j_L|l`K;^t{+O-Yfr;g+pVz*d_;>%*B`5V& z*Dza5ow<9T-Ti_eudlt3(-U{h-BjIkrn&X8?(9cZC(HFZc^iVCirajsOn#G-UAlV7 z=f`sV1!Anqw&`ns#+Cn`v{q051CO&W%K^`{x@zaeZ|CM2rR`d??DPvR)f`FY_ft~0 zdb{0TbLN_ybSyj)60@e+ zWv#Z?tH~h~u4unxR(`81{dt<^>ceTj<4ZqIy|?2^34=#a-NCL~|M*wjGd!uIvnFbj z-MhvGWvP$l7*>4>TQ+Cs(#^FW-|b*;x^?W=&SMgphbwpf(w_OJppkDunat!fhxDTA zEL<3_utn9iG^siqy7)=ir-b38>*@Xrf($2JPWO9kVDXTMs&nb&Q?ShVDQL;0Aer$~ zkb4?~;o_(6b6zk!az5RELy+O)f~W3E8(2CdbnPd#@F`er`QstOq#(8BkB7=M2F1ls z-6vgSY-yh+-|5cK(mYMxv{AJodCQ*_swE8XE__=2On@Qp!l%V*8(BCyrpa&Z;agC( z<ZblOHkV#3ANr>}^XJ2jAv%nHM!C5= zmz}$_T~&9=6zk*i?>Ak$b8&Cg$zvRCY$`#km>w+oGnYSRjiYt=CErPE^FFO-Q++$_ zOt9MJ{o7uzkK+E=BjC{%!ggR~)U)lT(|qo-F4MENc$4w};P-1=|M}lZW}2k*m674W mnV^qC0uH6bHxX|9XOeQga=r7s*E$9U1_n=8KbLh*2~7a6FTq>@ literal 0 HcmV?d00001 diff --git a/assets/icon/talk.svg b/assets/icon/talk.svg new file mode 100644 index 0000000..af7fef8 --- /dev/null +++ b/assets/icon/talk.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cmd/talk/rsrc_windows_amd64.syso b/cmd/talk/rsrc_windows_amd64.syso new file mode 100644 index 0000000000000000000000000000000000000000..829bef5462b6cb03681e7187a11756a1d0918baf GIT binary patch literal 115442 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 ZU}6w}v-FCJi;^M2;E@bw1{MYe1^}eb>R literal 0 HcmV?d00001 diff --git a/internal/icon/icon.go b/internal/icon/icon.go new file mode 100644 index 0000000..47aa43f --- /dev/null +++ b/internal/icon/icon.go @@ -0,0 +1,39 @@ +// Package icon carries the talk icon for the desktop: the PNG is embedded +// in the binary and written next to the config so the OS can show it in +// notifications (Windows toast identity, freedesktop app_icon). The source +// is assets/icon/talk.svg; `go run assets/icon/gen.go` renders it, the +// Windows .ico and the resource objects for talk.exe. +package icon + +import ( + "bytes" + _ "embed" + "errors" + "os" + "path/filepath" +) + +//go:embed talk.png +var PNG []byte + +// FileName is the icon's name in the config directory. +const FileName = "talk.png" + +// Install writes the icon into dir (if missing or outdated) and returns its +// path. Idempotent: an unchanged file is not touched. +func Install(dir string) (string, error) { + if dir == "" { + return "", errors.New("icon: no directory") + } + path := filepath.Join(dir, FileName) + if cur, err := os.ReadFile(path); err == nil && bytes.Equal(cur, PNG) { + return path, nil + } + if err := os.MkdirAll(dir, 0o755); err != nil { + return "", err + } + if err := os.WriteFile(path, PNG, 0o644); err != nil { + return "", err + } + return path, nil +} diff --git a/internal/icon/icon_test.go b/internal/icon/icon_test.go new file mode 100644 index 0000000..3cb5ce9 --- /dev/null +++ b/internal/icon/icon_test.go @@ -0,0 +1,49 @@ +package icon + +import ( + "bytes" + "os" + "path/filepath" + "testing" +) + +// TestInstall: the icon lands in the directory, is left alone when current, +// and is repaired when the file on disk differs. +func TestInstall(t *testing.T) { + if len(PNG) < 8 || !bytes.HasPrefix(PNG, []byte("\x89PNG")) { + t.Fatalf("embedded icon is not a PNG (%d bytes)", len(PNG)) + } + dir := filepath.Join(t.TempDir(), "talk") + path, err := Install(dir) + if err != nil { + t.Fatal(err) + } + if path != filepath.Join(dir, FileName) { + t.Errorf("path = %q", path) + } + got, err := os.ReadFile(path) + if err != nil || !bytes.Equal(got, PNG) { + t.Fatalf("installed file differs (err %v)", err) + } + first, _ := os.Stat(path) + if _, err := Install(dir); err != nil { + t.Fatal(err) + } + second, _ := os.Stat(path) + if !second.ModTime().Equal(first.ModTime()) { + t.Error("unchanged icon was rewritten") + } + if err := os.WriteFile(path, []byte("stale"), 0o644); err != nil { + t.Fatal(err) + } + if _, err := Install(dir); err != nil { + t.Fatal(err) + } + got, _ = os.ReadFile(path) + if !bytes.Equal(got, PNG) { + t.Error("stale icon was not replaced") + } + if _, err := Install(""); err == nil { + t.Error("Install(\"\") succeeded") + } +} diff --git a/internal/icon/talk.png b/internal/icon/talk.png new file mode 100644 index 0000000000000000000000000000000000000000..d1611f8a2ddc93fc4fa3b17d76eedbf2f438a258 GIT binary patch literal 4065 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