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 <noreply@anthropic.com>
This commit is contained in:
m3ta-chironandClaude Fable 5.1 committed 2026-10-25 12:43:14 +01:00
1 parent a0f91c2d64
commit 2e77cf0a11
8 files changed
+395

No files matched your search

+39
View File
@@ -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
}
+49
View File
@@ -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")
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB