Files
feresaria/compositor/src/activities/publication.rs
T
Samuel OnojaandSamuel Onoja 6b0d903ad0 Add Live Activities as configurable bar items (#30)
* Add Live Activities as configurable bar items

* Fix activity owner monitoring and Morph list interactions

* Keep activity rows stable during updates and tighten their layout

* Use subtle theme tint for activity cards

* Tighten activity title and progress spacing

* Use theme accent for active progress rings

* Fit compact activities to measured titles and badge

* Expose Live Activities display controls in Settings

* Open activity terminals and streamline the Morph header

* Give Morph continuous icons and prioritize workspace navigation

* Show activity progress inside its ring

* Inset activity progress rings and strengthen their stroke

* Fix activity lifecycle, command output handling and list navigation

* Reduce activity update work and harden publisher lifetimes

* Set up Live Activities for the detected user shell during installation

* Document Activities setup, daily use and troubleshooting

* Preserve Morph idle width across empty activity updates

* Fix dimming flicker, hide inactive music tab and consolidate Activities docs

* Fix panel clearance across styles and match previews to theme settings

* Prevent panel blur from repainting neighboring islands

---------

Co-authored-by: Samuel Onoja <[email protected]>
2026-10-12 22:52:25 +01:00

79 lines
3.0 KiB
Rust

//! Bounded serialization and delivery outside the compositor event loop.
use ferese_ipc::{Response, activities::Snapshot};
use std::sync::{
Arc,
mpsc::{self, SyncSender},
};
type Recipient = (u64, SyncSender<Response>);
struct Job {
snapshot: Arc<Snapshot>,
recipients: Vec<Recipient>,
}
pub(super) struct Publisher(SyncSender<Job>);
impl Publisher {
pub fn new() -> std::io::Result<Self> {
let (send, receive) = mpsc::sync_channel::<Job>(8);
std::thread::Builder::new()
.name("ferese-activity-snapshots".into())
.spawn(move || {
let mut cached: Option<(Arc<Snapshot>, serde_json::Value)> = None;
while let Ok(job) = receive.recv() {
if !cached
.as_ref()
.is_some_and(|(snapshot, _)| Arc::ptr_eq(snapshot, &job.snapshot))
{
cached = Some((job.snapshot.clone(), serde_json::json!(job.snapshot.as_ref())));
}
let value = &cached.as_ref().unwrap().1;
for (id, sender) in job.recipients {
// Client queues are bounded too. A slow/disconnected client
// cannot block serialization or another subscriber.
let _ = sender.try_send(Response::success(id, value.clone()));
}
}
})?;
Ok(Self(send))
}
pub fn send(&self, snapshot: Arc<Snapshot>, recipients: Vec<Recipient>) {
if let Err(error) = self.0.try_send(Job { snapshot, recipients }) {
let (mpsc::TrySendError::Full(job) | mpsc::TrySendError::Disconnected(job)) = error;
unavailable(job.recipients);
}
}
}
pub(super) fn unavailable(recipients: Vec<Recipient>) {
for (id, sender) in recipients {
let _ = sender.try_send(Response::error(
id,
"activity_busy",
"Activity snapshot delivery unavailable; retry",
));
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn saturated_queue_rejects_without_blocking_and_slow_clients_do_not_block_others() {
let (send, _receive) = mpsc::sync_channel(0);
let publisher = Publisher(send);
let (reply, receive) = mpsc::sync_channel(1);
publisher.send(Arc::new(Snapshot::default()), vec![(7, reply)]);
let response = receive.recv_timeout(Duration::from_secs(1)).unwrap();
assert_eq!(response.id, 7);
assert_eq!(response.error.unwrap().code, "activity_busy");
let publisher = Publisher::new().unwrap();
let (slow, _slow_receive) = mpsc::sync_channel(1);
slow.send(Response::success(1, serde_json::json!({}))).unwrap();
let (fast, fast_receive) = mpsc::sync_channel(1);
publisher.send(Arc::new(Snapshot::default()), vec![(2, slow), (3, fast)]);
assert_eq!(fast_receive.recv_timeout(Duration::from_secs(1)).unwrap().id, 3);
}
}