Skip to main content

crabshell_services/
notifications.rs

1//! A notification server, as the Desktop Notifications spec 1.2 has it:
2//! apps call `org.freedesktop.Notifications` on the session bus, and the
3//! shell keeps what they send.
4//!
5//! The shell queues for the name, so it serves while no other daemon,
6//! such as mako or dunst, does, and takes over when that one leaves.
7//! zbus has no object server here, so the calls are answered by hand on
8//! the main thread, in [`server`]. One stream reads every message the bus
9//! sends this connection, and one loop timer runs only while a popup
10//! waits to expire, so a quiet bus leaves the shell asleep.
11
12mod server;
13
14#[cfg(test)]
15mod tests;
16
17use std::cell::RefCell;
18use std::future::poll_fn;
19use std::panic::{self, AssertUnwindSafe, Location};
20use std::pin::Pin;
21use std::rc::Rc;
22use std::sync::Once;
23use std::time::{Duration, Instant, SystemTime};
24
25use calloop::RegistrationToken;
26use calloop::timer::TimeoutAction;
27use crabshell_reactive::{Owner, ReadSignal, Signal, untrack};
28use crabshell_ui::{Cx, IconSource};
29use zbus::export::futures_core::Stream;
30use zbus::fdo::{RequestNameFlags, RequestNameReply};
31use zbus::message::Type as Kind;
32use zbus::{Connection, Message, MessageStream};
33
34use crate::Shared;
35use crate::dbus::BUS;
36use crate::dbus::set_changed;
37use crate::lines::panic_message;
38
39const NOTIFICATIONS: &str = "org.freedesktop.Notifications";
40const NOTIFICATIONS_PATH: &str = "/org/freedesktop/Notifications";
41
42/// How long a popup shows when its app leaves that to the server.
43pub(crate) const POPUP_TIMEOUT: Duration = Duration::from_secs(5);
44
45/// The most notifications kept. Past it the oldest goes, as if it
46/// expired.
47const MAX_KEPT: usize = 256;
48
49/// The most pixel bytes the history keeps: 128 pictures of 128 px.
50const MAX_PIXELS_KEPT: usize = 8 * 1024 * 1024;
51
52/// Why a notification closed, from [`Notifications::on_closed`]. Its app
53/// hears the same in `NotificationClosed`.
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum CloseReason {
56  /// It went by itself: a transient one's popup timed out, or it was the
57  /// oldest when the history was full.
58  Expired,
59  /// The user dismissed it, cleared them all, or picked one of its
60  /// actions.
61  Dismissed,
62  /// Its app took it back.
63  CloseRequested,
64}
65
66impl CloseReason {
67  /// Its number in `NotificationClosed`.
68  fn code(self) -> u32 {
69    match self {
70      CloseReason::Expired => 1,
71      CloseReason::Dismissed => 2,
72      CloseReason::CloseRequested => 3,
73    }
74  }
75}
76
77/// How urgent a notification is, from its `urgency` hint.
78#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
79pub enum Urgency {
80  /// It can wait, such as a finished download.
81  Low,
82  /// The usual.
83  #[default]
84  Normal,
85  /// It needs the user now, such as a battery about to run out. Its
86  /// popup stays until dismissed.
87  Critical,
88}
89
90/// How long a notification's popup shows, as its app asks.
91#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
92pub enum Timeout {
93  /// As long as the server likes: 5 s.
94  #[default]
95  Default,
96  /// Until dismissed.
97  Never,
98  /// This long.
99  After(Duration),
100}
101
102/// One notification an app sent.
103#[derive(Clone, Debug, PartialEq)]
104pub struct Notification {
105  /// Never 0, and unique among those kept. The key to give
106  /// `children_keyed`, and what [`Notifications::dismiss`] and
107  /// [`Notifications::invoke`] take.
108  pub id: u32,
109  /// The app's name for itself, such as `Firefox`, or empty.
110  pub app_name: String,
111  /// The app's icon as it sent it: an icon name, a `file://` URI, or
112  /// empty.
113  pub app_icon: String,
114  /// One line about it, at most 1 KiB.
115  pub summary: String,
116  /// The text under that, at most 4 KiB, as plain text: tags an app sent
117  /// are taken out and entities decoded.
118  pub body: String,
119  /// The actions the user can pick, as (key, label), at most 16. The
120  /// app means `default` for a click on the notification itself.
121  pub actions: Vec<(String, String)>,
122  /// How urgent it is.
123  pub urgency: Urgency,
124  /// How long its popup shows.
125  pub timeout: Timeout,
126  /// The picture to show: the app's pixels, an image file, or else its
127  /// icon. `None` when it has none.
128  pub image: Option<IconSource>,
129  /// The app's desktop entry, such as `org.mozilla.firefox`, or empty.
130  pub desktop_entry: String,
131  /// What kind it is, such as `email.arrived`, or empty.
132  pub category: String,
133  /// Whether it stays after one of its actions is picked.
134  pub resident: bool,
135  /// Whether it goes once its popup expires, instead of staying in the
136  /// history.
137  pub transient: bool,
138  /// When it arrived, or was last replaced.
139  pub received: SystemTime,
140}
141
142/// The notifications, from `cx.notifications()`. A clone is cheap and
143/// shares the one server.
144#[derive(Clone)]
145pub struct Notifications {
146  /// `None` when the shell provided no services.
147  service: Option<Rc<Service>>,
148  list: ReadSignal<Vec<Notification>>,
149  popups: ReadSignal<Vec<Notification>>,
150  /// The asking context's owner, which [`on_closed`](Self::on_closed)'s
151  /// handlers live under.
152  owner: Owner,
153}
154
155impl Notifications {
156  /// Every notification kept, oldest first: the history, from arrival
157  /// until it is dismissed, picked or closed by its app. A replaced one
158  /// keeps its place.
159  pub fn list(&self) -> ReadSignal<Vec<Notification>> {
160    self.list
161  }
162
163  /// The notifications still showing as popups, newest first. Each stays
164  /// for its timeout, and until dismissed when that is
165  /// [`Timeout::Never`] or it is [`Urgency::Critical`]. An expired one
166  /// stays in [`list`](Self::list), unless it is transient.
167  pub fn popups(&self) -> ReadSignal<Vec<Notification>> {
168    self.popups
169  }
170
171  /// Takes notification `id` away, as the user's close button does, and
172  /// tells its app. An unknown `id` is ignored.
173  pub fn dismiss(&self, id: u32) {
174    if let Some(service) = &self.service {
175      service.close(&[id], CloseReason::Dismissed);
176    }
177  }
178
179  /// Tells notification `id`'s app that the user picked its action
180  /// `key`, then takes it away unless it is resident. An unknown `id` or
181  /// `key` is logged.
182  pub fn invoke(&self, id: u32, key: &str) {
183    if let Some(service) = &self.service {
184      service.invoke(id, key);
185    }
186  }
187
188  /// Dismisses every notification.
189  pub fn clear(&self) {
190    if let Some(service) = &self.service {
191      let state = service.state.borrow();
192      let history = state.as_ref().map(|state| state.history.as_slice());
193      let ids: Vec<u32> = history.unwrap_or_default().iter().map(|n| n.id).collect();
194      drop(state);
195      service.close(&ids, CloseReason::Dismissed);
196    }
197  }
198
199  /// Calls `handler` with each notification that leaves the history and
200  /// why, as Quickshell's `Notification.closed`, for as long as the
201  /// context `cx.notifications()` was called on lives. It runs on the
202  /// main thread once the lists no longer show it. A popup that times out
203  /// stays in the history, so it hasn't closed, unless it is transient.
204  ///
205  /// If it panics, that is logged with where `on_closed` was called, and
206  /// it isn't called again. Called on a handle whose context went, it
207  /// logs that and does nothing.
208  #[track_caller]
209  pub fn on_closed(&self, handler: impl Fn(&Notification, CloseReason) + 'static) {
210    let at = Location::caller();
211    if self.owner.is_disposed() {
212      return tracing::warn!(
213        "notifications on_closed (called at {at}) on a context that is gone does nothing"
214      );
215    }
216    if let Some(service) = &self.service {
217      let id = service.listen(Rc::new(handler), at);
218      // Dropped with the asking context's owner, which takes the handler away.
219      self.owner.signal(Unlisten(Rc::clone(service), id));
220    }
221  }
222}
223
224/// What `on_closed` calls.
225type CloseHandler = Rc<dyn Fn(&Notification, CloseReason)>;
226
227/// A config's handler of closed notifications, from `on_closed`.
228#[derive(Clone)]
229struct Listener {
230  id: u64,
231  handler: CloseHandler,
232  /// Where `on_closed` was called, for the log if it panics.
233  at: &'static Location<'static>,
234}
235
236/// Takes a listener away when its context's owner drops this.
237struct Unlisten(Rc<Service>, u64);
238
239impl Drop for Unlisten {
240  fn drop(&mut self) {
241    // Dropped outside the borrow: the handler may hold a `Notifications`.
242    drop(self.0.unlisten(self.1));
243  }
244}
245
246/// `cx.notifications()`.
247pub(crate) fn notifications(cx: &Cx) -> Notifications {
248  let Some(service) = cx.service::<Service>() else {
249    static LOGGED: Once = Once::new();
250    LOGGED.call_once(|| {
251      tracing::warn!("cx.notifications() needs the services the shell provides; none arrive");
252    });
253    return Notifications {
254      service: None,
255      list: cx.signal(Vec::new()).read_only(),
256      popups: cx.signal(Vec::new()).read_only(),
257      owner: cx.owner(),
258    };
259  };
260  let (list, popups) = service.start();
261  Notifications {
262    service: Some(service),
263    list,
264    popups,
265    owner: cx.owner(),
266  }
267}
268
269/// The notification server of one shell, started on first use.
270pub(crate) struct Service {
271  shared: Rc<Shared>,
272  state: RefCell<Option<State>>,
273}
274
275struct State {
276  list: Signal<Vec<Notification>>,
277  popups: Signal<Vec<Notification>>,
278  /// The session bus, once connected.
279  connection: Option<Connection>,
280  /// Whether this shell owns the name now, so it answers and signals.
281  serving: bool,
282  /// Every notification kept, oldest first.
283  history: Vec<Notification>,
284  /// The ids showing as popups, newest first, each with when it expires,
285  /// `None` for never.
286  showing: Vec<(u32, Option<Instant>)>,
287  /// The id the next notification gets, unless one kept has it.
288  next_id: u32,
289  /// The expiry timer, while a popup waits to expire.
290  timer: Option<RegistrationToken>,
291  /// The config's close handlers, in the order given.
292  listeners: Vec<Listener>,
293  /// The id the next listener gets.
294  next_listener: u64,
295}
296
297impl State {
298  /// A new id: the count goes on from the last, skipping 0 when it wraps
299  /// and any id still kept.
300  fn new_id(&mut self) -> u32 {
301    loop {
302      let id = self.next_id;
303      self.next_id = id.checked_add(1).unwrap_or(1);
304      if !self.history.iter().any(|kept| kept.id == id) {
305        return id;
306      }
307    }
308  }
309}
310
311/// A signal for the apps, sent in order.
312enum Event {
313  /// `NotificationClosed`: the id and why.
314  Closed(u32, CloseReason),
315  /// `ActionInvoked`: the id and the action's key.
316  Invoked(u32, String),
317}
318
319impl Service {
320  pub(crate) fn new(shared: Rc<Shared>) -> Service {
321    Service {
322      shared,
323      state: RefCell::new(None),
324    }
325  }
326
327  /// The history and the popups, joining the bus if this is the first
328  /// ask.
329  fn start(self: &Rc<Self>) -> (ReadSignal<Vec<Notification>>, ReadSignal<Vec<Notification>>) {
330    if let Some(state) = &*self.state.borrow() {
331      return (state.list.read_only(), state.popups.read_only());
332    }
333    let list = self.shared.root.signal(Vec::new());
334    let popups = self.shared.root.signal(Vec::new());
335    *self.state.borrow_mut() = Some(State {
336      list,
337      popups,
338      connection: None,
339      serving: false,
340      history: Vec::new(),
341      showing: Vec::new(),
342      next_id: 1,
343      timer: None,
344      listeners: Vec::new(),
345      next_listener: 0,
346    });
347    match self.shared.config.session_bus.clone() {
348      Some(address) => self.shared.spawn(run(Rc::clone(self), address)),
349      None => tracing::info!("no session bus, so no notifications arrive"),
350    }
351    (list.read_only(), popups.read_only())
352  }
353
354  /// Whether the history is still there: its owner, the root, isn't
355  /// disposed.
356  fn alive(&self) -> bool {
357    let state = self.state.borrow();
358    let alive = state.as_ref().and_then(|state| state.list.try_with(|_| ()));
359    alive.is_some()
360  }
361
362  fn serving(&self) -> bool {
363    let state = self.state.borrow();
364    state.as_ref().is_some_and(|state| state.serving)
365  }
366
367  /// Keeps `notification`, in place of `replaces` if that one is kept,
368  /// and shows it as a popup. Returns its id.
369  fn notify(self: &Rc<Self>, replaces: u32, mut notification: Notification) -> u32 {
370    let now = Instant::now();
371    let expires = match (notification.urgency, notification.timeout) {
372      (Urgency::Critical, _) | (_, Timeout::Never) => None,
373      (_, Timeout::Default) => Some(now + self.shared.config.popup_timeout),
374      (_, Timeout::After(after)) => Some(now + after),
375    };
376    let (id, dropped) = {
377      let mut state = self.state.borrow_mut();
378      let Some(state) = state.as_mut() else {
379        return 0;
380      };
381      // The spec returns `replaces` even when it is gone, so it names
382      // the new one then.
383      let id = match replaces {
384        0 => state.new_id(),
385        id => id,
386      };
387      notification.id = id;
388      match state.history.iter().position(|kept| kept.id == id) {
389        Some(at) => state.history[at] = notification,
390        None => state.history.push(notification),
391      }
392      match state.showing.iter_mut().find(|(shown, _)| *shown == id) {
393        Some(shown) => shown.1 = expires,
394        None => state.showing.insert(0, (id, expires)),
395      }
396      let excess = state.history.len().saturating_sub(MAX_KEPT);
397      let dropped: Vec<Notification> = state.history.drain(..excess).collect();
398      let history = &state.history;
399      state
400        .showing
401        .retain(|(id, _)| history.iter().any(|kept| kept.id == *id));
402      keep_pixels_within(&mut state.history, MAX_PIXELS_KEPT);
403      (id, dropped)
404    };
405    self.closed(Vec::new(), dropped, CloseReason::Expired);
406    id
407  }
408
409  /// Takes `ids` away and tells their apps `reason`. Returns whether any
410  /// was kept.
411  fn close(self: &Rc<Self>, ids: &[u32], reason: CloseReason) -> bool {
412    let taken = self.take(ids);
413    if taken.is_empty() {
414      return false;
415    }
416    self.closed(Vec::new(), taken, reason);
417    true
418  }
419
420  /// Picks action `key` of notification `id`, as [`Notifications::invoke`]
421  /// does.
422  fn invoke(self: &Rc<Self>, id: u32, key: &str) {
423    let resident = self.state.borrow().as_ref().and_then(|state| {
424      let kept = state.history.iter().find(|kept| kept.id == id)?;
425      let offered = kept.actions.iter().any(|(action, _)| action == key);
426      offered.then_some(kept.resident)
427    });
428    let Some(resident) = resident else {
429      return tracing::warn!(id, key, "no such notification action");
430    };
431    let events = vec![Event::Invoked(id, key.to_owned())];
432    let taken = if resident {
433      Vec::new()
434    } else {
435      self.take(&[id])
436    };
437    self.closed(events, taken, CloseReason::Dismissed);
438  }
439
440  /// Takes `ids` out of the history and the popups, and returns those
441  /// that were kept, oldest first.
442  fn take(&self, ids: &[u32]) -> Vec<Notification> {
443    let mut state = self.state.borrow_mut();
444    let Some(state) = state.as_mut() else {
445      return Vec::new();
446    };
447    let (taken, kept): (Vec<_>, Vec<_>) = std::mem::take(&mut state.history)
448      .into_iter()
449      .partition(|kept| ids.contains(&kept.id));
450    state.history = kept;
451    state.showing.retain(|(id, _)| !ids.contains(id));
452    taken
453  }
454
455  /// Tells the apps `events`, then that `closed` went for `reason`, shows
456  /// the lists without them, and then calls the config's handlers.
457  fn closed(
458    self: &Rc<Self>,
459    mut events: Vec<Event>,
460    closed: Vec<Notification>,
461    reason: CloseReason,
462  ) {
463    events.extend(closed.iter().map(|gone| Event::Closed(gone.id, reason)));
464    self.emit(events);
465    self.changed();
466    self.deliver(&closed, reason);
467  }
468
469  /// Adds a config's close handler. Its id.
470  fn listen(&self, handler: CloseHandler, at: &'static Location<'static>) -> u64 {
471    let mut state = self.state.borrow_mut();
472    let state = state
473      .as_mut()
474      .expect("invariant: started by cx.notifications()");
475    state.next_listener += 1;
476    let id = state.next_listener;
477    state.listeners.push(Listener { id, handler, at });
478    id
479  }
480
481  /// Takes a handler away, handing it back to drop outside the borrow.
482  fn unlisten(&self, id: u64) -> Option<Listener> {
483    let mut state = self.state.borrow_mut();
484    let listeners = &mut state.as_mut()?.listeners;
485    let index = listeners.iter().position(|listener| listener.id == id)?;
486    Some(listeners.remove(index))
487  }
488
489  fn listening(&self, id: u64) -> bool {
490    let state = self.state.borrow();
491    let mut listeners = state.iter().flat_map(|state| &state.listeners);
492    listeners.any(|listener| listener.id == id)
493  }
494
495  /// Hands each of `closed` to the config's handlers, with nothing
496  /// borrowed, since a handler may call the service, and untracked, so
497  /// what one reads never reruns an effect that closed a notification. A
498  /// handler that panics is logged with where `on_closed` was called, and
499  /// taken away.
500  fn deliver(&self, closed: &[Notification], reason: CloseReason) {
501    let listeners = match self.state.borrow().as_ref() {
502      Some(state) if !closed.is_empty() => state.listeners.clone(),
503      _ => return,
504    };
505    for gone in closed {
506      for Listener { id, handler, at } in &listeners {
507        // A handler before this one may have taken this one's context.
508        if !self.listening(*id) {
509          continue;
510        }
511        let run = AssertUnwindSafe(|| untrack(|| handler(gone, reason)));
512        if let Err(payload) = panic::catch_unwind(run) {
513          let message = panic_message(payload.as_ref());
514          tracing::error!("notifications on_closed (called at {at}) panicked: {message}");
515          drop(self.unlisten(*id));
516        }
517      }
518    }
519  }
520
521  /// The timer's tick: takes the expired popups away, and the transient
522  /// ones out of the history too.
523  fn expire(self: &Rc<Self>) -> TimeoutAction {
524    if let Some(state) = self.state.borrow_mut().as_mut() {
525      // This timer goes once the tick returns.
526      state.timer = None;
527    }
528    if !self.alive() {
529      return TimeoutAction::Drop;
530    }
531    let now = Instant::now();
532    let transient = {
533      let mut state = self.state.borrow_mut();
534      let Some(state) = state.as_mut() else {
535        return TimeoutAction::Drop;
536      };
537      let (expired, showing) = std::mem::take(&mut state.showing)
538        .into_iter()
539        .partition::<Vec<_>, _>(|(_, expires)| expires.is_some_and(|at| at <= now));
540      state.showing = showing;
541      let history = state.history.iter();
542      let transient = history.filter(|kept| kept.transient);
543      transient
544        .map(|kept| kept.id)
545        .filter(|id| expired.iter().any(|(gone, _)| gone == id))
546        .collect::<Vec<_>>()
547    };
548    let taken = self.take(&transient);
549    self.closed(Vec::new(), taken, CloseReason::Expired);
550    TimeoutAction::Drop
551  }
552
553  /// Shows the history and popups as they are now, and sets the timer
554  /// for the next popup to expire.
555  fn changed(self: &Rc<Self>) {
556    // Set apart from the borrow, since effects may ask for more.
557    let shown = self.state.borrow().as_ref().map(|state| {
558      let popups = state.showing.iter().filter_map(|(id, _)| {
559        let kept = state.history.iter().find(|kept| kept.id == *id);
560        kept.cloned()
561      });
562      let popups = popups.collect::<Vec<_>>();
563      (state.list, state.history.clone(), state.popups, popups)
564    });
565    if let Some((list_signal, list, popups_signal, popups)) = shown {
566      set_changed(list_signal, list);
567      set_changed(popups_signal, popups);
568    }
569    self.schedule();
570  }
571
572  /// Sets the one timer afresh, at the soonest expiry, or none while no
573  /// popup expires.
574  fn schedule(self: &Rc<Self>) {
575    let Some(event_loop) = self.shared.event_loop() else {
576      return;
577    };
578    let mut state = self.state.borrow_mut();
579    let Some(state) = state.as_mut() else {
580      return;
581    };
582    if let Some(token) = state.timer.take() {
583      event_loop.remove(token);
584    }
585    let Some(next) = state.showing.iter().filter_map(|(_, at)| *at).min() else {
586      return;
587    };
588    let service = Rc::clone(self);
589    let tick = Box::new(move || service.expire());
590    state.timer = event_loop.insert_timer(next, tick);
591  }
592
593  /// Sends `events` in order, while this shell serves.
594  fn emit(&self, events: Vec<Event>) {
595    let connection = {
596      let state = self.state.borrow();
597      let serving = state.as_ref().filter(|state| state.serving);
598      serving.and_then(|state| state.connection.clone())
599    };
600    let Some(connection) = connection.filter(|_| !events.is_empty()) else {
601      return;
602    };
603    self.shared.spawn(async move {
604      for event in events {
605        let (path, interface) = (NOTIFICATIONS_PATH, NOTIFICATIONS);
606        let sent = match &event {
607          Event::Closed(id, reason) => {
608            let body = (*id, reason.code());
609            let signal = "NotificationClosed";
610            let emit = connection.emit_signal(None::<&str>, path, interface, signal, &body);
611            emit.await
612          }
613          Event::Invoked(id, key) => {
614            let body = (*id, key.as_str());
615            let signal = "ActionInvoked";
616            let emit = connection.emit_signal(None::<&str>, path, interface, signal, &body);
617            emit.await
618          }
619        };
620        if let Err(error) = sent {
621          tracing::warn!(%error, "a notification signal wasn't sent");
622        }
623      }
624    });
625  }
626
627  /// Acts on one message from the bus.
628  async fn on_message(self: &Rc<Self>, connection: &Connection, message: &Message) {
629    match message.header().message_type() {
630      Kind::MethodCall => server::answer(self, connection, message).await,
631      Kind::Signal => self.on_signal(message),
632      _ => {}
633    }
634  }
635
636  /// The bus handing this shell the name, or taking it away.
637  fn on_signal(&self, message: &Message) {
638    let header = message.header();
639    let from_bus = header.sender().is_some_and(|sender| sender.as_str() == BUS);
640    let ours = message.body().deserialize::<&str>().ok() == Some(NOTIFICATIONS);
641    if !from_bus || !ours {
642      return;
643    }
644    let serving = match header.member().map(|member| member.as_str()) {
645      Some("NameAcquired") => {
646        tracing::info!("serving notifications");
647        true
648      }
649      Some("NameLost") => {
650        tracing::info!("another program took over notifications; waiting for it to leave");
651        false
652      }
653      _ => return,
654    };
655    if let Some(state) = self.state.borrow_mut().as_mut() {
656      state.serving = serving;
657    }
658  }
659}
660
661/// Joins the session bus at `address`, queues for the name, and acts on
662/// what arrives, until the bus closes or the history is disposed.
663async fn run(service: Rc<Service>, address: zbus::Address) {
664  let builder = zbus::connection::Builder::address(address);
665  let connection = match builder.build().await {
666    Ok(connection) => connection,
667    Err(error) => {
668      return tracing::info!(%error, "can't reach the session bus, so no notifications arrive");
669    }
670  };
671  // Made before anything is asked, so no call or NameAcquired is missed.
672  let mut messages = MessageStream::from(&connection);
673  if let Some(state) = service.state.borrow_mut().as_mut() {
674    state.connection = Some(connection.clone());
675  }
676  // Queued rather than refused, so the bus hands the name over when the
677  // daemon that has it leaves. NameAcquired says so, in `on_signal`.
678  let flags = RequestNameFlags::ALLOW_REPLACEMENT;
679  match connection
680    .request_name_with_flags(NOTIFICATIONS, flags)
681    .await
682  {
683    Ok(RequestNameReply::InQueue) => {
684      tracing::info!("another program serves notifications; waiting for it to leave");
685    }
686    Ok(_) => {}
687    Err(error) => return tracing::warn!(%error, "couldn't ask for the notifications name"),
688  }
689  loop {
690    let next = poll_fn(|cx| Pin::new(&mut messages).poll_next(cx)).await;
691    if !service.alive() {
692      return;
693    }
694    match next {
695      Some(Ok(message)) => service.on_message(&connection, &message).await,
696      Some(Err(error)) => tracing::warn!(%error, "a bad message on the session bus"),
697      None => break,
698    }
699  }
700  tracing::warn!("the session bus closed, so no more notifications arrive");
701  if let Some(state) = service.state.borrow_mut().as_mut() {
702    state.serving = false;
703    state.connection = None;
704  }
705}
706
707/// Drops the pixels of the oldest notifications past `budget` bytes in
708/// all, each falling back to its app's icon.
709fn keep_pixels_within(history: &mut [Notification], budget: usize) {
710  let mut total = 0;
711  for kept in history.iter_mut().rev() {
712    let Some(IconSource::Pixels(pixels)) = &kept.image else {
713      continue;
714    };
715    let bytes = pixels.data.len();
716    if total + bytes <= budget {
717      total += bytes;
718    } else {
719      tracing::debug!(
720        id = kept.id,
721        "past the pictures' budget; showing the app icon"
722      );
723      kept.image = server::icon(&kept.app_icon);
724    }
725  }
726}