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:
1 parent
a0f91c2d64
commit
2e77cf0a11
8 files changed
+395
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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 |
Reference in new issue
Block a user