mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-08 09:49:54 +00:00
Wails3's Linux systray hands the icon off to whatever process owns
org.kde.StatusNotifierWatcher on the session bus. Bare WMs (Fluxbox,
OpenBox, i3, dwm, sway, vanilla GNOME without the AppIndicator
extension) ship no watcher, so the icon registration silently fails
and the tray never appears — leaving a tray-only app like NetBird
unreachable.
Add a Linux-only watcher fallback that claims the watcher name when
nobody else does, plus an XEmbed bridge so legacy X11 system trays
(_NET_SYSTEM_TRAY_S0) can still render the icon. Both no-op on other
platforms via build tags.
Pieces:
- tray_watcher_linux.go: claims org.kde.StatusNotifierWatcher on a
private session bus, exports the bare RegisterStatusNotifierItem /
RegisterStatusNotifierHost surface, and spins up an XEmbed host per
registered SNI item.
- xembed_host_linux.go: per-item event loop. Polls X11 events with a
50ms ticker, listens for the SNI NewIcon signal, dispatches Activate
/ context menu through dbusmenu (com.canonical.dbusmenu).
- xembed_tray_linux.{c,h}: the X11/cairo native bits. Window is created
with CopyFromParent visual + ParentRelative background so transparent
pixels show the toolbar beneath instead of solid black on 24-bit
trays. cairo paints the IconPixmap with OVER blending so per-pixel
alpha is honoured against the parent-relative base. GTK3 owns the
context-menu popup; menu items round-trip through dbusmenu Event.
- tray_linux.go: forces WEBKIT_DISABLE_DMABUF_RENDERER=1 in init() so
developers running `task dev` / launching the binary directly get the
same software rendering path the .desktop launcher already enables;
the deb/rpm Exec wrapper covers installed users.
- tray_watcher_other.go and xembed_host_other.go: build-tag stubs so
main.go's startStatusNotifierWatcher() compiles on every platform.
- main.go: calls startStatusNotifierWatcher() before NewTray so the
Wails systray's RegisterStatusNotifierItem call hits a watcher we
control on bare WMs.
- build/linux/netbird-ui.desktop: regenerated by `task build` to wrap
the dev launcher's Exec line with the WEBKIT_DISABLE_DMABUF_RENDERER
env, matching what the tray_linux.go init does at runtime.
Adapted from work originally prototyped on the prototype/ui-wails branch.
Tested on Fluxbox (Debian 13): the icon appears in the slit/toolbar with
the toolbar's background showing through transparent pixels, left-click
opens the window, right-click brings up the GTK popup of the dbusmenu
items.
156 lines
4.9 KiB
Go
156 lines
4.9 KiB
Go
//go:build !android && !ios && !freebsd && !js
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"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)
|
|
application.RegisterEvent[services.UpdateAvailable](services.EventUpdateAvailable)
|
|
application.RegisterEvent[services.UpdateProgress](services.EventUpdateProgress)
|
|
}
|
|
|
|
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
|
|
// declared before app.New so the closure has a stable reference.
|
|
var tray *Tray
|
|
|
|
app := application.New(application.Options{
|
|
Name: "netbird-ui",
|
|
Description: "NetBird desktop client",
|
|
Icon: iconWindow,
|
|
Assets: application.AssetOptions{
|
|
Handler: application.AssetFileServerFS(assets),
|
|
},
|
|
Mac: application.MacOptions{
|
|
ApplicationShouldTerminateAfterLastWindowClosed: false,
|
|
},
|
|
Linux: application.LinuxOptions{
|
|
ProgramName: "netbird",
|
|
},
|
|
SingleInstance: &application.SingleInstanceOptions{
|
|
UniqueID: "io.netbird.ui",
|
|
OnSecondInstanceLaunch: func(_ application.SecondInstanceData) {
|
|
if tray != nil {
|
|
tray.ShowWindow()
|
|
}
|
|
},
|
|
},
|
|
})
|
|
|
|
connection := services.NewConnection(conn)
|
|
settings := services.NewSettings(conn)
|
|
profiles := services.NewProfiles(conn)
|
|
peers := services.NewPeers(conn, app.Event)
|
|
update := services.NewUpdate(conn)
|
|
notifier := notifications.New()
|
|
|
|
app.RegisterService(application.NewService(connection))
|
|
app.RegisterService(application.NewService(settings))
|
|
app.RegisterService(application.NewService(services.NewNetworks(conn)))
|
|
app.RegisterService(application.NewService(services.NewForwarding(conn)))
|
|
app.RegisterService(application.NewService(profiles))
|
|
app.RegisterService(application.NewService(services.NewDebug(conn)))
|
|
app.RegisterService(application.NewService(update))
|
|
app.RegisterService(application.NewService(peers))
|
|
app.RegisterService(application.NewService(notifier))
|
|
|
|
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
|
Title: "NetBird",
|
|
Width: 960,
|
|
Height: 640,
|
|
Hidden: false,
|
|
BackgroundColour: application.NewRGB(24, 26, 29),
|
|
URL: "/",
|
|
Mac: application.MacWindow{
|
|
InvisibleTitleBarHeight: 38,
|
|
Backdrop: application.MacBackdropTranslucent,
|
|
TitleBar: application.MacTitleBarHiddenInset,
|
|
},
|
|
Linux: application.LinuxWindow{
|
|
Icon: iconWindow,
|
|
},
|
|
})
|
|
|
|
// Intercept the window close to hide instead of quit. The user reaches
|
|
// "really quit" via tray -> Quit.
|
|
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
|
|
e.Cancel()
|
|
window.Hide()
|
|
})
|
|
|
|
// Register an in-process StatusNotifierWatcher so the tray works on
|
|
// minimal WMs (Fluxbox, OpenBox, i3, dwm, vanilla GNOME without the
|
|
// AppIndicator extension) that don't ship one themselves. No-op on
|
|
// non-Linux platforms. Must run before NewTray so the Wails systray's
|
|
// RegisterStatusNotifierItem call hits a watcher we control.
|
|
startStatusNotifierWatcher()
|
|
|
|
tray = NewTray(app, window, TrayServices{
|
|
Connection: connection,
|
|
Settings: settings,
|
|
Profiles: profiles,
|
|
Peers: peers,
|
|
Notifier: notifier,
|
|
Update: update,
|
|
})
|
|
listenForShowSignal(context.Background(), tray)
|
|
|
|
peers.Watch(context.Background())
|
|
|
|
if err := app.Run(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|