From 8d05fe07bff9f22a1a8746b7ae1fd63025a8555a Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:48:39 +0200 Subject: [PATCH] Fix watcher registration on wayland (#6320) * Fix hover label on linux * Fix watcher registration on wayland --- client/ui/tray_watcher_linux.go | 22 ++++++++++++++++++++-- client/ui/xembed_host_linux.go | 19 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/client/ui/tray_watcher_linux.go b/client/ui/tray_watcher_linux.go index 7a9b72096..63213985e 100644 --- a/client/ui/tray_watcher_linux.go +++ b/client/ui/tray_watcher_linux.go @@ -98,9 +98,27 @@ func (w *statusNotifierWatcher) tryStartXembedHost(busName string, objPath dbus. } // startStatusNotifierWatcher claims org.kde.StatusNotifierWatcher on the -// session bus if it is not already provided by another process. -// Safe to call unconditionally — it does nothing when a real watcher is present. +// session bus, but ONLY as a bridge to an XEmbed system tray on minimal WMs. +// +// The in-process watcher is a stub: its RegisterStatusNotifierItem only +// tracks items so it can mirror them into an XEmbed tray icon — it does +// NOT relay them to any other StatusNotifierHost. So if we claim the name +// on a desktop that has a real watcher/host (e.g. Hyprland + Waybar), every +// other tray app (Slack, etc.) registers into our dead-end watcher and its +// icon never reaches the real host. We won that name purely by starting +// first; a GetNameOwner check doesn't help against a login-order race. +// +// The correct discriminator is whether an XEmbed tray actually exists. If +// one does, we are the bridge of last resort and should claim the watcher. +// If not (pure Wayland, or any environment already running a real watcher), +// we have nothing to bridge and must stay off the bus entirely so the real +// watcher owns the name. Safe to call unconditionally. func startStatusNotifierWatcher() { + if !xembedTrayAvailable() { + log.Debugf("StatusNotifierWatcher: no XEmbed tray present, leaving the watcher to the desktop") + return + } + conn, err := dbus.SessionBusPrivate() if err != nil { log.Debugf("StatusNotifierWatcher: cannot open private session bus: %v", err) diff --git a/client/ui/xembed_host_linux.go b/client/ui/xembed_host_linux.go index f90e7de75..2b66ddb32 100644 --- a/client/ui/xembed_host_linux.go +++ b/client/ui/xembed_host_linux.go @@ -88,6 +88,25 @@ func goMenuItemClicked(id C.int) { } // newXembedHost creates an XEmbed tray icon for the given SNI item. +// xembedTrayAvailable reports whether an XEmbed system tray manager +// (_NET_SYSTEM_TRAY_S0) currently owns its selection on the default screen. +// It is a cheap, side-effect-free probe — it only queries the selection +// owner, creating no windows. Used to decide whether the in-process +// StatusNotifierWatcher is needed at all: the watcher exists solely to +// bridge SNI items into an XEmbed tray on minimal WMs, so when no XEmbed +// tray is present (e.g. Wayland compositors with a real SNI host like +// Waybar) we must not claim org.kde.StatusNotifierWatcher and shadow the +// real one. Returns false when there is no X display (pure Wayland). +func xembedTrayAvailable() bool { + dpy := C.XOpenDisplay(nil) + if dpy == nil { + return false + } + defer C.XCloseDisplay(dpy) + screen := C.xembed_default_screen(dpy) + return C.xembed_find_tray(dpy, screen) != 0 +} + // Returns an error if no XEmbed tray manager is available (graceful fallback). func newXembedHost(conn *dbus.Conn, busName string, objPath dbus.ObjectPath) (*xembedHost, error) { dpy := C.XOpenDisplay(nil)