From c30418a0f5e41958f760483e2195ea6ca4ef37f6 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Thu, 28 May 2026 15:33:11 -0400 Subject: [PATCH] fix(gui/tray): track watcher loss and never strand the window The `registered` flag was one-way: set true once the tray registered, but never cleared if the StatusNotifierWatcher later disappeared (panel restart, tray plugin disabled, tray app killed). After that, a Wayland close-to-tray would destroy the window into a tray that no longer exists, with no recovery short of SIGTERM. Implement ksni's `watcher_online`/`watcher_offline` callbacks to keep the shared flag in sync. On offline, also force the window back (idempotent `show()`) so a window that was already hidden when the watcher died isn't stranded, and return true to keep the service alive for re-registration. Co-Authored-By: Claude Opus 4.8 --- src/gui/tray.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/gui/tray.rs b/src/gui/tray.rs index a963208..49e0480 100644 --- a/src/gui/tray.rs +++ b/src/gui/tray.rs @@ -87,6 +87,10 @@ struct PixelPassTray { /// Wakes the winit loop and delivers the action — works even when the /// window has been dropped to the tray (no egui frame is running then). proxy: EventLoopProxy, + /// Shared with [`TrayHandle`]; kept in sync with the watcher's presence via + /// the `watcher_online`/`watcher_offline` callbacks so the app never diverts + /// a close to a tray that has since disappeared. + registered: Arc, } impl PixelPassTray { @@ -131,6 +135,25 @@ impl ksni::Tray for PixelPassTray { self.notify(TrayAction::Show); } + /// The StatusNotifierWatcher came back (e.g. the panel restarted). Mark the + /// tray live again so close-to-tray can resume hiding the window. + fn watcher_online(&self) { + self.registered.store(true, Ordering::Release); + } + + /// The watcher went away (panel restart, tray plugin disabled, …). Clear the + /// flag so a subsequent close quits normally instead of destroying the window + /// into a tray that no longer exists, and force the window back now in case + /// it was already hidden (otherwise it'd be stranded with no way to restore). + /// Returning `true` keeps the service alive so it re-registers if the watcher + /// returns. + fn watcher_offline(&self, reason: ksni::OfflineReason) -> bool { + tracing::warn!("tray: StatusNotifierWatcher offline ({reason:?}); restoring window"); + self.registered.store(false, Ordering::Release); + self.notify(TrayAction::Show); + true + } + fn menu(&self) -> Vec> { use ksni::menu::{MenuItem, StandardItem}; vec![ @@ -202,6 +225,7 @@ pub fn start(proxy: EventLoopProxy) -> Option { status: TrayStatus::Idle, icon, proxy, + registered: registered_thread.clone(), }; let handle = match tray.spawn().await { Ok(handle) => handle,