[client/ui-wails] Use original Fyne tray PNGs and drop the .ico split

The SVG-derived tray icons + multi-resolution .ico path looked correct on
disk but Wails3's Shell_NotifyIcon update never landed on the running
Windows tray — the icon stayed frozen on the .exe resource regardless of
how many times we called SetIcon. Single-PNG fed through the same path
updates correctly, so revert to the source-of-truth PNGs that ship with
the legacy Fyne UI and remove the icons_windows.go / tray_icon_*.go
split. The 6 colored tray PNGs and 6 macOS-template PNGs come from
client/ui/assets verbatim. Generation pipeline (assets/svg/) is gone.
This commit is contained in:
Zoltán Papp
2026-04-29 18:54:51 +02:00
parent 8f16a19b8f
commit 0148d926d5
31 changed files with 75 additions and 95 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

+21 -17
View File
@@ -110,22 +110,31 @@ tasks:
- wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico -iconcomposerinput appicon.icon -macassetdir darwin
generate:tray:icons:
summary: Rasterize the SVG tray sources to PNG/ICO embedded by the Go side.
summary: Rebuild Windows multi-res .ico files from the per-state PNGs.
desc: |
Reads assets/svg/*.svg and writes assets/<state>.png (Linux/macOS
tray) plus assets/<state>.ico (Windows tray). The .ico packs 16/24/32/48
px frames so Shell_NotifyIcon picks the size matching the user's DPI
instead of poorly downscaling a single large PNG.
Run after editing any SVG; CI runs this before build.
The colored tray PNGs (assets/netbird-systemtray-<state>.png) and the
macOS template variants are committed to the repo as the canonical
source. This task only regenerates the Windows multi-resolution .ico
files from those PNGs by downscaling each to 16/24/32/48 px and
packing them with icotool, so Shell_NotifyIcon picks the frame
matching the user's DPI instead of downscaling a single large PNG.
Run after replacing any of the colored PNGs (e.g. when copying a new
version of the icons from client/ui/assets). The SVG sources in
assets/svg/ are kept for reference but are not built by default.
dir: assets
sources:
- "svg/*.svg"
- "netbird-systemtray-connected.png"
- "netbird-systemtray-disconnected.png"
- "netbird-systemtray-connecting.png"
- "netbird-systemtray-error.png"
- "netbird-systemtray-update-connected.png"
- "netbird-systemtray-update-disconnected.png"
generates:
- "netbird-systemtray-*.png"
- "netbird-systemtray-*.ico"
preconditions:
- sh: command -v inkscape >/dev/null 2>&1
msg: "inkscape is required to rasterize tray SVGs (apt install inkscape)"
- sh: command -v magick >/dev/null 2>&1 || command -v convert >/dev/null 2>&1
msg: "ImageMagick is required to downscale PNGs (apt install imagemagick)"
- sh: command -v icotool >/dev/null 2>&1
msg: "icotool is required to pack tray .ico files (apt install icoutils)"
cmds:
@@ -133,19 +142,14 @@ tasks:
set -euo pipefail
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
resize=$(command -v magick || echo convert)
for state in connected disconnected connecting error update-connected update-disconnected; do
# 64px PNG for Linux tray + macOS-template PNG (per-state)
inkscape --export-type=png --export-width=64 --export-filename="netbird-systemtray-$state.png" "svg/$state.svg" >/dev/null
inkscape --export-type=png --export-width=64 --export-filename="netbird-systemtray-$state-macos.png" "svg/$state-macos.svg" >/dev/null
# multi-resolution .ico for Windows tray
for sz in 16 24 32 48; do
inkscape --export-type=png --export-width=$sz --export-filename="$tmp/$state-$sz.png" "svg/$state.svg" >/dev/null
"$resize" "netbird-systemtray-$state.png" -resize ${sz}x${sz} "$tmp/$state-$sz.png"
done
icotool -c -o "netbird-systemtray-$state.ico" \
"$tmp/$state-16.png" "$tmp/$state-24.png" "$tmp/$state-32.png" "$tmp/$state-48.png"
done
# Linux dark-mode variant currently shares the connected artwork.
inkscape --export-type=png --export-width=64 --export-filename="netbird-systemtray-connected-dark.png" "svg/connected.svg" >/dev/null
dev:frontend:
summary: Runs the frontend in development mode
+6 -4
View File
@@ -4,10 +4,12 @@ package main
import _ "embed"
// Tray icons embedded from the legacy Fyne UI's asset set so the rewrite has
// something to render until Stage 3 produces SVG sources. Each pair is a
// light-mode PNG and its dark-mode variant; macOS template variants live
// alongside for menubar use.
// Tray icons embedded from the legacy Fyne UI's asset set. Each pair is a
// light-mode PNG and its dark-mode variant; macOS template variants
// (*-macos.png) live alongside for menubar use. Windows uses the same
// PNGs — multi-resolution .ico files looked promising on disk but
// Wails3's Shell_NotifyIcon NIM_MODIFY never redrew them on the running
// tray; PNG single-frame works.
//go:embed assets/netbird-systemtray-connected.png
var iconConnected []byte
-29
View File
@@ -1,29 +0,0 @@
//go:build windows
package main
import _ "embed"
// Windows tray icons. Wails3 hands these to Shell_NotifyIcon via
// CreateIconFromResourceEx, which picks the frame matching SM_CXSMICON
// (16/32 px depending on DPI). A single high-res PNG forces the OS to
// downscale and the result is fuzzy at tray size — multi-frame .ico files
// avoid that by embedding 16/24/32/48 px raster frames in one resource.
//go:embed assets/netbird-systemtray-connected.ico
var winIconConnected []byte
//go:embed assets/netbird-systemtray-disconnected.ico
var winIconDisconnected []byte
//go:embed assets/netbird-systemtray-connecting.ico
var winIconConnecting []byte
//go:embed assets/netbird-systemtray-error.ico
var winIconError []byte
//go:embed assets/netbird-systemtray-update-connected.ico
var winIconUpdateConnected []byte
//go:embed assets/netbird-systemtray-update-disconnected.ico
var winIconUpdateDisconnected []byte
+31
View File
@@ -7,17 +7,41 @@ import (
"embed"
"flag"
"log"
"strings"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
"github.com/wailsapp/wails/v3/pkg/services/notifications"
"github.com/netbirdio/netbird/client/ui-wails/services"
"github.com/netbirdio/netbird/util"
)
//go:embed all:frontend/dist
var assets embed.FS
// stringList is a flag.Value that collects repeated string flags. The first
// time the user passes -log-file the seeded default ("console") is dropped;
// subsequent passes append. Lets the user replace or extend the log target
// list without a separate "reset" flag.
type stringList struct {
values []string
userSet bool
}
func (s *stringList) String() string {
return strings.Join(s.values, ",")
}
func (s *stringList) Set(v string) error {
if !s.userSet {
s.values = nil
s.userSet = true
}
s.values = append(s.values, v)
return nil
}
func init() {
application.RegisterEvent[services.Status](services.EventStatus)
application.RegisterEvent[services.SystemEvent](services.EventSystem)
@@ -27,8 +51,15 @@ func init() {
func main() {
daemonAddr := flag.String("daemon-addr", DaemonAddr(), "Daemon gRPC address: unix:///path or tcp://host:port")
logFiles := &stringList{values: []string{"console"}}
flag.Var(logFiles, "log-file", "Log destination. Repeat to log to multiple targets at once, e.g. `--log-file console --log-file Y:/netbird-ui.log`. Each value is one of: console, syslog, or a file path. File destinations are rotated by lumberjack (same as the daemon). Defaults to console.")
logLevel := flag.String("log-level", "info", "Log level: trace|debug|info|warn|error.")
flag.Parse()
if err := util.InitLog(*logLevel, logFiles.values...); err != nil {
log.Fatalf("init log: %v", err)
}
conn := NewConn(*daemonAddr)
// tray is captured in the SingleInstance callback below; the var is
+6 -1
View File
@@ -178,12 +178,17 @@ func (s *Peers) pollLoop(ctx context.Context) {
ticker := time.NewTicker(PollInterval)
defer ticker.Stop()
first := true
for {
st, err := s.Get(ctx)
if err == nil {
if first {
log.Infof("peers pollLoop: first status ok status=%q peers=%d", st.Status, len(st.Peers))
first = false
}
s.emitter.Emit(EventStatus, st)
} else if ctx.Err() == nil {
log.Debugf("status poll: %v", err)
log.Warnf("peers pollLoop: status poll error: %v", err)
}
select {
+11 -9
View File
@@ -299,8 +299,10 @@ func (t *Tray) onSystemEvent(ev *application.CustomEvent) {
func (t *Tray) onUpdateAvailable(ev *application.CustomEvent) {
upd, ok := ev.Data.(services.UpdateAvailable)
if !ok {
log.Warnf("update event payload not UpdateAvailable: %T", ev.Data)
return
}
log.Infof("tray onUpdateAvailable: version=%s enforced=%v", upd.Version, upd.Enforced)
t.mu.Lock()
t.hasUpdate = true
t.mu.Unlock()
@@ -383,15 +385,14 @@ func (t *Tray) rebuildExitNodes(nodes []string) {
}
func (t *Tray) applyIcon() {
if runtime.GOOS == "windows" {
t.mu.Lock()
ico := trayIcon(t.connected, t.hasUpdate, t.lastStatus)
t.mu.Unlock()
if ico != nil {
t.tray.SetIcon(ico)
}
return
}
t.mu.Lock()
connected := t.connected
hasUpdate := t.hasUpdate
statusLabel := t.lastStatus
t.mu.Unlock()
log.Infof("tray applyIcon: connected=%v hasUpdate=%v status=%q goos=%s",
connected, hasUpdate, statusLabel, runtime.GOOS)
icon, dark := t.iconForState()
if runtime.GOOS == "darwin" {
@@ -563,3 +564,4 @@ func titleCase(s string) string {
}
return strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
}
-8
View File
@@ -1,8 +0,0 @@
//go:build !windows && !android && !ios && !freebsd && !js
package main
// trayIcon is unused on non-Windows hosts — Linux feeds setIcon a PNG and
// macOS uses SetTemplateIcon. This stub exists so the compiler is happy and
// callers don't need build tags around references.
func trayIcon(_, _ bool, _ string) []byte { return nil }
-27
View File
@@ -1,27 +0,0 @@
//go:build windows
package main
import "strings"
// trayIcon returns the Windows-tray .ico bytes for the given state. The
// other-platform implementation in tray_icon_other.go returns the colored
// PNG instead. Splitting it this way keeps the Linux/macOS paths free of
// .ico artifacts in their //go:embed search and avoids loading large icon
// resources where they aren't used.
func trayIcon(connected, hasUpdate bool, statusLabel string) []byte {
switch {
case strings.EqualFold(statusLabel, "Connecting"):
return winIconConnecting
case strings.EqualFold(statusLabel, "Error"):
return winIconError
case connected && hasUpdate:
return winIconUpdateConnected
case connected:
return winIconConnected
case hasUpdate:
return winIconUpdateDisconnected
default:
return winIconDisconnected
}
}