Deliver shell status updates without a polling timer

This commit is contained in:
Samuel Onoja committed 2026-09-27 07:37:41 +01:00
1 parent 4bf075734c
commit b0583eb993
3 files changed
+104 -15

No files matched your search

+6 -6
View File
@@ -259,7 +259,7 @@ enum Message {
Tick,
ActivateWorkspace(u64),
ToggleOverview,
StatusTick,
StatusUpdated(status::Update),
AnimateMenu,
OpenMenu(status_ui::Menu, cosmic::iced::Rectangle<i32>),
OpenMenuOn(window::Id, status_ui::Menu, cosmic::iced::Rectangle<i32>),
@@ -348,7 +348,9 @@ impl cosmic::Application for FereseShell {
_ => None,
}),
cosmic::iced::time::every(Duration::from_millis(500)).map(|_| Message::Tick),
cosmic::iced::time::every(Duration::from_millis(250)).map(|_| Message::StatusTick),
self.status_service
.subscription()
.map(Message::StatusUpdated),
if self.config.desktop_widgets.clock.enabled
|| self
.config
@@ -491,10 +493,8 @@ impl cosmic::Application for FereseShell {
}
Task::none()
}
Message::StatusTick => {
if let Some(update) = self.status_service.poll()
&& update.generation >= self.status_service.generation
{
Message::StatusUpdated(update) => {
if update.generation >= self.status_service.generation {
self.status = update.snapshot;
self.status_error = update.error;
}
+97 -8
View File
@@ -5,7 +5,7 @@ use std::{
process::{Command, Stdio},
sync::{
Arc, Condvar, Mutex,
mpsc::{self, Receiver, SyncSender},
mpsc::{self, SyncSender},
},
thread,
time::Duration,
@@ -77,6 +77,7 @@ impl Action {
}
}
#[derive(Clone, Debug)]
pub struct Update {
pub snapshot: Snapshot,
pub generation: u64,
@@ -86,10 +87,35 @@ pub struct Update {
pub struct Service {
settings: Arc<Mutex<Option<Vec<String>>>>,
tx: SyncSender<(u64, Action)>,
rx: Receiver<Update>,
updates: Updates,
pub generation: u64,
}
// The receiver stays with the service so a subscription can start after the
// first poll. A watch channel retains the newest result without blocking workers.
#[derive(Clone)]
struct Updates(Arc<tokio::sync::watch::Receiver<Option<Update>>>);
impl std::hash::Hash for Updates {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::ptr::hash(Arc::as_ptr(&self.0), state);
}
}
impl Updates {
fn stream(&self) -> impl cosmic::iced::futures::Stream<Item = Update> + use<> {
cosmic::iced::futures::stream::unfold(self.0.as_ref().clone(), |mut receiver| async move {
loop {
receiver.changed().await.ok()?;
let update = receiver.borrow_and_update().clone();
if let Some(update) = update {
return Some((update, receiver));
}
}
})
}
}
type PollState = (Mutex<(u64, bool, Option<String>, bool)>, Condvar);
fn wait_for_poll(shared: &PollState) -> Option<u64> {
@@ -105,7 +131,7 @@ impl Service {
let live_settings = Arc::new(Mutex::new(settings));
let settings = live_settings.clone();
let (tx, commands) = mpsc::sync_channel::<(u64, Action)>(64);
let (updates, rx) = mpsc::sync_channel(1);
let (updates, rx) = tokio::sync::watch::channel(None);
// Polling and writes have separate workers: a missing D-Bus service must
// never hold up volume/brightness changes. Publish only coherent polls.
let shared = Arc::new((
@@ -129,11 +155,11 @@ impl Service {
break;
}
if state.0 == before && !state.1 {
let _ = updates.try_send(Update {
updates.send_replace(Some(Update {
snapshot,
generation: state.0,
error: state.2.clone(),
});
}));
let _ = polling.1.wait_timeout(state, Duration::from_secs(2));
}
}
@@ -169,7 +195,7 @@ impl Service {
Self {
settings: live_settings,
tx,
rx,
updates: Updates(Arc::new(rx)),
generation: 0,
}
}
@@ -187,8 +213,8 @@ impl Service {
*self.settings.lock().unwrap() = settings;
}
pub fn poll(&self) -> Option<Update> {
self.rx.try_iter().last()
pub fn subscription(&self) -> cosmic::iced::Subscription<Update> {
cosmic::iced::Subscription::run_with(self.updates.clone(), Updates::stream)
}
}
@@ -666,6 +692,69 @@ mod tests {
}
}
#[test]
fn status_stream_retains_latest_result_wakes_and_closes() {
use cosmic::iced::futures::{
Stream,
task::{ArcWake, waker},
};
use std::{
sync::atomic::{AtomicUsize, Ordering},
task::{Context, Poll},
};
#[derive(Default)]
struct WakeCount(AtomicUsize);
impl ArcWake for WakeCount {
fn wake_by_ref(arc_self: &Arc<Self>) {
arc_self.0.fetch_add(1, Ordering::Relaxed);
}
}
let result = |generation| {
Some(Update {
snapshot: Snapshot {
brightness: Some(42),
..Snapshot::default()
},
generation,
error: Some("service unavailable".into()),
})
};
let (sender, receiver) = tokio::sync::watch::channel(None);
let updates = Updates(Arc::new(receiver));
// Results produced before the UI subscribes remain available; a slow
// consumer gets the newest generation instead of a stale queued result.
sender.send_replace(result(1));
sender.send_replace(result(2));
let mut stream = Box::pin(updates.stream());
let wakes = Arc::new(WakeCount::default());
let waker = waker(wakes.clone());
let mut context = Context::from_waker(&waker);
let Poll::Ready(Some(update)) = stream.as_mut().poll_next(&mut context) else {
panic!("startup result missing");
};
assert_eq!(update.generation, 2);
assert_eq!(update.snapshot.brightness, Some(42));
assert_eq!(update.error.as_deref(), Some("service unavailable"));
assert!(stream.as_mut().poll_next(&mut context).is_pending());
let before = wakes.0.load(Ordering::Relaxed);
thread::spawn(move || {
sender.send_replace(result(3));
// Closing still delivers the last unseen update before ending.
})
.join()
.unwrap();
assert!(wakes.0.load(Ordering::Relaxed) > before);
let Poll::Ready(Some(update)) = stream.as_mut().poll_next(&mut context) else {
panic!("worker result missing");
};
assert_eq!(update.generation, 3);
assert!(matches!(
stream.as_mut().poll_next(&mut context),
Poll::Ready(None)
));
}
#[test]
fn polling_waits_for_a_write_and_uses_the_completed_generation() {
let shared = Arc::new((Mutex::new((7, true, None, false)), Condvar::new()));