1mod server;
13
14#[cfg(test)]
15mod tests;
16
17use std::cell::RefCell;
18use std::future::poll_fn;
19use std::pin::Pin;
20use std::rc::Rc;
21use std::sync::Once;
22use std::time::{Duration, Instant, SystemTime};
23
24use calloop::RegistrationToken;
25use calloop::timer::TimeoutAction;
26use crabshell_reactive::{ReadSignal, Signal};
27use crabshell_ui::{Cx, IconSource};
28use zbus::export::futures_core::Stream;
29use zbus::fdo::{RequestNameFlags, RequestNameReply};
30use zbus::message::Type as Kind;
31use zbus::{Connection, Message, MessageStream};
32
33use crate::Shared;
34use crate::dbus::BUS;
35use crate::dbus::set_changed;
36
37const NOTIFICATIONS: &str = "org.freedesktop.Notifications";
38const NOTIFICATIONS_PATH: &str = "/org/freedesktop/Notifications";
39
40pub(crate) const POPUP_TIMEOUT: Duration = Duration::from_secs(5);
42
43const MAX_KEPT: usize = 256;
46
47const MAX_PIXELS_KEPT: usize = 8 * 1024 * 1024;
49
50const EXPIRED: u32 = 1;
52const DISMISSED: u32 = 2;
53const CLOSED: u32 = 3;
54
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
57pub enum Urgency {
58 Low,
60 #[default]
62 Normal,
63 Critical,
66}
67
68#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
70pub enum Timeout {
71 #[default]
73 Default,
74 Never,
76 After(Duration),
78}
79
80#[derive(Clone, Debug, PartialEq)]
82pub struct Notification {
83 pub id: u32,
87 pub app_name: String,
89 pub app_icon: String,
92 pub summary: String,
94 pub body: String,
97 pub actions: Vec<(String, String)>,
100 pub urgency: Urgency,
102 pub timeout: Timeout,
104 pub image: Option<IconSource>,
107 pub desktop_entry: String,
109 pub category: String,
111 pub resident: bool,
113 pub transient: bool,
116 pub received: SystemTime,
118}
119
120#[derive(Clone)]
123pub struct Notifications {
124 service: Option<Rc<Service>>,
126 list: ReadSignal<Vec<Notification>>,
127 popups: ReadSignal<Vec<Notification>>,
128}
129
130impl Notifications {
131 pub fn list(&self) -> ReadSignal<Vec<Notification>> {
135 self.list
136 }
137
138 pub fn popups(&self) -> ReadSignal<Vec<Notification>> {
143 self.popups
144 }
145
146 pub fn dismiss(&self, id: u32) {
149 if let Some(service) = &self.service {
150 service.close(&[id], DISMISSED);
151 }
152 }
153
154 pub fn invoke(&self, id: u32, key: &str) {
158 if let Some(service) = &self.service {
159 service.invoke(id, key);
160 }
161 }
162
163 pub fn clear(&self) {
165 if let Some(service) = &self.service {
166 let state = service.state.borrow();
167 let history = state.as_ref().map(|state| state.history.as_slice());
168 let ids: Vec<u32> = history.unwrap_or_default().iter().map(|n| n.id).collect();
169 drop(state);
170 service.close(&ids, DISMISSED);
171 }
172 }
173}
174
175pub(crate) fn notifications(cx: &Cx) -> Notifications {
177 let Some(service) = cx.service::<Service>() else {
178 static LOGGED: Once = Once::new();
179 LOGGED.call_once(|| {
180 tracing::warn!("cx.notifications() needs the services the shell provides; none arrive");
181 });
182 return Notifications {
183 service: None,
184 list: cx.signal(Vec::new()).read_only(),
185 popups: cx.signal(Vec::new()).read_only(),
186 };
187 };
188 let (list, popups) = service.start();
189 Notifications {
190 service: Some(service),
191 list,
192 popups,
193 }
194}
195
196pub(crate) struct Service {
198 shared: Rc<Shared>,
199 state: RefCell<Option<State>>,
200}
201
202struct State {
203 list: Signal<Vec<Notification>>,
204 popups: Signal<Vec<Notification>>,
205 connection: Option<Connection>,
207 serving: bool,
209 history: Vec<Notification>,
211 showing: Vec<(u32, Option<Instant>)>,
214 next_id: u32,
216 timer: Option<RegistrationToken>,
218}
219
220impl State {
221 fn new_id(&mut self) -> u32 {
224 loop {
225 let id = self.next_id;
226 self.next_id = id.checked_add(1).unwrap_or(1);
227 if !self.history.iter().any(|kept| kept.id == id) {
228 return id;
229 }
230 }
231 }
232}
233
234enum Event {
236 Closed(u32, u32),
238 Invoked(u32, String),
240}
241
242impl Service {
243 pub(crate) fn new(shared: Rc<Shared>) -> Service {
244 Service {
245 shared,
246 state: RefCell::new(None),
247 }
248 }
249
250 fn start(self: &Rc<Self>) -> (ReadSignal<Vec<Notification>>, ReadSignal<Vec<Notification>>) {
253 if let Some(state) = &*self.state.borrow() {
254 return (state.list.read_only(), state.popups.read_only());
255 }
256 let list = self.shared.root.signal(Vec::new());
257 let popups = self.shared.root.signal(Vec::new());
258 *self.state.borrow_mut() = Some(State {
259 list,
260 popups,
261 connection: None,
262 serving: false,
263 history: Vec::new(),
264 showing: Vec::new(),
265 next_id: 1,
266 timer: None,
267 });
268 match self.shared.config.session_bus.clone() {
269 Some(address) => self.shared.spawn(run(Rc::clone(self), address)),
270 None => tracing::info!("no session bus, so no notifications arrive"),
271 }
272 (list.read_only(), popups.read_only())
273 }
274
275 fn alive(&self) -> bool {
278 let state = self.state.borrow();
279 let alive = state.as_ref().and_then(|state| state.list.try_with(|_| ()));
280 alive.is_some()
281 }
282
283 fn serving(&self) -> bool {
284 let state = self.state.borrow();
285 state.as_ref().is_some_and(|state| state.serving)
286 }
287
288 fn notify(self: &Rc<Self>, replaces: u32, mut notification: Notification) -> u32 {
291 let now = Instant::now();
292 let expires = match (notification.urgency, notification.timeout) {
293 (Urgency::Critical, _) | (_, Timeout::Never) => None,
294 (_, Timeout::Default) => Some(now + self.shared.config.popup_timeout),
295 (_, Timeout::After(after)) => Some(now + after),
296 };
297 let (id, dropped) = {
298 let mut state = self.state.borrow_mut();
299 let Some(state) = state.as_mut() else {
300 return 0;
301 };
302 let id = match replaces {
305 0 => state.new_id(),
306 id => id,
307 };
308 notification.id = id;
309 match state.history.iter().position(|kept| kept.id == id) {
310 Some(at) => state.history[at] = notification,
311 None => state.history.push(notification),
312 }
313 match state.showing.iter_mut().find(|(shown, _)| *shown == id) {
314 Some(shown) => shown.1 = expires,
315 None => state.showing.insert(0, (id, expires)),
316 }
317 let excess = state.history.len().saturating_sub(MAX_KEPT);
318 let dropped: Vec<Event> = state
319 .history
320 .drain(..excess)
321 .map(|old| Event::Closed(old.id, EXPIRED))
322 .collect();
323 let history = &state.history;
324 state
325 .showing
326 .retain(|(id, _)| history.iter().any(|kept| kept.id == *id));
327 keep_pixels_within(&mut state.history, MAX_PIXELS_KEPT);
328 (id, dropped)
329 };
330 self.emit(dropped);
331 self.changed();
332 id
333 }
334
335 fn close(self: &Rc<Self>, ids: &[u32], reason: u32) -> bool {
338 let events = self.take(ids, reason);
339 if events.is_empty() {
340 return false;
341 }
342 self.emit(events);
343 self.changed();
344 true
345 }
346
347 fn invoke(self: &Rc<Self>, id: u32, key: &str) {
350 let resident = self.state.borrow().as_ref().and_then(|state| {
351 let kept = state.history.iter().find(|kept| kept.id == id)?;
352 let offered = kept.actions.iter().any(|(action, _)| action == key);
353 offered.then_some(kept.resident)
354 });
355 let Some(resident) = resident else {
356 return tracing::warn!(id, key, "no such notification action");
357 };
358 let mut events = vec![Event::Invoked(id, key.to_owned())];
359 if !resident {
360 events.extend(self.take(&[id], DISMISSED));
361 }
362 self.emit(events);
363 self.changed();
364 }
365
366 fn take(&self, ids: &[u32], reason: u32) -> Vec<Event> {
369 let mut state = self.state.borrow_mut();
370 let Some(state) = state.as_mut() else {
371 return Vec::new();
372 };
373 let taken: Vec<u32> = state
374 .history
375 .iter()
376 .map(|kept| kept.id)
377 .filter(|id| ids.contains(id))
378 .collect();
379 state.history.retain(|kept| !taken.contains(&kept.id));
380 state.showing.retain(|(id, _)| !taken.contains(id));
381 taken
382 .into_iter()
383 .map(|id| Event::Closed(id, reason))
384 .collect()
385 }
386
387 fn expire(self: &Rc<Self>) -> TimeoutAction {
390 if let Some(state) = self.state.borrow_mut().as_mut() {
391 state.timer = None;
393 }
394 if !self.alive() {
395 return TimeoutAction::Drop;
396 }
397 let now = Instant::now();
398 let transient = {
399 let mut state = self.state.borrow_mut();
400 let Some(state) = state.as_mut() else {
401 return TimeoutAction::Drop;
402 };
403 let (expired, showing) = std::mem::take(&mut state.showing)
404 .into_iter()
405 .partition::<Vec<_>, _>(|(_, expires)| expires.is_some_and(|at| at <= now));
406 state.showing = showing;
407 let history = state.history.iter();
408 let transient = history.filter(|kept| kept.transient);
409 transient
410 .map(|kept| kept.id)
411 .filter(|id| expired.iter().any(|(gone, _)| gone == id))
412 .collect::<Vec<_>>()
413 };
414 let events = self.take(&transient, EXPIRED);
415 self.emit(events);
416 self.changed();
417 TimeoutAction::Drop
418 }
419
420 fn changed(self: &Rc<Self>) {
423 let shown = self.state.borrow().as_ref().map(|state| {
425 let popups = state.showing.iter().filter_map(|(id, _)| {
426 let kept = state.history.iter().find(|kept| kept.id == *id);
427 kept.cloned()
428 });
429 let popups = popups.collect::<Vec<_>>();
430 (state.list, state.history.clone(), state.popups, popups)
431 });
432 if let Some((list_signal, list, popups_signal, popups)) = shown {
433 set_changed(list_signal, list);
434 set_changed(popups_signal, popups);
435 }
436 self.schedule();
437 }
438
439 fn schedule(self: &Rc<Self>) {
442 let Some(event_loop) = self.shared.event_loop() else {
443 return;
444 };
445 let mut state = self.state.borrow_mut();
446 let Some(state) = state.as_mut() else {
447 return;
448 };
449 if let Some(token) = state.timer.take() {
450 event_loop.remove(token);
451 }
452 let Some(next) = state.showing.iter().filter_map(|(_, at)| *at).min() else {
453 return;
454 };
455 let service = Rc::clone(self);
456 let tick = Box::new(move || service.expire());
457 state.timer = event_loop.insert_timer(next, tick);
458 }
459
460 fn emit(&self, events: Vec<Event>) {
462 let connection = {
463 let state = self.state.borrow();
464 let serving = state.as_ref().filter(|state| state.serving);
465 serving.and_then(|state| state.connection.clone())
466 };
467 let Some(connection) = connection.filter(|_| !events.is_empty()) else {
468 return;
469 };
470 self.shared.spawn(async move {
471 for event in events {
472 let (path, interface) = (NOTIFICATIONS_PATH, NOTIFICATIONS);
473 let sent = match &event {
474 Event::Closed(id, reason) => {
475 let body = (*id, *reason);
476 let signal = "NotificationClosed";
477 let emit = connection.emit_signal(None::<&str>, path, interface, signal, &body);
478 emit.await
479 }
480 Event::Invoked(id, key) => {
481 let body = (*id, key.as_str());
482 let signal = "ActionInvoked";
483 let emit = connection.emit_signal(None::<&str>, path, interface, signal, &body);
484 emit.await
485 }
486 };
487 if let Err(error) = sent {
488 tracing::warn!(%error, "a notification signal wasn't sent");
489 }
490 }
491 });
492 }
493
494 async fn on_message(self: &Rc<Self>, connection: &Connection, message: &Message) {
496 match message.header().message_type() {
497 Kind::MethodCall => server::answer(self, connection, message).await,
498 Kind::Signal => self.on_signal(message),
499 _ => {}
500 }
501 }
502
503 fn on_signal(&self, message: &Message) {
505 let header = message.header();
506 let from_bus = header.sender().is_some_and(|sender| sender.as_str() == BUS);
507 let ours = message.body().deserialize::<&str>().ok() == Some(NOTIFICATIONS);
508 if !from_bus || !ours {
509 return;
510 }
511 let serving = match header.member().map(|member| member.as_str()) {
512 Some("NameAcquired") => {
513 tracing::info!("serving notifications");
514 true
515 }
516 Some("NameLost") => {
517 tracing::info!("another program took over notifications; waiting for it to leave");
518 false
519 }
520 _ => return,
521 };
522 if let Some(state) = self.state.borrow_mut().as_mut() {
523 state.serving = serving;
524 }
525 }
526}
527
528async fn run(service: Rc<Service>, address: zbus::Address) {
531 let builder = zbus::connection::Builder::address(address);
532 let connection = match builder.build().await {
533 Ok(connection) => connection,
534 Err(error) => {
535 return tracing::info!(%error, "can't reach the session bus, so no notifications arrive");
536 }
537 };
538 let mut messages = MessageStream::from(&connection);
540 if let Some(state) = service.state.borrow_mut().as_mut() {
541 state.connection = Some(connection.clone());
542 }
543 let flags = RequestNameFlags::ALLOW_REPLACEMENT;
546 match connection
547 .request_name_with_flags(NOTIFICATIONS, flags)
548 .await
549 {
550 Ok(RequestNameReply::InQueue) => {
551 tracing::info!("another program serves notifications; waiting for it to leave");
552 }
553 Ok(_) => {}
554 Err(error) => return tracing::warn!(%error, "couldn't ask for the notifications name"),
555 }
556 loop {
557 let next = poll_fn(|cx| Pin::new(&mut messages).poll_next(cx)).await;
558 if !service.alive() {
559 return;
560 }
561 match next {
562 Some(Ok(message)) => service.on_message(&connection, &message).await,
563 Some(Err(error)) => tracing::warn!(%error, "a bad message on the session bus"),
564 None => break,
565 }
566 }
567 tracing::warn!("the session bus closed, so no more notifications arrive");
568 if let Some(state) = service.state.borrow_mut().as_mut() {
569 state.serving = false;
570 state.connection = None;
571 }
572}
573
574fn keep_pixels_within(history: &mut [Notification], budget: usize) {
577 let mut total = 0;
578 for kept in history.iter_mut().rev() {
579 let Some(IconSource::Pixels(pixels)) = &kept.image else {
580 continue;
581 };
582 let bytes = pixels.data.len();
583 if total + bytes <= budget {
584 total += bytes;
585 } else {
586 tracing::debug!(
587 id = kept.id,
588 "past the pictures' budget; showing the app icon"
589 );
590 kept.image = server::icon(&kept.app_icon);
591 }
592 }
593}