fix(tray): apply tooltip updates from a dedicated thread
ksni 0.3.4 with the "blocking" feature wraps every Handle::update in Runtime::block_on, which panics when called from inside an existing tokio runtime — exactly where Tauri commands and our async polling loop run. The panic was being swallowed by the runtime, so set_track and set_playing looked like no-ops and the SNI tooltip kept rendering the static "QBZ / Music Player / Nothing playing" fallback. Move all updates onto a dedicated std::thread that owns the ksni blocking handle and processes mutations from a std::sync::mpsc channel, fully outside any tokio context. cc
This commit is contained in:
1 file changed
+86
-27
@@ -19,7 +19,10 @@
|
|||||||
//! into the SNI tooltip — replicating the rich tooltip the Plasma media
|
//! into the SNI tooltip — replicating the rich tooltip the Plasma media
|
||||||
//! plasmoid shows on hover.
|
//! plasmoid shows on hover.
|
||||||
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{
|
||||||
|
mpsc::{self, Sender},
|
||||||
|
Arc, Mutex,
|
||||||
|
};
|
||||||
|
|
||||||
use image::GenericImageView;
|
use image::GenericImageView;
|
||||||
use ksni::{
|
use ksni::{
|
||||||
@@ -296,61 +299,117 @@ impl Tray for QbzTray {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cross-thread handle to the live ksni tray. Cloneable; updates funnel
|
/// Updates dispatched from the rest of the backend into the ksni worker.
|
||||||
/// through `Handle::update`, which schedules a property-changed signal so
|
|
||||||
/// SNI panels redraw the tooltip on the next hover.
|
|
||||||
///
|
///
|
||||||
/// When the tray is disabled or failed to start, the inner option stays
|
/// We cannot call `ksni::blocking::Handle::update` directly from a Tauri
|
||||||
/// `None` and every mutator becomes a no-op.
|
/// command or from the playback poll loop — `ksni` 0.3 with `feature =
|
||||||
|
/// "blocking"` wraps every update in `Runtime::block_on`, which panics when
|
||||||
|
/// invoked from within an existing tokio runtime (which is exactly what
|
||||||
|
/// Tauri commands and `tauri::async_runtime::spawn` tasks run on). The
|
||||||
|
/// panic is silently swallowed by the runtime, so updates appear to
|
||||||
|
/// succeed but the tooltip never changes.
|
||||||
|
///
|
||||||
|
/// To avoid the nested-runtime issue we serialise updates over a
|
||||||
|
/// `std::sync::mpsc` channel and apply them from a dedicated `std::thread`
|
||||||
|
/// that lives entirely outside any tokio context.
|
||||||
|
enum TrayUpdate {
|
||||||
|
SetTrack {
|
||||||
|
title: String,
|
||||||
|
artist: String,
|
||||||
|
album: String,
|
||||||
|
},
|
||||||
|
ClearTrack,
|
||||||
|
SetPlaying(bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cross-thread handle to the live ksni tray. Cloneable; mutators just
|
||||||
|
/// forward to the worker thread, so they are safe to call from any
|
||||||
|
/// async context. When the tray is disabled or failed to start, the
|
||||||
|
/// inner sender stays `None` and every mutator becomes a no-op.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct LinuxTrayHandle {
|
pub struct LinuxTrayHandle {
|
||||||
inner: Arc<Mutex<Option<ksni::blocking::Handle<QbzTray>>>>,
|
sender: Arc<Mutex<Option<Sender<TrayUpdate>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LinuxTrayHandle {
|
impl LinuxTrayHandle {
|
||||||
pub fn empty() -> Self {
|
pub fn empty() -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: Arc::new(Mutex::new(None)),
|
sender: Arc::new(Mutex::new(None)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn install(&self, handle: ksni::blocking::Handle<QbzTray>) {
|
fn install(&self, handle: ksni::blocking::Handle<QbzTray>) {
|
||||||
if let Ok(mut guard) = self.inner.lock() {
|
let (tx, rx) = mpsc::channel::<TrayUpdate>();
|
||||||
*guard = Some(handle);
|
std::thread::Builder::new()
|
||||||
|
.name("qbz-tray-updater".into())
|
||||||
|
.spawn(move || {
|
||||||
|
while let Ok(msg) = rx.recv() {
|
||||||
|
match msg {
|
||||||
|
TrayUpdate::SetTrack {
|
||||||
|
title,
|
||||||
|
artist,
|
||||||
|
album,
|
||||||
|
} => {
|
||||||
|
log::debug!(
|
||||||
|
"[tray] tooltip update -> {} / {} / {}",
|
||||||
|
title,
|
||||||
|
artist,
|
||||||
|
album
|
||||||
|
);
|
||||||
|
handle.update(move |tray| {
|
||||||
|
tray.now_playing = Some(NowPlaying {
|
||||||
|
title,
|
||||||
|
artist,
|
||||||
|
album,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
TrayUpdate::ClearTrack => {
|
||||||
|
log::debug!("[tray] tooltip cleared");
|
||||||
|
handle.update(|tray| {
|
||||||
|
tray.now_playing = None;
|
||||||
|
tray.is_playing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
TrayUpdate::SetPlaying(is_playing) => {
|
||||||
|
handle.update(move |tray| {
|
||||||
|
tray.is_playing = is_playing;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::debug!("[tray] updater thread exiting");
|
||||||
|
})
|
||||||
|
.expect("spawn tray updater thread");
|
||||||
|
if let Ok(mut guard) = self.sender.lock() {
|
||||||
|
*guard = Some(tx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with<F: FnOnce(&mut QbzTray)>(&self, f: F) {
|
fn send(&self, msg: TrayUpdate) {
|
||||||
let guard = match self.inner.lock() {
|
let guard = match self.sender.lock() {
|
||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(_) => return,
|
Err(_) => return,
|
||||||
};
|
};
|
||||||
if let Some(handle) = guard.as_ref() {
|
if let Some(tx) = guard.as_ref() {
|
||||||
handle.update(f);
|
let _ = tx.send(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_track(&self, title: String, artist: String, album: String) {
|
pub fn set_track(&self, title: String, artist: String, album: String) {
|
||||||
self.with(move |tray| {
|
self.send(TrayUpdate::SetTrack {
|
||||||
tray.now_playing = Some(NowPlaying {
|
title,
|
||||||
title,
|
artist,
|
||||||
artist,
|
album,
|
||||||
album,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_track(&self) {
|
pub fn clear_track(&self) {
|
||||||
self.with(|tray| {
|
self.send(TrayUpdate::ClearTrack);
|
||||||
tray.now_playing = None;
|
|
||||||
tray.is_playing = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_playing(&self, is_playing: bool) {
|
pub fn set_playing(&self, is_playing: bool) {
|
||||||
self.with(move |tray| {
|
self.send(TrayUpdate::SetPlaying(is_playing));
|
||||||
tray.is_playing = is_playing;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user