From 7f560df9be43311d87412d4affc70f53ec9c1fce Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Mon, 11 May 2026 12:01:46 +0200 Subject: [PATCH] [client/ui] Tray menu opens on click; hide window at startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Left-click on the tray icon now opens the menu on every platform — the window is reached through a new "Open NetBird" entry. Only the action that matches the current daemon state is shown: Connect when disconnected, Disconnect when connected. The main window starts hidden and is only surfaced via the tray, single-instance launch, or daemon events. --- client/ui/main.go | 2 +- client/ui/tray.go | 65 +++++++++++++++-------------------------------- 2 files changed, 21 insertions(+), 46 deletions(-) diff --git a/client/ui/main.go b/client/ui/main.go index b706761c2..6fc38a971 100644 --- a/client/ui/main.go +++ b/client/ui/main.go @@ -118,7 +118,7 @@ func main() { Title: "NetBird", Width: 960, Height: 640, - Hidden: false, + Hidden: true, BackgroundColour: application.NewRGB(24, 26, 29), URL: "/", Mac: application.MacWindow{ diff --git a/client/ui/tray.go b/client/ui/tray.go index a3f4e0b7f..f88149d37 100644 --- a/client/ui/tray.go +++ b/client/ui/tray.go @@ -139,32 +139,15 @@ func NewTray(app *application.App, window *application.WebviewWindow, svc TraySe t.applyIcon() t.tray.SetTooltip(trayTooltip) t.tray.SetMenu(t.buildMenu()) - // Tray click handling is platform-specific by design: - // - // On Windows and macOS the OS-level tray protocol cleanly separates left - // and right click. AttachWindow plus an explicit OnClick gives the - // expected "click the icon to toggle the window, right-click to open the - // menu" UX, and the platform never delivers both events at once. - // - // On Linux the tray rides on the org.kde.StatusNotifierItem D-Bus protocol - // (libayatana-appindicator). The SNI Activate signal *is* left-click, but - // several environments — GNOME Shell with the AppIndicator extension is - // the loudest offender — also pop the attached menu on left-click, - // regardless of the ItemIsMenu property the spec defines for that purpose. - // Worse, AttachWindow on its own is enough to trigger this: Wails3's - // SystemTray.applySmartDefaults installs ToggleWindow as the default - // click handler whenever a window is attached, so even without an - // explicit OnClick the window pops up alongside the menu. The result - // looks like a bug to users. - // - // Mirror the legacy Fyne client's behaviour on Linux: skip both - // AttachWindow and OnClick so left-click only opens the menu, and expose - // the window through an explicit "Open NetBird" item. Right-click still - // opens the menu through Wails' default rightClickHandler fallback. - if runtime.GOOS != "linux" { - t.tray.AttachWindow(window) - t.tray.OnClick(func() { t.toggleWindow() }) - } + // Left-click on the tray icon opens the menu on every platform. The + // window is reached through the explicit "Open NetBird" entry. This + // matches macOS NSStatusItem convention (click → menu), the Linux + // StatusNotifierItem spec, and the legacy Fyne client. On Linux, + // AttachWindow plus Wails3's applySmartDefaults would also pop the + // window alongside the menu on environments like GNOME Shell with the + // AppIndicator extension, so we intentionally skip both AttachWindow + // and OnClick here. Right-click still opens the menu through Wails' + // default rightClickHandler fallback. app.Event.On(services.EventStatus, t.onStatusEvent) app.Event.On(services.EventSystem, t.onSystemEvent) @@ -195,16 +178,17 @@ func (t *Tray) buildMenu() *application.Menu { SetEnabled(false) menu.AddSeparator() - // On Linux the tray icon's left-click handler is intentionally unbound - // (see NewTray for the rationale), so expose the window through an - // explicit menu entry. Windows and macOS get the window via left-click. - if runtime.GOOS == "linux" { - menu.Add(menuOpenNetBird).OnClick(func(*application.Context) { t.ShowWindow() }) - menu.AddSeparator() - } + // The tray icon's left-click handler is intentionally unbound (see + // NewTray for the rationale), so expose the window through an explicit + // menu entry on every platform. + menu.Add(menuOpenNetBird).OnClick(func(*application.Context) { t.ShowWindow() }) + menu.AddSeparator() + // Only the action that applies to the current state is visible: Connect + // when disconnected, Disconnect when connected. applyStatus swaps them on + // each daemon status change. t.upItem = menu.Add(menuConnect).OnClick(func(*application.Context) { t.handleConnect() }) t.downItem = menu.Add(menuDisconnect).OnClick(func(*application.Context) { t.handleDisconnect() }) - t.downItem.SetEnabled(false) + t.downItem.SetHidden(true) menu.AddSeparator() @@ -245,17 +229,6 @@ func (t *Tray) buildMenu() *application.Menu { return menu } -func (t *Tray) toggleWindow() { - if t.window == nil { - return - } - if t.window.IsVisible() { - t.window.Hide() - return - } - t.window.Show() -} - func (t *Tray) openRoute(route string) { if t.window == nil { return @@ -466,9 +439,11 @@ func (t *Tray) applyStatus(st services.Status) { t.statusItem.SetEnabled(needsLogin) } if t.upItem != nil { + t.upItem.SetHidden(connected || needsLogin) t.upItem.SetEnabled(!connected && !needsLogin) } if t.downItem != nil { + t.downItem.SetHidden(!connected) t.downItem.SetEnabled(connected) } }