Skip to main content

sioul_sync/
notify.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! The one exception to the admin windows: a quiet desktop notification for a
5//! verified code, with the code in it (docs/porch.md, "Right now"); a gentle
6//! reminder; new mail at the times it may come, once per batch
7//! (docs/porch.md, "Notifications"); and the time running, while a focus
8//! session runs (docs/tasks.md).
9//!
10//! On Linux and the BSDs each notification goes to the session's
11//! notification server over a D-Bus connection of its own, made by
12//! `crate::bus`: a session bus that takes it and never answers holds one
13//! thread of its own, never the caller nor libdbus's process-wide locks
14//! (review of battery part C, G1). Its call is the one notify-rust's
15//! `Notification` made before 13 October 2026, argument for argument
16//! (`notify_message`), but for its urgency, said once (notify-rust's hints
17//! are a set, where a mail's normal urgency and the critical one it started
18//! from were two, sent in no fixed order). Windows and macOS: notify-rust.
19//!
20//! A dose's reminder (`remind_dose`) is built on one rule: never invite a
21//! second dose, and when this device cannot know, say the doubt (docs/health.md,
22//! "Knowing"). What became of its first try is said in three ways, recorded
23//! as such: shown (the server answered while the tick waited), not shown
24//! (surely not gone out), or maybe (sent, its fate not known), which a
25//! phone's alarm then reminds with the doubt said first. Whatever shows late
26//! on this computer (a try after the first, or the first answered late) says
27//! the doubt first; tries after the first end before the phone's alarm could
28//! ring.
29
30#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
31use dbus::blocking::LocalConnection;
32#[cfg(any(windows, target_os = "macos"))]
33use notify_rust::{Notification, Timeout};
34#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
35use std::cell::RefCell;
36#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
37use std::rc::Rc;
38#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
39use std::sync::mpsc::{Receiver, Sender};
40use std::sync::{Arc, Condvar, Mutex};
41use std::time::Duration;
42#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
43use std::time::Instant;
44
45/// How long the notification stays: about the life of a code.
46#[cfg(not(target_os = "android"))]
47const SHOWN: u32 = 10 * 60 * 1000;
48
49/// How long the tick waits for what became of a dose reminder's first try
50/// (`Sending::outcome`): its whole first try fits in it (`TRY`), the
51/// connection's setup, its two matches, the server's two answers.
52pub const FIRST_TRY: Duration = Duration::from_secs(10);
53
54/// What a dose reminder's first try became, as the tick records it.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum Outcome {
57    /// The notification server answered the Notify while the tick waited: shown.
58    Shown,
59    /// It surely did not go out (no connection, no notification server, the
60    /// bus or the server said no, nothing sent in time): why.
61    Unshown(String),
62    /// Sent, and what became of it is not known (no answer in time, the bus
63    /// stalled under it, the tick done waiting): it may be on the screen. Why.
64    Maybe(String),
65    /// Not sent: no longer to remind when it was about to go (marked meanwhile).
66    Gone,
67}
68
69/// What a dose's reminder records, and asks, as it goes (docs/health.md,
70/// "What became of a reminder"), so that another device knows what this one
71/// did before it reminds the dose itself: each notice recorded before it
72/// goes (`going`), what became of it after. Its own device's entry alone
73/// (`sioul_core::dose_reminders`).
74pub trait DoseRecord: Send {
75    /// A notice of the dose is about to go: recorded so first ("maybe": it
76    /// may be on the screen from now on), then its words now. `Say::Nothing`:
77    /// no longer to remind (marked, answered or reminded anywhere read here,
78    /// the doses kept elsewhere now): nothing sent, nothing more tried.
79    fn going(&mut self, going: Going) -> Say;
80    /// The notice just recorded as going was refused (the bus or the server
81    /// said no, or it could not be sent): it did not show, taken back.
82    fn refused(&mut self);
83    /// One was shown: the server answered (late, or in time).
84    fn shown(&mut self);
85    /// The first try surely did not go out: nothing was sent.
86    fn unshown(&mut self);
87    /// The tries pause, a phone's alarm due meanwhile: promised, no notice
88    /// until `until` (Unix seconds); false when no longer to remind (no more tries).
89    fn pause(&mut self, until: i64) -> bool;
90    /// No more tries (the grace's end): promised, no notice again.
91    fn over(&mut self);
92    /// Whether the dose was marked or answered on any device, as read here
93    /// now: a notice of it still shown is taken away.
94    fn settled(&mut self) -> bool;
95}
96
97/// Which notice is about to go (`DoseRecord::going`).
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum Going {
100    /// The first try, while the tick waits: its own words.
101    First,
102    /// Shown late, the doubt first: `Late::Retry` a try after the first, or
103    /// the first once the tick stopped waiting; `Late::Unconfirmed` the first,
104    /// on the screen since its time, answered late (said again in place).
105    Late(Late),
106}
107
108/// Why a reminder shows late (`Going::Late`): its words differ.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum Late {
111    /// Its first try could not be shown in time.
112    Retry,
113    /// It was shown in time, but not confirmed in time.
114    Unconfirmed,
115}
116
117/// What a notice about to go says (`DoseRecord::going`).
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum Say {
120    /// Its own words, as given to `remind_dose`.
121    Own,
122    /// These words: title and body.
123    Words(String, String),
124    /// Nothing: no longer to remind.
125    Nothing,
126}
127
128/// What a dose's reminder does after its first try (`remind_dose`), Linux and
129/// the BSDs; elsewhere the first try is the only one:
130/// - not gone out: tried again on its own thread, the doubt first, five
131///   seconds apart, then half a minute (`again_in`), until `end` (its
132///   grace), then `DoseRecord::over`; during `quiet` (a phone's alarm rings
133///   then), none: promised before it begins (`DoseRecord::pause`), resumed
134///   after it while still to remind;
135/// - sent and not answered in time: never sent again (it may be on the
136///   screen), its answer still heard on its connection, however late, and
137///   its button; answered, said again in place with the doubt first.
138///
139/// A notice shown is taken away once its dose is marked or answered anywhere
140/// read here (`DoseRecord::settled`).
141pub struct Again {
142    pub end: std::time::Instant,
143    /// From, to, and "to" in Unix seconds (the promise's words).
144    pub quiet: Option<(std::time::Instant, std::time::Instant, i64)>,
145    pub record: Box<dyn DoseRecord>,
146}
147
148/// A dose's reminder under way (`remind_dose`): what became of its first
149/// try, said by its thread while the tick waits; once the tick stopped
150/// waiting (`outcome` returned, or this dropped), what shows is late.
151pub struct Sending(Arc<(Mutex<Slot>, Condvar)>);
152
153/// Where a first try's outcome is left for the tick, and whether the tick
154/// still waits for it.
155#[derive(Default)]
156struct Slot {
157    outcome: Option<Outcome>,
158    closed: bool,
159}
160
161impl Sending {
162    fn new() -> Sending {
163        Sending(Arc::default())
164    }
165
166    /// A first try whose outcome is known at once.
167    #[cfg(not(all(unix, not(any(target_os = "macos", target_os = "android")))))]
168    fn known(outcome: Outcome) -> Sending {
169        let sending = Sending::new();
170        let _ = told(&sending.0, outcome);
171        sending
172    }
173
174    /// What the first try became, waited for `wait` at most: not said by
175    /// then, `Maybe` (it may still show: late, the doubt said, `Again`). The
176    /// tick waits no more after this.
177    pub fn outcome(&self, wait: Duration) -> Outcome {
178        let (slot, bell) = &*self.0;
179        let Ok(guard) = slot.lock() else { return Outcome::Maybe("the notification's outcome could not be read".to_string()) };
180        let Ok((mut guard, _)) = bell.wait_timeout_while(guard, wait, |slot| slot.outcome.is_none()) else { return Outcome::Maybe("the notification's outcome could not be read".to_string()) };
181        guard.closed = true;
182        guard.outcome.take().unwrap_or_else(|| Outcome::Maybe("the notification server did not answer in time".to_string()))
183    }
184}
185
186impl Drop for Sending {
187    fn drop(&mut self) {
188        if let Ok(mut slot) = self.0.0.lock() {
189            slot.closed = true;
190        }
191    }
192}
193
194/// A first try's outcome left for the tick: whether the tick still waited
195/// for it (else what shows now shows late).
196fn told(sending: &Arc<(Mutex<Slot>, Condvar)>, outcome: Outcome) -> bool {
197    let (slot, bell) = &**sending;
198    let Ok(mut slot) = slot.lock() else { return false };
199    if slot.closed {
200        return false;
201    }
202    slot.outcome = Some(outcome);
203    bell.notify_all();
204    true
205}
206
207/// Sioul's bundle id on macOS, as `packaging/macos/Info.plist` names it
208/// (`CFBundleIdentifier`). macOS shows a notification as coming from the
209/// application whose id it is given; without one, notify-rust's macOS side
210/// (mac-notification-sys) names another application.
211pub const BUNDLE_ID: &str = "com.aurelienpierre.Sioul";
212
213/// macOS: Sioul's notifications said to come from Sioul (`BUNDLE_ID`), once
214/// per process, before the first is shown: macOS takes the application only
215/// once.
216#[cfg(target_os = "macos")]
217fn as_sioul() {
218    static ONCE: std::sync::Once = std::sync::Once::new();
219    ONCE.call_once(|| {
220        let _ = notify_rust::set_application(BUNDLE_ID);
221    });
222}
223
224/// Shows a code: `title` says what and from whom, `body` the code and how long
225/// it lasts. No sound. With `copy`, the notification offers a button labelled
226/// with its text, and its action runs when the button is pressed; the
227/// notification then lives on its own thread, which waits for the button.
228#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
229pub fn code(title: &str, body: &str, copy: Option<(String, Box<dyn FnOnce() + Send>)>) -> Result<(), String> {
230    one_button(Notice::new(title, body, "dialog-password", CRITICAL), copy)
231}
232
233/// A gentle reminder: one notification, no sound, with one button whose
234/// action runs when pressed (a dose's own: `remind_dose`).
235#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
236pub fn remind(title: &str, body: &str, action: Option<(String, Box<dyn FnOnce() + Send>)>) -> Result<(), String> {
237    one_button(Notice::new(title, body, "appointment-soon", CRITICAL), action)
238}
239
240/// A dose's reminder (docs/health.md, "Knowing"): what `remind` shows with
241/// its button ("Taken"), on a thread of its own, whose first try fits in
242/// `FIRST_TRY` and says what it became (`Sending::outcome`); then as `Again`
243/// says. "Taken" runs once, however late it is pressed while the notice lives.
244#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
245pub fn remind_dose(title: &str, body: &str, button: (String, Box<dyn FnOnce() + Send>), again: Again) -> Sending {
246    let mut notice = Notice::new(title, body, "appointment-soon", CRITICAL);
247    let (label, run) = button;
248    notice.buttons.push(("copy".to_string(), label));
249    let pressed: Pressed = Box::new(move |key: &str| {
250        if key == "copy" {
251            run();
252        }
253    });
254    let sending = Sending::new();
255    let slot = Arc::clone(&sending.0);
256    // The D-Bus connection cannot change threads, so it is made where it waits.
257    std::thread::spawn(move || first_try(notice, again, pressed, &slot));
258    sending
259}
260
261/// Elsewhere, the reminder as `remind` shows it, recorded before it goes,
262/// its outcome known at once; no try after the first.
263#[cfg(not(all(unix, not(any(target_os = "macos", target_os = "android")))))]
264pub fn remind_dose(title: &str, body: &str, button: (String, Box<dyn FnOnce() + Send>), mut again: Again) -> Sending {
265    let (title, body) = match again.record.going(Going::First) {
266        Say::Own => (title.to_string(), body.to_string()),
267        Say::Words(title, body) => (title, body),
268        Say::Nothing => return Sending::known(Outcome::Gone),
269    };
270    Sending::known(match remind(&title, &body, Some(button)) {
271        Ok(()) => {
272            again.record.shown();
273            Outcome::Shown
274        }
275        Err(e) => {
276            again.record.refused();
277            again.record.unshown();
278            Outcome::Unshown(e)
279        }
280    })
281}
282
283/// New mail at the times it may come (docs/porch.md, "Notifications"): one
284/// notification for the batch, no sound, with one button ("Open": the
285/// Porch) whose action runs when pressed. At normal urgency, unlike codes
286/// and reminders: the desktop's own do-not-disturb holds it; but at critical
287/// urgency when someone Always through is in it (`through`), which passes it
288/// (docs/attention.md, Q10).
289#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
290pub fn mail(title: &str, body: &str, through: bool, action: Option<(String, Box<dyn FnOnce() + Send>)>) -> Result<(), String> {
291    one_button(Notice::new(title, body, "mail-unread", if through { CRITICAL } else { NORMAL }), action)
292}
293
294/// Windows and macOS: the mail's words alone; their notifications carry no button here.
295#[cfg(any(windows, target_os = "macos"))]
296pub fn mail(title: &str, body: &str, _through: bool, _action: Option<(String, Box<dyn FnOnce() + Send>)>) -> Result<(), String> {
297    #[cfg(target_os = "macos")]
298    as_sioul();
299    Notification::new().appname("Sioul").summary(title).body(body).timeout(Timeout::Milliseconds(SHOWN)).show().map(drop).map_err(|e| e.to_string())
300}
301
302/// Android: made on the window's side, through Java (sioul-app's `eventalarms`).
303#[cfg(target_os = "android")]
304pub fn mail(_title: &str, _body: &str, _through: bool, _action: Option<(String, Box<dyn FnOnce() + Send>)>) -> Result<(), String> {
305    Err("notifications are made through Java on Android".to_string())
306}
307
308/// A gentle reminder with several buttons, each its label and its action:
309/// the one pressed runs (the night's notice: "Close the day" and "Options…").
310#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
311pub fn remind_choices(title: &str, body: &str, choices: Vec<(String, Box<dyn FnOnce() + Send>)>) -> Result<(), String> {
312    if choices.is_empty() {
313        return remind(title, body, None);
314    }
315    let mut notice = Notice::new(title, body, "appointment-soon", CRITICAL);
316    let mut runs: Vec<Option<Box<dyn FnOnce() + Send>>> = Vec::new();
317    for (index, (label, run)) in choices.into_iter().enumerate() {
318        notice.buttons.push((format!("choice-{index}"), label));
319        runs.push(Some(run));
320    }
321    notify(
322        notice,
323        Some(Box::new(move |key: &str| {
324            if let Some(run) = key.strip_prefix("choice-").and_then(|n| n.parse::<usize>().ok()).and_then(|n| runs.get_mut(n)).and_then(Option::take) {
325                run();
326            }
327        })),
328    )
329}
330
331/// Elsewhere, as a reminder without buttons (`remind`).
332#[cfg(not(all(unix, not(any(target_os = "macos", target_os = "android")))))]
333pub fn remind_choices(title: &str, body: &str, _choices: Vec<(String, Box<dyn FnOnce() + Send>)>) -> Result<(), String> {
334    remind(title, body, None)
335}
336
337/// A gentle reminder with its buttons wherever the system carries them, each
338/// its label and its action (the end of work's notice and the night's:
339/// docs/reviews.md). On Linux and the BSDs as `remind` (one button) or
340/// `remind_choices` (several) show them. On macOS with NSUserNotification's
341/// own: the action button, or "Options" and its list when there are several;
342/// a click on the notice itself is its first button's. Sent and waited for on
343/// a thread of its own, until a press or until the notice leaves the
344/// Notification Center (macOS hands the press to the main thread, which Qt's
345/// loop runs). The doses' reminders stay as `remind` shows them. On Windows,
346/// the words alone (the window shows these through Sioul's icon in the tray
347/// instead, a click opening the first button's); on Android, none (Java's).
348pub fn remind_offering(title: &str, body: &str, choices: Vec<(String, Box<dyn FnOnce() + Send>)>) -> Result<(), String> {
349    #[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
350    {
351        let mut choices = choices;
352        if choices.len() == 1 {
353            return remind(title, body, choices.pop());
354        }
355        remind_choices(title, body, choices)
356    }
357    #[cfg(target_os = "macos")]
358    {
359        as_sioul();
360        let (title, body) = (title.to_string(), body.to_string());
361        let labels: Vec<String> = choices.iter().map(|(label, _)| label.clone()).collect();
362        let mut runs: Vec<Option<Box<dyn FnOnce() + Send>>> = choices.into_iter().map(|(_, run)| Some(run)).collect();
363        // Made, sent and waited for on its own thread: notify-rust sends a
364        // macOS notice from its handle, and its wait blocks until an answer.
365        std::thread::spawn(move || {
366            let mut notification = Notification::new();
367            notification.appname("Sioul").summary(&title).body(&body).timeout(Timeout::Milliseconds(SHOWN));
368            for (index, label) in labels.iter().enumerate() {
369                notification.action(&format!("choice-{index}"), label);
370            }
371            match notification.show() {
372                Ok(handle) => handle.wait_for_action(|action| {
373                    if let Some(run) = pressed_choice(action).and_then(|n| runs.get_mut(n)).and_then(Option::take) {
374                        run();
375                    }
376                }),
377                Err(e) => eprintln!("{e}"),
378            }
379        });
380        Ok(())
381    }
382    #[cfg(windows)]
383    {
384        drop(choices);
385        remind(title, body, None)
386    }
387    #[cfg(target_os = "android")]
388    {
389        drop(choices);
390        let _ = (title, body);
391        Err("notifications are made through Java on Android".to_string())
392    }
393}
394
395/// Which of a notice's buttons a press names (`remind_offering`): "choice-1"
396/// the second; a click on the notice itself ("default") the first; none when
397/// it closed unanswered ("__closed") or for anything else.
398pub fn pressed_choice(action: &str) -> Option<usize> {
399    if action == "default" {
400        return Some(0);
401    }
402    action.strip_prefix("choice-")?.parse().ok()
403}
404
405/// Windows and macOS: the reminder is in the text; no button here.
406#[cfg(any(windows, target_os = "macos"))]
407pub fn remind(title: &str, body: &str, _action: Option<(String, Box<dyn FnOnce() + Send>)>) -> Result<(), String> {
408    #[cfg(target_os = "macos")]
409    as_sioul();
410    Notification::new().appname("Sioul").summary(title).body(body).timeout(Timeout::Milliseconds(SHOWN)).show().map(drop).map_err(|e| e.to_string())
411}
412
413/// Windows and macOS: the code is in the text; their notifications carry no button here.
414#[cfg(any(windows, target_os = "macos"))]
415pub fn code(title: &str, body: &str, _copy: Option<(String, Box<dyn FnOnce() + Send>)>) -> Result<(), String> {
416    #[cfg(target_os = "macos")]
417    as_sioul();
418    Notification::new().appname("Sioul").summary(title).body(body).timeout(Timeout::Milliseconds(SHOWN)).show().map(drop).map_err(|e| e.to_string())
419}
420
421/// Android: no notifications yet (Android's own are reached through Java);
422/// the code shows in the window only.
423#[cfg(target_os = "android")]
424pub fn code(_title: &str, _body: &str, _copy: Option<(String, Box<dyn FnOnce() + Send>)>) -> Result<(), String> {
425    Err("notifications are not made on Android yet".to_string())
426}
427
428/// Android: no notifications yet; a reminder is not shown (docs/android.md).
429#[cfg(target_os = "android")]
430pub fn remind(_title: &str, _body: &str, _action: Option<(String, Box<dyn FnOnce() + Send>)>) -> Result<(), String> {
431    Err("notifications are not made on Android yet".to_string())
432}
433
434/// The notification server, on the session bus.
435#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
436const SERVER: &str = "org.freedesktop.Notifications";
437#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
438const SERVER_PATH: &str = "/org/freedesktop/Notifications";
439
440/// Sioul's desktop file, named on each notification ("desktop-entry"): the
441/// desktop knows them as Sioul's, and lists Sioul in its notification
442/// settings (Plasma's "Show in do not disturb mode", for the doses during a
443/// pause: docs/pauses.md).
444#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
445const DESKTOP_ENTRY: &str = "com.aurelienpierre.Sioul";
446
447/// How long a lasting notification's thread listens to the server before it
448/// looks at what Sioul asked: a change shows within it.
449#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
450const LISTENED: Duration = Duration::from_millis(500);
451
452/// A notification the server refused (one starting with the session, after
453/// Sioul) is asked again this often.
454#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
455const ASKED_AGAIN: Duration = Duration::from_secs(60);
456
457/// How long the server may take to answer about a notification (notify-rust's own wait).
458#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
459const ASKED: Duration = Duration::from_secs(2);
460
461/// What a dose reminder's first try spends at most, its setup and all: the
462/// tick's wait (`FIRST_TRY`) less half a second, for its word to reach the tick.
463#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
464const TRY: Duration = Duration::from_millis(9_500);
465
466/// How long each of a dose reminder's two matches (its button, its close,
467/// heard on its connection) may take on its first try: the bus answers them
468/// at once.
469#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
470const MATCH: Duration = Duration::from_secs(1);
471
472/// The same for a try after the first, which no tick waits for: a slow bus
473/// (each answer seconds late, a machine swapping) still shows it, late, as
474/// a connection of dbus-rs's own waits (five seconds a call).
475#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
476const TRY_AGAIN: Duration = Duration::from_secs(15);
477
478/// Each match's wait on a try after the first (`TRY_AGAIN`).
479#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
480const MATCH_AGAIN: Duration = Duration::from_secs(5);
481
482#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
483const _: () = assert!(
484    TRY.as_millis() < FIRST_TRY.as_millis() && TRY.as_millis() > (2 * MATCH.as_millis() + ASKED.as_millis()) && TRY_AGAIN.as_millis() > (2 * MATCH_AGAIN.as_millis() + 2 * ASKED.as_millis())
485);
486
487/// Urgencies, as the specification numbers them: normal, and critical (what
488/// Sioul sends always gets through: it filters its own notifications already).
489#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
490const NORMAL: u8 = 1;
491#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
492const CRITICAL: u8 = 2;
493
494/// A notification as Sioul sends it on Linux and the BSDs (`notify_message`):
495/// its words, its icon (a theme's name), its urgency, its buttons as (key, label).
496#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
497struct Notice {
498    title: String,
499    body: String,
500    icon: &'static str,
501    urgency: u8,
502    buttons: Vec<(String, String)>,
503}
504
505#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
506impl Notice {
507    fn new(title: &str, body: &str, icon: &'static str, urgency: u8) -> Notice {
508        Notice { title: title.to_string(), body: body.to_string(), icon, urgency, buttons: Vec::new() }
509    }
510}
511
512/// What runs with the key of the button pressed.
513#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
514type Pressed = Box<dyn FnOnce(&str) + Send>;
515
516/// `notice` with one button at most, keyed "copy" (`code`, `remind`,
517/// `mail`): its action runs when it is pressed.
518#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
519fn one_button(mut notice: Notice, button: Option<(String, Box<dyn FnOnce() + Send>)>) -> Result<(), String> {
520    let Some((label, run)) = button else { return notify(notice, None) };
521    notice.buttons.push(("copy".to_string(), label));
522    notify(
523        notice,
524        Some(Box::new(move |key: &str| {
525            if key == "copy" {
526                run();
527            }
528        })),
529    )
530}
531
532/// Shows `notice`. Without `pressed`, on the caller's thread, the server's
533/// refusal said. With it, on a thread of its own, which keeps the connection
534/// that showed it (the server tells a press to that connection: Plasma
535/// addresses it) and waits there (`wait_for_press`).
536#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
537fn notify(notice: Notice, pressed: Option<Pressed>) -> Result<(), String> {
538    let Some(pressed) = pressed else {
539        let bus = session().map_err(|e| said(&e))?;
540        return send(&bus, &notice).map(drop).map_err(|e| said(&e));
541    };
542    // The D-Bus connection cannot change threads, so it is made where it waits.
543    std::thread::spawn(move || {
544        let Ok(bus) = session() else { return };
545        let heard = Rc::default();
546        if listened(&bus, &heard).is_err() {
547            return;
548        }
549        let Ok(id) = send(&bus, &notice) else { return };
550        wait_for_press(&Shown { bus, heard, replies: Rc::default(), id, capabilities: Vec::new() }, pressed, None);
551    });
552    Ok(())
553}
554
555/// A notice sent: the connection that sent it (kept: the server tells a
556/// press to it), what it hears, the answers to its own calls (`Replies`),
557/// the server's number for it (0 while not known), what the server reads.
558#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
559struct Shown {
560    bus: LocalConnection,
561    heard: Rc<RefCell<Vec<Heard>>>,
562    replies: Replies,
563    id: u32,
564    capabilities: Vec<String>,
565}
566
567/// A shown notice's button waited for on its connection: `pressed` runs with
568/// the key of the first one pressed, and the wait ends then, or when the
569/// notification closes, or the connection goes. What it heard before its
570/// number was known is read now. A dose's reminder (`record`) is taken away
571/// once its dose is marked or answered anywhere read here, looked at each
572/// `LOOK` (review of battery part C, the fifth review's L2).
573#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
574fn wait_for_press(shown: &Shown, pressed: Pressed, mut record: Option<&mut dyn DoseRecord>) {
575    let mut looked = Instant::now();
576    loop {
577        for item in std::mem::take(&mut *shown.heard.borrow_mut()) {
578            match item {
579                Heard::Pressed(at, key) if at == shown.id => {
580                    pressed(&key);
581                    return;
582                }
583                Heard::Closed(at) if at == shown.id => return,
584                _ => {}
585            }
586        }
587        if let Some(record) = record.as_deref_mut()
588            && looked.elapsed() >= LOOK
589        {
590            looked = Instant::now();
591            if record.settled() {
592                if shown.id != 0 {
593                    let _: Result<(), _> = shown.bus.with_proxy(SERVER, SERVER_PATH, ASKED).method_call(SERVER, "CloseNotification", (shown.id,));
594                }
595                return;
596            }
597        }
598        if shown.bus.process(Duration::from_secs(1)).is_err() {
599            return;
600        }
601    }
602}
603
604/// How often a dose's reminder still shown looks whether its dose was marked
605/// or answered elsewhere (`wait_for_press`): the sharing reads the others
606/// each minute.
607#[cfg(all(unix, not(any(target_os = "macos", target_os = "android")), not(test)))]
608const LOOK: Duration = Duration::from_secs(30);
609#[cfg(all(unix, not(any(target_os = "macos", target_os = "android")), test))]
610const LOOK: Duration = Duration::from_secs(1);
611
612/// How long a dose's notice sent and not answered is still listened for
613/// (`wait_late`): its answer, however late (a bus or a server stalled for
614/// minutes, hours), says it is shown, and it is then said again in place,
615/// the doubt first, or taken away, never left in its first words after
616/// another device reminded the dose. A day: past it, the dose is long asked
617/// about as one of the past.
618#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
619const LISTEN_LATE: Duration = Duration::from_secs(24 * 3600);
620
621/// The Notify call, as notify-rust's `Notification` made it, argument for
622/// argument: "Sioul", the notification it replaces (0, none), the icon, the
623/// title as it is, the body as the server reads it (`body_text`), the
624/// buttons, the hints (no sound, Sioul's desktop file, the urgency, each
625/// once), ten minutes (`SHOWN`).
626#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
627fn notify_message(notice: &Notice, replaces: u32, capabilities: &[String]) -> dbus::Message {
628    use dbus::arg::{PropMap, RefArg, Variant};
629    let mut hints = PropMap::new();
630    hints.insert("suppress-sound".into(), Variant(Box::new(true) as Box<dyn RefArg>));
631    hints.insert("desktop-entry".into(), Variant(Box::new(DESKTOP_ENTRY.to_string()) as Box<dyn RefArg>));
632    hints.insert("urgency".into(), Variant(Box::new(notice.urgency) as Box<dyn RefArg>));
633    let buttons: Vec<&str> = notice.buttons.iter().flat_map(|(key, label)| [key.as_str(), label.as_str()]).collect();
634    let timeout = i32::try_from(SHOWN).unwrap_or(i32::MAX);
635    let mut call = dbus::Message::new_method_call(SERVER, SERVER_PATH, SERVER, "Notify").expect("the notification server's names");
636    call.append_all(("Sioul", replaces, notice.icon, notice.title.as_str(), body_text(&notice.body, capabilities), buttons, hints, timeout));
637    call
638}
639
640/// What the server reads (`GetCapabilities`), asked `limit` at most; none:
641/// not asked, or no answer (read as markup: entities at worst).
642#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
643fn capabilities(bus: &LocalConnection, limit: Duration) -> Vec<String> {
644    if limit.is_zero() {
645        return Vec::new();
646    }
647    bus.with_proxy(SERVER, SERVER_PATH, limit).method_call(SERVER, "GetCapabilities", ()).map(|(found,): (Vec<String>,)| found).unwrap_or_default()
648}
649
650/// `notice` shown on `bus`, its answer waited for `ASKED`: the server's number for it.
651#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
652fn send(bus: &LocalConnection, notice: &Notice) -> Result<u32, dbus::Error> {
653    use dbus::blocking::BlockingSender;
654    let capabilities = capabilities(bus, ASKED);
655    let answer = bus.send_with_reply_and_block(notify_message(notice, 0, &capabilities), ASKED)?;
656    answer.read1::<u32>().map_err(|e| dbus::Error::new_failed(&e.to_string()))
657}
658
659/// What the server says of notifications, heard on `bus` from now on:
660/// buttons pressed, notifications closed (`Heard`), kept in `heard`.
661#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
662fn listened(bus: &LocalConnection, heard: &Rc<RefCell<Vec<Heard>>>) -> Result<(), dbus::Error> {
663    use dbus::message::MatchRule;
664    let (pressed, closed) = (Rc::clone(heard), Rc::clone(heard));
665    bus.add_match(MatchRule::new_signal(SERVER, "ActionInvoked").with_path(SERVER_PATH), move |(id, key): (u32, String), _: &LocalConnection, _: &dbus::Message| {
666        pressed.borrow_mut().push(Heard::Pressed(id, key));
667        true
668    })?;
669    bus.add_match(MatchRule::new_signal(SERVER, "NotificationClosed").with_path(SERVER_PATH), move |(id, _why): (u32, u32), _: &LocalConnection, _: &dbus::Message| {
670        closed.borrow_mut().push(Heard::Closed(id));
671        true
672    })?;
673    Ok(())
674}
675
676/// `listened`, each of its two matches waited for `each` at most (a dose's
677/// first try fits in the tick's wait: `TRY`).
678#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
679fn listened_within(bus: &LocalConnection, heard: &Rc<RefCell<Vec<Heard>>>, each: Duration) -> Result<(), dbus::Error> {
680    use dbus::channel::MatchingReceiver;
681    use dbus::message::MatchRule;
682    let pressed = MatchRule::new_signal(SERVER, "ActionInvoked").with_path(SERVER_PATH);
683    let closed = MatchRule::new_signal(SERVER, "NotificationClosed").with_path(SERVER_PATH);
684    let bus_itself = bus.with_proxy("org.freedesktop.DBus", "/org/freedesktop/DBus", each);
685    for rule in [&pressed, &closed] {
686        let () = bus_itself.method_call("org.freedesktop.DBus", "AddMatch", (rule.match_str(),))?;
687    }
688    let (on_press, on_close) = (Rc::clone(heard), Rc::clone(heard));
689    bus.start_receive(
690        pressed,
691        Box::new(move |signal: dbus::Message, _: &LocalConnection| {
692            if let Ok((id, key)) = signal.read2::<u32, String>() {
693                on_press.borrow_mut().push(Heard::Pressed(id, key));
694            }
695            true
696        }),
697    );
698    bus.start_receive(
699        closed,
700        Box::new(move |signal: dbus::Message, _: &LocalConnection| {
701            if let Ok((id, _why)) = signal.read2::<u32, u32>() {
702                on_close.borrow_mut().push(Heard::Closed(id));
703            }
704            true
705        }),
706    );
707    Ok(())
708}
709
710/// An answer to a call of this connection's own sent without waiting
711/// (`dbus::channel::Sender::send`): its return, or the error the bus or the
712/// server gave: whether it says the call reached nobody (`undelivered`), and
713/// in a few words.
714#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
715enum Reply {
716    Return(dbus::Message),
717    Error { undelivered: bool, why: String },
718}
719
720/// Whether a D-Bus error says the call reached nobody: no server owns the
721/// name. Any other (no reply, the server gone, the server's own error) may
722/// come after the server took the notice and showed it (the sixth review's L2).
723#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
724fn undelivered(e: &dbus::Error) -> bool {
725    matches!(e.name(), Some("org.freedesktop.DBus.Error.ServiceUnknown" | "org.freedesktop.DBus.Error.NameHasNoOwner"))
726}
727
728/// The answers to this connection's calls sent without waiting, by serial,
729/// kept as they come, late ones too (`heard_replies`).
730#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
731type Replies = Rc<RefCell<std::collections::HashMap<u32, Reply>>>;
732
733/// The answers to the calls sent without waiting on `bus`, heard from now on.
734#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
735fn heard_replies(bus: &LocalConnection) -> Replies {
736    use dbus::channel::MatchingReceiver;
737    use dbus::message::{MatchRule, MessageType};
738    let replies: Replies = Rc::default();
739    for kind in [MessageType::MethodReturn, MessageType::Error] {
740        let kept = Rc::clone(&replies);
741        let mut rule = MatchRule::new();
742        rule.msg_type = Some(kind);
743        bus.start_receive(
744            rule,
745            Box::new(move |mut answer: dbus::Message, _: &LocalConnection| {
746                if let Some(serial) = answer.get_reply_serial() {
747                    let failed = answer.as_result().err().map(|e| Reply::Error { undelivered: undelivered(&e), why: said(&e) });
748                    kept.borrow_mut().insert(serial, failed.unwrap_or(Reply::Return(answer)));
749                }
750                true
751            }),
752        );
753    }
754    replies
755}
756
757/// The answer to `serial`, hearing on `bus` all that comes until `until`;
758/// none by then, or the connection gone.
759#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
760fn answer_by(bus: &LocalConnection, replies: &Replies, serial: u32, until: Instant) -> Option<Reply> {
761    loop {
762        if let Some(reply) = replies.borrow_mut().remove(&serial) {
763            return Some(reply);
764        }
765        let left = until.saturating_duration_since(Instant::now());
766        if left.is_zero() || bus.process(left).is_err() {
767            return None;
768        }
769    }
770}
771
772/// What one try of a dose's reminder became.
773#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
774enum Tried {
775    /// Answered: shown, its number known.
776    Shown(Shown),
777    /// Surely not gone out: why.
778    Unshown(String),
779    /// Sent, not answered in time: why; its connection kept (number 0 until
780    /// its answer comes), and the serial of its Notify.
781    Maybe(String, Shown, u32),
782    /// Sent, then refused otherwise than "no server" (no reply, the server
783    /// gone, its own error): it may have shown, and no answer will come.
784    Lost(String),
785    /// Not sent: no longer to remind, as its words said just before it would
786    /// have gone (`Again::late`).
787    Done,
788}
789
790/// One try of a dose's reminder: the connection (made now, within the
791/// first try's `TRY`; or `bus`, a try after the first, within `TRY_AGAIN`),
792/// its two matches, what the server reads, then its words as they are now
793/// (`words`: shown late, the doubt first; false, no longer to remind), then
794/// the Notify, sent only with its own wait (`ASKED`) still ahead, and that
795/// wait. Not gone out when anything before the Notify fails or there is no
796/// time left for it; sent and not answered in time, maybe.
797#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
798fn try_once(notice: &mut Notice, bus: Option<LocalConnection>, words: &mut dyn FnMut(&mut Notice) -> bool) -> Tried {
799    let (all, matching) = if bus.is_none() { (TRY, MATCH) } else { (TRY_AGAIN, MATCH_AGAIN) };
800    let deadline = Instant::now() + all;
801    let left = || deadline.saturating_duration_since(Instant::now());
802    let bus = match bus {
803        Some(bus) => bus,
804        None => {
805            // Room kept after the setup for the two matches and the Notify's wait.
806            let Some(limit) = left().checked_sub(2 * matching + ASKED).filter(|limit| !limit.is_zero()) else { return Tried::Unshown("no time left to connect".to_string()) };
807            match session_within(limit.min(crate::bus::SETUP)) {
808                Ok(bus) => bus,
809                Err(e) => return Tried::Unshown(said(&e)),
810            }
811        }
812    };
813    let heard = Rc::default();
814    let each = left().saturating_sub(ASKED).min(2 * matching) / 2;
815    if each.is_zero() {
816        return Tried::Unshown("no time left to listen".to_string());
817    }
818    if let Err(e) = listened_within(&bus, &heard, each) {
819        return Tried::Unshown(said(&e));
820    }
821    let capabilities = capabilities(&bus, left().saturating_sub(ASKED).min(ASKED));
822    // Its words read last, just before it goes: a dose marked meanwhile is not reminded.
823    if !words(notice) {
824        return Tried::Done;
825    }
826    if left() < ASKED {
827        return Tried::Unshown("no time left to send".to_string());
828    }
829    let replies = heard_replies(&bus);
830    use dbus::channel::Sender as _;
831    let Ok(serial) = bus.send(notify_message(notice, 0, &capabilities)) else { return Tried::Unshown("the connection was gone".to_string()) };
832    let answer = answer_by(&bus, &replies, serial, Instant::now() + ASKED);
833    let mut shown = Shown { bus, heard, replies, id: 0, capabilities };
834    match answer {
835        Some(Reply::Return(answer)) => {
836            shown.id = answer.read1::<u32>().unwrap_or(0);
837            Tried::Shown(shown)
838        }
839        // No server (plasmashell starting again): it reached nobody.
840        Some(Reply::Error { undelivered: true, why }) => Tried::Unshown(why),
841        Some(Reply::Error { why, .. }) => Tried::Lost(why),
842        None => Tried::Maybe("the notification server did not answer in time".to_string(), shown, serial),
843    }
844}
845
846/// `notice`'s words as `record` says them now, recorded first as about to
847/// go (`DoseRecord::going`); false when it is no longer to remind.
848#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
849fn said_now(notice: &mut Notice, record: &mut dyn DoseRecord, going: Going) -> bool {
850    match record.going(going) {
851        Say::Own => true,
852        Say::Words(title, body) => {
853            (notice.title, notice.body) = (title, body);
854            true
855        }
856        Say::Nothing => false,
857    }
858}
859
860/// Whether the tick still waits for a first try's outcome (`Sending`).
861#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
862fn waited_for(slot: &Arc<(Mutex<Slot>, Condvar)>) -> bool {
863    slot.0.lock().is_ok_and(|slot| !slot.closed)
864}
865
866/// A dose reminder's first try, on its own thread: recorded before it goes,
867/// its outcome left for the tick (`told`), then as `Again` says. Sent while
868/// the tick waits, in its own words; once the tick stopped waiting, in late
869/// words (the doubt first), or not at all (no longer to remind). Answered
870/// late, said again in place, the doubt first (`shown_late`).
871#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
872fn first_try(mut notice: Notice, mut again: Again, pressed: Pressed, slot: &Arc<(Mutex<Slot>, Condvar)>) {
873    let (mut sent_late, mut went) = (false, false);
874    let tried = try_once(&mut notice, None, &mut |notice: &mut Notice| {
875        let going = if waited_for(slot) { Going::First } else { Going::Late(Late::Retry) };
876        sent_late = going != Going::First;
877        went = said_now(notice, &mut *again.record, going);
878        went
879    });
880    let outcome = match &tried {
881        Tried::Shown(_) => Outcome::Shown,
882        Tried::Unshown(why) => Outcome::Unshown(why.clone()),
883        Tried::Maybe(why, ..) | Tried::Lost(why) => Outcome::Maybe(why.clone()),
884        Tried::Done => Outcome::Gone,
885    };
886    let waited = told(slot, outcome);
887    // Gone out in its own words and known shown only after the tick stopped
888    // waiting: on the screen since its time, its fate not said then.
889    let late = if sent_late { Late::Retry } else { Late::Unconfirmed };
890    match tried {
891        Tried::Shown(mut shown) => {
892            if waited || sent_late {
893                again.record.shown();
894            } else {
895                shown_late(&mut shown, &mut notice, &mut *again.record, late);
896            }
897            wait_for_press(&shown, pressed, Some(&mut *again.record));
898        }
899        Tried::Maybe(_, shown, serial) => wait_late(shown, serial, notice, again, pressed, late),
900        // Taken, then refused otherwise than "no server": it may be on the screen; tried again, the doubt first.
901        Tried::Lost(_) => show_again(notice, again, pressed, Late::Unconfirmed),
902        Tried::Unshown(_) => {
903            if went {
904                again.record.refused();
905            }
906            again.record.unshown();
907            show_again(notice, again, pressed, Late::Retry);
908        }
909        Tried::Done => {}
910    }
911}
912
913/// A notice sent and not answered in time: its answer still heard on its
914/// connection, however late (`LISTEN_LATE`), never sent again; answered, it
915/// was shown, late (`shown_late`), and its button counts. Refused late, or
916/// its connection gone (the server, or the bus, gone): it may have shown,
917/// its record says so still, and it is tried again, the doubt first, while
918/// its grace lasts (`show_again`: the sixth review's M2).
919#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
920fn wait_late(mut shown: Shown, serial: u32, mut notice: Notice, mut again: Again, pressed: Pressed, late: Late) {
921    let until = Instant::now() + LISTEN_LATE;
922    match answer_by(&shown.bus, &shown.replies, serial, until) {
923        Some(Reply::Return(answer)) => {
924            shown.id = answer.read1::<u32>().unwrap_or(0);
925            shown_late(&mut shown, &mut notice, &mut *again.record, late);
926            wait_for_press(&shown, pressed, Some(&mut *again.record));
927        }
928        Some(Reply::Error { .. }) | None => {
929            drop(shown);
930            // Taken by the server, then lost: it may have shown, its tries say it was not confirmed (L2).
931            show_again(notice, again, pressed, Late::Unconfirmed);
932        }
933    }
934}
935
936/// A dose's reminder known shown late (answered late, or after the tick
937/// stopped waiting). Its words read first (`DoseRecord::going`; they depend
938/// on the record, which says it shown right after): still to remind, it is
939/// recorded shown (`DoseRecord::shown`) and said again in place with the
940/// doubt first (`Late::Unconfirmed`: "was not confirmed in time"), unless it
941/// says so already, was closed meanwhile (seen), or its "Taken" was pressed
942/// meanwhile (the press runs after, `wait_for_press`: never shown again over
943/// it). The server's answer to that Notify gives its number from then on
944/// (GNOME gives a new one to a notice no longer shown). No longer to remind
945/// (marked, reminded elsewhere, kept elsewhere now), it is taken away, never
946/// recorded shown: another device reads it sent, maybe shown, and says so.
947#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
948fn shown_late(shown: &mut Shown, notice: &mut Notice, record: &mut dyn DoseRecord, late: Late) {
949    use dbus::blocking::BlockingSender;
950    let id = shown.id;
951    let pressed = shown.heard.borrow().iter().any(|heard| matches!(heard, Heard::Pressed(at, _) if *at == id));
952    let closed = shown.heard.borrow().iter().any(|heard| matches!(heard, Heard::Closed(at) if *at == id));
953    let before = (notice.title.clone(), notice.body.clone());
954    if !said_now(notice, record, Going::Late(late)) {
955        if id != 0 && !pressed {
956            let _: Result<(), _> = shown.bus.with_proxy(SERVER, SERVER_PATH, ASKED).method_call(SERVER, "CloseNotification", (id,));
957        }
958        return;
959    }
960    record.shown();
961    if id != 0 && !closed && !pressed && (&notice.title, &notice.body) != (&before.0, &before.1)
962        && let Ok(answer) = shown.bus.send_with_reply_and_block(notify_message(notice, id, &shown.capabilities), ASKED)
963        && let Ok(number) = answer.read1::<u32>()
964        && number != 0
965    {
966        shown.id = number;
967    }
968}
969
970/// A dose's reminder tried again after a first try that did not go out, the
971/// doubt first, until `Again::end`, then no more (`DoseRecord::over`): the
972/// bus first (a stalled one refuses at once, `bus`, and nothing else is read
973/// meanwhile), then the server, its words read last, just before it goes,
974/// recorded as going (none: no longer to remind, and no more tries). None
975/// during `Again::quiet` (a phone's alarm rings then): promised before it
976/// begins (`DoseRecord::pause`), the tries resumed after it while still to
977/// remind. Sent and not answered in time: never again (`wait_late`).
978#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
979fn show_again(mut notice: Notice, mut again: Again, pressed: Pressed, mut late: Late) {
980    let mut tries = 0;
981    loop {
982        let wait = again_in(tries);
983        tries += 1;
984        let next = Instant::now() + wait;
985        if let Some((from, to, until)) = again.quiet
986            && next >= from
987        {
988            again.quiet = None;
989            if !again.record.pause(until) {
990                return;
991            }
992            std::thread::sleep(to.saturating_duration_since(Instant::now()));
993            tries = 0;
994            continue;
995        }
996        if next >= again.end {
997            again.record.over();
998            return;
999        }
1000        std::thread::sleep(wait);
1001        let Ok(bus) = session_within(crate::bus::SETUP) else { continue };
1002        let mut went = false;
1003        let tried = try_once(&mut notice, Some(bus), &mut |notice: &mut Notice| {
1004            went = said_now(notice, &mut *again.record, Going::Late(late));
1005            went
1006        });
1007        match tried {
1008            Tried::Shown(shown) => {
1009                again.record.shown();
1010                return wait_for_press(&shown, pressed, Some(&mut *again.record));
1011            }
1012            Tried::Unshown(_) if went => again.record.refused(),
1013            Tried::Unshown(_) => {}
1014            // Taken, then refused otherwise than "no server": it may have shown; tried again, the doubt first.
1015            Tried::Lost(_) => late = Late::Unconfirmed,
1016            Tried::Done => return,
1017            Tried::Maybe(_, shown, serial) => return wait_late(shown, serial, notice, again, pressed, late),
1018        }
1019    }
1020}
1021
1022/// How long a dose's reminder waits before its try after `tries` others:
1023/// five seconds for two minutes (a bus that stalled a while: shown soon after
1024/// it answers), then half a minute.
1025#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1026fn again_in(tries: u32) -> Duration {
1027    if tries < 24 { Duration::from_secs(5) } else { Duration::from_secs(30) }
1028}
1029
1030/// A connection to the session's bus, made by `crate::bus` (`bus::SETUP` at most).
1031#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1032fn session() -> Result<LocalConnection, dbus::Error> {
1033    crate::bus::session().map(LocalConnection::from)
1034}
1035
1036/// The same, made within `limit`.
1037#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1038fn session_within(limit: Duration) -> Result<LocalConnection, dbus::Error> {
1039    let address = crate::bus::session_address().ok_or_else(|| dbus::Error::new_failed("no session bus"))?;
1040    crate::bus::connect_within(&address, limit).map(LocalConnection::from)
1041}
1042
1043/// A D-Bus error in a few words: its message, else its name.
1044#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1045fn said(e: &dbus::Error) -> String {
1046    e.message().or(e.name()).unwrap_or("D-Bus").to_string()
1047}
1048
1049/// The notification of something going on, the time running while a focus
1050/// session runs (docs/tasks.md): shown until it is closed, changed in place,
1051/// its buttons pressed any number of times. Its own thread holds the D-Bus
1052/// connection that shows it, and listens there: the server tells a press to
1053/// that connection alone (Plasma addresses it). Resident, kept when a button
1054/// is pressed; at normal urgency, for Plasma shows a low one nowhere unless
1055/// told to; without sound. A server that keeps what it shows ("persistence":
1056/// its history, its tray) takes it there after its popup, buttons alive;
1057/// elsewhere it does not expire. No session bus, no server: nothing shows,
1058/// nothing breaks. Dropped, it is taken away.
1059#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1060pub struct Ongoing {
1061    changes: Sender<Change>,
1062}
1063
1064/// What a lasting notification says: its buttons as (key, label), "default"
1065/// for a click on it.
1066#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1067struct Said {
1068    title: String,
1069    body: String,
1070    actions: Vec<(String, String)>,
1071}
1072
1073#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1074enum Change {
1075    Show(Said),
1076    /// Taken away; said done on the sender, if any.
1077    Close(Option<Sender<()>>),
1078}
1079
1080/// What the server said, read between two looks.
1081#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1082enum Heard {
1083    Pressed(u32, String),
1084    Closed(u32),
1085}
1086
1087/// Shows the notification of something going on. `actions` are its buttons
1088/// as (key, label), "default" for a click on it; `on_action` runs with the
1089/// key of each one pressed, on the notification's own thread.
1090#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1091pub fn ongoing(title: &str, body: &str, actions: Vec<(String, String)>, on_action: Box<dyn Fn(&str) + Send>) -> Ongoing {
1092    let (changes, asked) = std::sync::mpsc::channel();
1093    let said = Said { title: title.to_string(), body: body.to_string(), actions };
1094    // The D-Bus connection cannot change threads, so it is made where it listens.
1095    std::thread::spawn(move || serve(said, &asked, &*on_action));
1096    Ongoing { changes }
1097}
1098
1099#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1100impl Ongoing {
1101    /// Changes it in place; shown again if it was closed meanwhile.
1102    pub fn update(&self, title: &str, body: &str, actions: Vec<(String, String)>) {
1103        let _ = self.changes.send(Change::Show(Said { title: title.to_string(), body: body.to_string(), actions }));
1104    }
1105
1106    /// Takes it away, waiting at most `wait` for the server to have done it:
1107    /// as Sioul quits, before the connection goes with the process.
1108    pub fn close(self, wait: Duration) {
1109        let (done, closed) = std::sync::mpsc::channel();
1110        if self.changes.send(Change::Close(Some(done))).is_ok() {
1111            let _ = closed.recv_timeout(wait);
1112        }
1113    }
1114}
1115
1116/// A lasting notification, across its connections: what it says, whether
1117/// that changed since it was last sent, the server's number for it (0 while
1118/// none shows), when the server last refused it.
1119#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1120struct Lasting {
1121    said: Said,
1122    fresh: bool,
1123    id: u32,
1124    refused: Option<Instant>,
1125}
1126
1127/// A lasting notification's thread: what Sioul asks shown, what the server
1128/// says heard, until it is taken away. Its connection is made again while
1129/// the bus does not give one (a stall: `bus` refuses at once meanwhile, five
1130/// seconds apart, `bus::SETUP`) and once it is lost, rather than the
1131/// notification being lost for the session (review of battery part C, L4);
1132/// what Sioul asks meanwhile is kept, the last of it, and a close ends it.
1133#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1134fn serve(said: Said, asked: &Receiver<Change>, on_action: &dyn Fn(&str)) {
1135    use std::sync::mpsc::RecvTimeoutError;
1136    let mut lasting = Lasting { said, fresh: true, id: 0, refused: None };
1137    loop {
1138        if let Ok(connection) = session()
1139            && let Served::Done = served(&mut lasting, asked, on_action, &connection)
1140        {
1141            return;
1142        }
1143        match asked.recv_timeout(crate::bus::SETUP) {
1144            Ok(Change::Show(next)) => (lasting.said, lasting.fresh) = (next, true),
1145            Ok(Change::Close(done)) => {
1146                if let Some(done) = done {
1147                    let _ = done.send(());
1148                }
1149                return;
1150            }
1151            Err(RecvTimeoutError::Timeout) => {}
1152            Err(RecvTimeoutError::Disconnected) => return,
1153        }
1154    }
1155}
1156
1157/// What became of a lasting notification on one connection (`served`):
1158/// taken away, or its connection lost (made again, the notification shown
1159/// again in place of its number, when the server still knows it).
1160#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1161enum Served {
1162    Done,
1163    Lost,
1164}
1165
1166/// A lasting notification on `connection`: what Sioul asks shown, what the
1167/// server says heard, until it is taken away or the connection is lost. Each
1168/// Notify goes without waiting, its answer heard as it comes: while one is
1169/// not answered, none other is sent, so that a server answering late never
1170/// shows it twice (each shown with no number to replace, neither taken
1171/// away: review of battery part C, L1); its late answer gives the number,
1172/// and what changed meanwhile goes in its place.
1173#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1174fn served(lasting: &mut Lasting, asked: &Receiver<Change>, on_action: &dyn Fn(&str), connection: &LocalConnection) -> Served {
1175    use dbus::channel::Sender as _;
1176    use std::sync::mpsc::TryRecvError;
1177    let heard: Rc<RefCell<Vec<Heard>>> = Rc::default();
1178    if listened(connection, &heard).is_err() {
1179        return Served::Lost;
1180    }
1181    let replies = heard_replies(connection);
1182    let server = connection.with_proxy(SERVER, SERVER_PATH, ASKED);
1183    // Asked again while not known: a server starting after Sioul answers later.
1184    let mut capabilities: Vec<String> = Vec::new();
1185    // A Notify sent and not answered yet: its serial.
1186    let mut unanswered: Option<u32> = None;
1187    loop {
1188        // What the server said meanwhile, all of it: answers, buttons pressed, notifications closed.
1189        while connection.process(Duration::ZERO).unwrap_or(false) {}
1190        if let Some(serial) = unanswered {
1191            let reply = replies.borrow_mut().remove(&serial);
1192            match reply {
1193                Some(Reply::Return(answer)) => {
1194                    (lasting.id, lasting.refused, unanswered) = (answer.read1::<u32>().unwrap_or(0), None, None);
1195                }
1196                Some(Reply::Error { .. }) => (lasting.refused, unanswered) = (Some(Instant::now()), None),
1197                None => {}
1198            }
1199        }
1200        let news = std::mem::take(&mut *heard.borrow_mut());
1201        for item in news {
1202            match item {
1203                Heard::Pressed(at, key) if at == lasting.id && lasting.id != 0 => on_action(&key),
1204                // Closed by you, or by the server: shown again at the next change.
1205                Heard::Closed(at) if at == lasting.id => lasting.id = 0,
1206                _ => {}
1207            }
1208        }
1209        // What Sioul asked meanwhile: the last of it counts.
1210        loop {
1211            match asked.try_recv() {
1212                Ok(Change::Show(next)) => (lasting.said, lasting.fresh) = (next, true),
1213                Ok(Change::Close(done)) => {
1214                    close_lasting(connection, &server, &replies, unanswered, lasting.id);
1215                    if let Some(done) = done {
1216                        let _ = done.send(());
1217                    }
1218                    return Served::Done;
1219                }
1220                Err(TryRecvError::Empty) => break,
1221                Err(TryRecvError::Disconnected) => {
1222                    close_lasting(connection, &server, &replies, unanswered, lasting.id);
1223                    return Served::Done;
1224                }
1225            }
1226        }
1227        if unanswered.is_none() && (lasting.fresh || lasting.refused.is_some_and(|at| at.elapsed() >= ASKED_AGAIN)) {
1228            if capabilities.is_empty() {
1229                capabilities = server.method_call(SERVER, "GetCapabilities", ()).map(|(found,): (Vec<String>,)| found).unwrap_or_default();
1230            }
1231            match connection.send(lasting_message(lasting.id, &lasting.said, &capabilities)) {
1232                Ok(serial) => unanswered = Some(serial),
1233                Err(()) => return Served::Lost,
1234            }
1235            lasting.fresh = false;
1236        }
1237        if connection.process(LISTENED).is_err() {
1238            return Served::Lost;
1239        }
1240    }
1241}
1242
1243/// How long a lasting notification taken away while its Notify is not answered
1244/// waits for that answer, its number, to close it (`close_lasting`): a
1245/// server seconds late still gets it closed.
1246#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1247const CLOSE_LATE: Duration = Duration::from_secs(30);
1248
1249/// The lasting notification's Notify, in place of `id` if not 0.
1250#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1251fn lasting_message(id: u32, said: &Said, capabilities: &[String]) -> dbus::Message {
1252    use dbus::arg::{PropMap, RefArg, Variant};
1253    let mut hints = PropMap::new();
1254    hints.insert("resident".into(), Variant(Box::new(true) as Box<dyn RefArg>));
1255    hints.insert("suppress-sound".into(), Variant(Box::new(true) as Box<dyn RefArg>));
1256    hints.insert("urgency".into(), Variant(Box::new(1u8) as Box<dyn RefArg>));
1257    hints.insert("desktop-entry".into(), Variant(Box::new(DESKTOP_ENTRY.to_string()) as Box<dyn RefArg>));
1258    let actions: Vec<&str> = said.actions.iter().flat_map(|(key, label)| [key.as_str(), label.as_str()]).collect();
1259    // Kept by the server once its popup goes, or never expiring where nothing keeps it.
1260    let timeout: i32 = if capabilities.iter().any(|c| c == "persistence") { -1 } else { 0 };
1261    let mut call = dbus::Message::new_method_call(SERVER, SERVER_PATH, SERVER, "Notify").expect("the notification server's names");
1262    call.append_all(("Sioul", id, "chronometer", said.title.as_str(), body_text(&said.body, capabilities), actions, hints, timeout));
1263    call
1264}
1265
1266/// Taken away: by its number; a Notify not answered yet, its answer waited
1267/// for first (`CLOSE_LATE` at most: its number), so that it does not stay
1268/// (the fifth review's L4); the close of a session goes on meanwhile.
1269#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1270fn close_lasting(connection: &LocalConnection, server: &dbus::blocking::Proxy<'_, &LocalConnection>, replies: &Replies, unanswered: Option<u32>, id: u32) {
1271    let id = match unanswered.and_then(|serial| answer_by(connection, replies, serial, Instant::now() + CLOSE_LATE)) {
1272        Some(Reply::Return(answer)) => answer.read1::<u32>().unwrap_or(id),
1273        _ => id,
1274    };
1275    if id != 0 {
1276        let _: Result<(), _> = server.method_call(SERVER, "CloseNotification", (id,));
1277    }
1278}
1279
1280/// Windows and macOS: no lasting notification yet, the focus window shows
1281/// the time. Android: Android's own, made on the window's side (sioul-app).
1282#[cfg(not(all(unix, not(any(target_os = "macos", target_os = "android")))))]
1283pub struct Ongoing;
1284
1285#[cfg(not(all(unix, not(any(target_os = "macos", target_os = "android")))))]
1286pub fn ongoing(_title: &str, _body: &str, _actions: Vec<(String, String)>, _on_action: Box<dyn Fn(&str) + Send>) -> Ongoing {
1287    Ongoing
1288}
1289
1290#[cfg(not(all(unix, not(any(target_os = "macos", target_os = "android")))))]
1291impl Ongoing {
1292    pub fn update(&self, _title: &str, _body: &str, _actions: Vec<(String, String)>) {}
1293
1294    pub fn close(self, _wait: Duration) {}
1295}
1296
1297/// A body as the notification server reads it. Most read markup there
1298/// ("body-markup": `<b>`, `<a href>`, `<img src>`), and bodies carry a mail's
1299/// or a site's words: there, `&`, `<` and `>` are written as entities, so
1300/// that the words show as they came and nothing in them is a link or a
1301/// picture to fetch. A server that reads no markup shows the text as it is.
1302/// The summary is plain text for every server (the specification), and goes as it is.
1303#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1304fn body_text(body: &str, capabilities: &[String]) -> String {
1305    // A server that could not be asked is taken to read markup: entities at worst.
1306    if capabilities.is_empty() || capabilities.iter().any(|c| c == "body-markup") {
1307        body.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
1308    } else {
1309        body.to_string()
1310    }
1311}
1312
1313/// The bundle id macOS is given is the one Sioul's macOS bundle declares:
1314/// any other would attribute its notifications to another application.
1315#[cfg(test)]
1316mod bundle {
1317    #[test]
1318    fn macos_is_given_the_bundles_own_id() {
1319        let plist = std::fs::read_to_string(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../packaging/macos/Info.plist")).unwrap();
1320        let declared = plist.split("<key>CFBundleIdentifier</key>").nth(1).and_then(|rest| rest.split("<string>").nth(1)).and_then(|rest| rest.split("</string>").next()).unwrap();
1321        assert_eq!(super::BUNDLE_ID, declared.trim());
1322    }
1323
1324    /// macOS's answer to a closing notice (`remind_offering`), read on every
1325    /// system: a button by its key, a click on the notice as its first
1326    /// button, nothing when it closed unanswered.
1327    #[test]
1328    fn a_press_names_its_button_and_a_click_the_first() {
1329        assert_eq!(super::pressed_choice("choice-0"), Some(0));
1330        assert_eq!(super::pressed_choice("choice-1"), Some(1));
1331        assert_eq!(super::pressed_choice("default"), Some(0));
1332        assert_eq!(super::pressed_choice("__closed"), None);
1333        assert_eq!(super::pressed_choice("choice-"), None);
1334        assert_eq!(super::pressed_choice("Options"), None);
1335    }
1336}
1337
1338#[cfg(all(test, unix, not(any(target_os = "macos", target_os = "android"))))]
1339mod tests {
1340    use super::*;
1341
1342    #[test]
1343    fn bodies_are_words_not_markup() {
1344        let markup = vec!["actions".to_string(), "body-markup".to_string()];
1345        let body = "Code 482913 <img src=\"https://example.org/t.png\"> & <a href=\"https://example.org\">here</a>";
1346        let shown = body_text(body, &markup);
1347        assert!(!shown.contains('<') && !shown.contains('>'), "{shown}");
1348        assert!(shown.starts_with("Code 482913 &lt;img") && shown.contains(" &amp; "), "{shown}");
1349        // Not asked, read as markup; a server without markup gets the words as they are.
1350        assert_eq!(body_text("a < b", &[]), "a &lt; b");
1351        assert_eq!(body_text("a < b & c", &["actions".to_string()]), "a < b & c");
1352    }
1353
1354    /// A connection to the test's own session bus (dbus-run-session) owning
1355    /// the notification server's name, a stand-in of a test before given two
1356    /// seconds to leave it; none where a server is there already (the
1357    /// desktop's): never in a server's place, no replacing it, no waiting in line.
1358    fn stand_in_bus() -> Option<dbus::blocking::LocalConnection> {
1359        use dbus::blocking::stdintf::org_freedesktop_dbus::RequestNameReply;
1360        let until = std::time::Instant::now() + Duration::from_secs(2);
1361        loop {
1362            let bus = session().ok().filter(|bus| matches!(bus.request_name(SERVER, false, false, true), Ok(RequestNameReply::PrimaryOwner)));
1363            if bus.is_some() || std::time::Instant::now() >= until {
1364                return bus;
1365            }
1366            std::thread::sleep(Duration::from_millis(50));
1367        }
1368    }
1369
1370    /// The hints' names a call carries, each as many times as it is there
1371    /// (a dictionary read as such would keep one of each).
1372    fn hint_names(call: &dbus::Message) -> Vec<String> {
1373        use dbus::arg::ArgType;
1374        let mut args = call.iter_init();
1375        for _ in 0..6 {
1376            args.next();
1377        }
1378        let mut names = Vec::new();
1379        if let Some(mut entries) = args.recurse(ArgType::Array) {
1380            while entries.arg_type() == ArgType::DictEntry {
1381                if let Some(mut entry) = entries.recurse(ArgType::DictEntry) {
1382                    names.extend(entry.get::<String>());
1383                }
1384                entries.next();
1385            }
1386        }
1387        names.sort();
1388        names
1389    }
1390
1391    /// What a stand-in server was sent: when, the title, the number it replaces.
1392    type Notices = std::sync::mpsc::Receiver<(std::time::Instant, String, u32)>;
1393
1394    /// A stand-in notification server on the bus at `address`, on a thread of
1395    /// its own until `stop`: "answers" answers each Notify and presses "copy"
1396    /// on each notice that has it, to the connection that showed it; "shows"
1397    /// answers, never presses; "late" shows it (says it) at once but answers
1398    /// three seconds later, then presses; "press-first" the same, pressed
1399    /// first, at one second, and kept on the screen after; "renumber" answers
1400    /// a new Notify three seconds late, never presses, and gives a notice
1401    /// replaced a number of its own (GNOME); "stall" answers as "answers",
1402    /// but its first GetCapabilities stops the bus (`bus_pid`, SIGSTOP)
1403    /// before its answer goes, for six seconds; "silent" takes each call and
1404    /// never answers; "late-error" refuses its first Notify three seconds
1405    /// late, then answers; "exits-fast" and "exits-late" take their first
1406    /// Notify and go away, half a second or three seconds later (the bus then
1407    /// says no reply will come). A CloseNotification answered with NotificationClosed,
1408    /// as a server does. Each Notify said on `told`: when it came, its title,
1409    /// the number it replaces; each CloseNotification, `CLOSED` and its
1410    /// number. Whether it got the name.
1411    fn notice_server(address: &str, mode: &'static str, told: std::sync::mpsc::Sender<(std::time::Instant, String, u32)>, stop: std::sync::Arc<std::sync::atomic::AtomicBool>) -> bool {
1412        notice_server_on(address, mode, 0, told, stop)
1413    }
1414
1415    /// `notice_server`, the bus's pid given for "stall".
1416    fn notice_server_on(address: &str, mode: &'static str, bus_pid: u32, told: std::sync::mpsc::Sender<(std::time::Instant, String, u32)>, stop: std::sync::Arc<std::sync::atomic::AtomicBool>) -> bool {
1417        use dbus::blocking::LocalConnection;
1418        use dbus::blocking::stdintf::org_freedesktop_dbus::RequestNameReply;
1419        use dbus::channel::{MatchingReceiver, Sender as _};
1420        use dbus::message::MatchRule;
1421        let (ready, started) = std::sync::mpsc::channel::<bool>();
1422        let address = address.to_string();
1423        std::thread::spawn(move || {
1424            let Some(bus) = crate::bus::connect(&address).ok().map(LocalConnection::from).filter(|bus| matches!(bus.request_name(SERVER, false, false, true), Ok(RequestNameReply::PrimaryOwner))) else {
1425                let _ = ready.send(false);
1426                return;
1427            };
1428            // Answers and presses due later: when, what.
1429            let due: Rc<RefCell<Vec<(std::time::Instant, dbus::Message)>>> = Rc::default();
1430            let later = Rc::clone(&due);
1431            let mut next = 40u32;
1432            let mut stalled = false;
1433            // "late-error": its first Notify refused three seconds late; "exits-fast"/"exits-late": gone after its first Notify.
1434            let mut first_notify = true;
1435            let gone: Rc<RefCell<Option<std::time::Instant>>> = Rc::default();
1436            let leaving = Rc::clone(&gone);
1437            bus.start_receive(
1438                MatchRule::new_method_call(),
1439                Box::new(move |call: dbus::Message, bus: &LocalConnection| {
1440                    let member = call.member().map(|m| m.to_string()).unwrap_or_default();
1441                    if member == "Notify"
1442                        && let Ok((_, replaces, _, title)) = call.read4::<String, u32, String, String>()
1443                    {
1444                        let _ = told.send((std::time::Instant::now(), title, replaces));
1445                    }
1446                    if member == "CloseNotification"
1447                        && let Ok(id) = call.read1::<u32>()
1448                    {
1449                        let _ = told.send((std::time::Instant::now(), CLOSED.to_string(), id));
1450                        if mode != "silent" {
1451                            let mut closed = dbus::Message::new_signal(SERVER_PATH, SERVER, "NotificationClosed").unwrap().append2(id, 3u32);
1452                            closed.set_destination(call.sender().map(|s| s.into_static()));
1453                            let _ = bus.send(closed);
1454                        }
1455                    }
1456                    if mode == "silent" {
1457                        return true;
1458                    }
1459                    if member == "Notify" && first_notify && matches!(mode, "late-error" | "exits-fast" | "exits-late") {
1460                        first_notify = false;
1461                        let now = std::time::Instant::now();
1462                        if mode == "late-error" {
1463                            let refused = call.error(&dbus::strings::ErrorName::new("org.freedesktop.Notifications.Error.Failed").unwrap(), &std::ffi::CString::new("refused late").unwrap());
1464                            later.borrow_mut().push((now + Duration::from_secs(3), refused));
1465                        } else {
1466                            *leaving.borrow_mut() = Some(now + if mode == "exits-fast" { Duration::from_millis(500) } else { Duration::from_secs(3) });
1467                        }
1468                        return true;
1469                    }
1470                    if mode == "stall" && member == "GetCapabilities" && !stalled {
1471                        stalled = true;
1472                        crate::bus::StandIns::signal(bus_pid, "-STOP");
1473                        std::thread::spawn(move || {
1474                            std::thread::sleep(Duration::from_secs(6));
1475                            crate::bus::StandIns::signal(bus_pid, "-CONT");
1476                        });
1477                    }
1478                    let (_, replaces) = if member == "Notify" { call.read2::<String, u32>().unwrap_or_default() } else { (String::new(), 0) };
1479                    let reply = match member.as_str() {
1480                        // GNOME's way, "renumber": a notice replaced gets a number of its own.
1481                        "Notify" if replaces != 0 && mode == "renumber" => call.method_return().append1(replaces + 100),
1482                        "Notify" => {
1483                            let id = if replaces != 0 {
1484                                replaces
1485                            } else {
1486                                next += 1;
1487                                next
1488                            };
1489                            call.method_return().append1(id)
1490                        }
1491                        "GetCapabilities" => call.method_return().append1(vec!["actions", "body"]),
1492                        "CloseNotification" => call.method_return(),
1493                        _ => return true,
1494                    };
1495                    let buttons: Vec<String> = if member == "Notify" { call.read_all::<(String, u32, String, String, String, Vec<String>, dbus::arg::PropMap, i32)>().map(|all| all.5).unwrap_or_default() } else { Vec::new() };
1496                    let press = buttons.iter().any(|key| key == "copy" && !matches!(mode, "shows" | "renumber")).then(|| {
1497                        let id = reply.read1::<u32>().unwrap_or(0);
1498                        let mut press = dbus::Message::new_signal(SERVER_PATH, SERVER, "ActionInvoked").unwrap().append2(id, "copy");
1499                        press.set_destination(call.sender().map(|s| s.into_static()));
1500                        press
1501                    });
1502                    let now = std::time::Instant::now();
1503                    if matches!(mode, "late" | "press-first" | "renumber") && member == "Notify" && replaces == 0 {
1504                        later.borrow_mut().push((now + Duration::from_secs(3), reply));
1505                        let pressed_at = if mode == "press-first" { Duration::from_secs(1) } else { Duration::from_millis(3_300) };
1506                        if let Some(press) = press {
1507                            later.borrow_mut().push((now + pressed_at, press));
1508                        }
1509                    } else if mode == "late" && member == "Notify" {
1510                        later.borrow_mut().push((now + Duration::from_secs(3), reply));
1511                    } else {
1512                        let _ = bus.send(reply);
1513                        if let Some(press) = press {
1514                            let _ = bus.send(press);
1515                        }
1516                    }
1517                    true
1518                }),
1519            );
1520            let _ = ready.send(true);
1521            while !stop.load(std::sync::atomic::Ordering::Relaxed) {
1522                // Gone: its connection closed, the bus answers its callers that no reply will come.
1523                if gone.borrow().is_some_and(|at| std::time::Instant::now() >= at) {
1524                    return;
1525                }
1526                let _ = bus.process(Duration::from_millis(50));
1527                let now = std::time::Instant::now();
1528                let ready: Vec<dbus::Message> = {
1529                    let mut waiting = due.borrow_mut();
1530                    let (go, stay): (Vec<_>, Vec<_>) = std::mem::take(&mut *waiting).into_iter().partition(|(at, _)| *at <= now);
1531                    *waiting = stay;
1532                    go.into_iter().map(|(_, message)| message).collect()
1533                };
1534                for message in ready {
1535                    let _ = bus.send(message);
1536                }
1537            }
1538        });
1539        started.recv_timeout(Duration::from_secs(5)).unwrap_or(false)
1540    }
1541
1542    /// What a stand-in server says of a CloseNotification on its `told`, the
1543    /// number closed beside it.
1544    const CLOSED: &str = "(closed)";
1545
1546    /// What a stand-in server was sent within `within`: (title, replaces), in order.
1547    fn sent_within(notices: &Notices, within: Duration) -> Vec<(String, u32)> {
1548        let until = std::time::Instant::now() + within;
1549        let mut seen = Vec::new();
1550        while let Ok((_, title, replaces)) = notices.recv_timeout(until.saturating_duration_since(std::time::Instant::now())) {
1551            seen.push((title, replaces));
1552        }
1553        seen
1554    }
1555
1556    /// A dose's record as health.rs keeps it, for the stand-ins: its first
1557    /// try's words its own and its late words from `late`, while still to
1558    /// remind (none in `late`: no longer to remind), none once recorded shown
1559    /// too (`late_reminder_wanted`); a late word unconfirmed says so in its
1560    /// body. What it records said on `log`, if any ("going First", "going
1561    /// Late(Retry)", "unshown", "pause", "over"); `settled`: marked elsewhere.
1562    struct StandIn {
1563        name: String,
1564        late: std::sync::Arc<std::sync::Mutex<Option<String>>>,
1565        shown: std::sync::mpsc::Sender<String>,
1566        log: Option<std::sync::mpsc::Sender<String>>,
1567        recorded: bool,
1568        settled: std::sync::Arc<std::sync::atomic::AtomicBool>,
1569    }
1570
1571    impl StandIn {
1572        fn say(&self, what: String) {
1573            if let Some(log) = &self.log {
1574                let _ = log.send(what);
1575            }
1576        }
1577    }
1578
1579    impl DoseRecord for StandIn {
1580        fn going(&mut self, going: Going) -> Say {
1581            self.say(format!("going {going:?}"));
1582            let words = if self.recorded { None } else { self.late.lock().unwrap().clone() };
1583            match (going, words) {
1584                (_, None) => Say::Nothing,
1585                (Going::First, Some(_)) => Say::Own,
1586                (Going::Late(Late::Retry), Some(title)) => Say::Words(title, "22:00. Could not be shown in time. Check before taking it.".into()),
1587                (Going::Late(Late::Unconfirmed), Some(title)) => Say::Words(title, "22:00. Not confirmed in time. Check before taking it.".into()),
1588            }
1589        }
1590        fn refused(&mut self) {
1591            self.say("refused".into());
1592        }
1593        fn shown(&mut self) {
1594            self.recorded = true;
1595            let _ = self.shown.send(self.name.clone());
1596        }
1597        fn unshown(&mut self) {
1598            self.say("unshown".into());
1599        }
1600        fn pause(&mut self, until: i64) -> bool {
1601            self.say(format!("pause {until}"));
1602            !self.recorded && self.late.lock().unwrap().is_some()
1603        }
1604        fn over(&mut self) {
1605            self.say("over".into());
1606        }
1607        fn settled(&mut self) -> bool {
1608            self.settled.load(std::sync::atomic::Ordering::SeqCst)
1609        }
1610    }
1611
1612    /// A dose's reminder for the stand-ins: `name` its title; its button's
1613    /// action and what shows late said on channels; `late` its late words
1614    /// (none: no longer to remind), as health.rs gives them; tried until `until`.
1615    fn dose(name: &str, until: Duration, pressed: &std::sync::mpsc::Sender<String>, shown: &std::sync::mpsc::Sender<String>, late: std::sync::Arc<std::sync::Mutex<Option<String>>>) -> Sending {
1616        dose_as(name, Again { end: std::time::Instant::now() + until, quiet: None, record: Box::new(StandIn { name: name.into(), late, shown: shown.clone(), log: None, recorded: false, settled: std::sync::Arc::default() }) }, pressed)
1617    }
1618
1619    /// The same, its record and its tries as given.
1620    fn dose_as(name: &str, again: Again, pressed: &std::sync::mpsc::Sender<String>) -> Sending {
1621        let (pressed, pressed_name) = (pressed.clone(), name.to_string());
1622        remind_dose(name, "22:00", ("Taken".into(), Box::new(move || drop(pressed.send(pressed_name.clone())))), again)
1623    }
1624
1625    /// Dose reminders and the focus session's lasting notification against a
1626    /// stand-in session bus (`bus::StandIns`, never the session of whoever
1627    /// runs this) and a stand-in notification server on it (review of
1628    /// battery part C, H1 and L4 of the third review, the review's own
1629    /// scenarios):
1630    /// - healthy: shown, while the tick waits; its button counts once;
1631    /// - the bus stopped (SIGSTOP) before the dose's minute, let go 8 s later:
1632    ///   not gone out (the connection never came), said within `FIRST_TRY`;
1633    ///   tried again, shown once the bus answers, late (its words late's, the
1634    ///   doubt first), and its button counts;
1635    /// - stopped, reminders at 1, 10 and 20 s, let go at 30 s: none gone out,
1636    ///   all three shown once the bus answers;
1637    /// - a server that never answers: sent, so maybe, never sent again;
1638    /// - no server, then one (plasmashell starting again): not gone out, then
1639    ///   shown once it is there; none at all once no longer to remind;
1640    /// - the lasting notification, its connection stalled at first: shown
1641    ///   once the bus answers, not lost for the session.
1642    /// Sets this process's session bus address: run alone, by name:
1643    /// `cargo test --release -p sioul-sync notify::tests::reminders_say_their_outcome_and_show_late_after_a_stall -- --ignored --exact --nocapture`.
1644    #[test]
1645    #[ignore = "starts a stand-in dbus-daemon and sets the session bus address: run alone, by name"]
1646    fn reminders_say_their_outcome_and_show_late_after_a_stall() {
1647        use crate::bus::StandIns;
1648        use std::sync::atomic::{AtomicBool, Ordering};
1649        use std::sync::mpsc::channel;
1650        use std::sync::{Arc, Mutex};
1651        use std::time::Instant;
1652        let mut stand_ins = StandIns::new("stand-in-notices");
1653        let (pid, address) = stand_ins.start("session");
1654        // SAFETY: run alone, by name; nothing else in this process reads it meanwhile.
1655        unsafe { std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &address) };
1656        let (told, notices) = channel::<(Instant, String, u32)>();
1657        let stop = Arc::new(AtomicBool::new(false));
1658        assert!(notice_server(&address, "answers", told.clone(), Arc::clone(&stop)), "the stand-in server");
1659        let (pressed, presses) = channel::<String>();
1660        let (late, lates) = channel::<String>();
1661        // The late words: none once no longer to remind.
1662        let words = |title: &str| Arc::new(Mutex::new(Some(format!("Check first: {title}"))));
1663
1664        // Healthy: shown at once, while the tick waits; its button pressed, once.
1665        let start = Instant::now();
1666        assert_eq!(dose("Magnesium · 300 mg", Duration::from_secs(60), &pressed, &late, words("Magnesium · 300 mg")).outcome(FIRST_TRY), Outcome::Shown);
1667        assert!(start.elapsed() < Duration::from_secs(2));
1668        assert_eq!(presses.recv_timeout(Duration::from_secs(5)).as_deref(), Ok("Magnesium · 300 mg"));
1669        assert_eq!(sent_within(&notices, Duration::from_millis(300)), [("Magnesium · 300 mg".to_string(), 0)], "its own words");
1670        assert_eq!(lates.recv_timeout(Duration::from_secs(1)).as_deref(), Ok("Magnesium · 300 mg"), "recorded shown");
1671
1672        // One stall: stopped at 0, the reminder at 1 s, let go at 9 s.
1673        StandIns::signal(pid, "-STOP");
1674        std::thread::sleep(Duration::from_secs(1));
1675        let start = Instant::now();
1676        let outcome = dose("Magnesium · 300 mg", Duration::from_secs(60), &pressed, &late, words("Magnesium · 300 mg")).outcome(FIRST_TRY);
1677        let took = start.elapsed();
1678        assert!(matches!(outcome, Outcome::Unshown(_)) && took <= FIRST_TRY, "the connection never came: not gone out, said within FIRST_TRY: {outcome:?} after {took:?}");
1679        std::thread::sleep(Duration::from_secs(8).saturating_sub(start.elapsed() + Duration::from_secs(1)));
1680        StandIns::signal(pid, "-CONT");
1681        assert_eq!(lates.recv_timeout(Duration::from_secs(20)).as_deref(), Ok("Magnesium · 300 mg"), "shown late once the bus answers");
1682        assert_eq!(presses.recv_timeout(Duration::from_secs(5)).as_deref(), Ok("Magnesium · 300 mg"), "its button works");
1683        assert_eq!(sent_within(&notices, Duration::from_millis(500)), [("Check first: Magnesium · 300 mg".to_string(), 0)], "once, the doubt first");
1684
1685        // Three reminders over 30 s, the bus stopped all along.
1686        StandIns::signal(pid, "-STOP");
1687        let start = Instant::now();
1688        let mut sending = Vec::new();
1689        for (at, name) in [(1, "First"), (10, "Second"), (20, "Third")] {
1690            std::thread::sleep(Duration::from_secs(at).saturating_sub(start.elapsed()));
1691            let sent = dose(name, Duration::from_secs(90), &pressed, &late, words(name));
1692            assert!(matches!(sent.outcome(FIRST_TRY), Outcome::Unshown(_)), "{name}: not gone out");
1693            sending.push(sent);
1694        }
1695        std::thread::sleep(Duration::from_secs(30).saturating_sub(start.elapsed()));
1696        StandIns::signal(pid, "-CONT");
1697        let mut shown: Vec<String> = (0..3).filter_map(|_| lates.recv_timeout(Duration::from_secs(25)).ok()).collect();
1698        shown.sort();
1699        assert_eq!(shown, ["First", "Second", "Third"], "all three shown once the bus answers");
1700        let mut seen: Vec<String> = sent_within(&notices, Duration::from_secs(2)).into_iter().map(|(title, _)| title).collect();
1701        seen.sort();
1702        assert_eq!(seen, ["Check first: First", "Check first: Second", "Check first: Third"]);
1703        for _ in 0..3 {
1704            let _ = presses.recv_timeout(Duration::from_secs(5));
1705        }
1706
1707        // A server that never answers: sent, so maybe; never sent again while
1708        // it is there (its tries over at 12 s: gone with it unanswered after
1709        // that, nothing more; the tries again after a server gone are
1710        // `reminders_go_on_after_a_late_refusal_or_a_lost_server`'s).
1711        stop.store(true, Ordering::Relaxed);
1712        std::thread::sleep(Duration::from_millis(500));
1713        let quiet = Arc::new(AtomicBool::new(false));
1714        assert!(notice_server(&address, "silent", told.clone(), Arc::clone(&quiet)), "the silent server");
1715        let start = Instant::now();
1716        let outcome = dose("Magnesium · 300 mg", Duration::from_secs(12), &pressed, &late, words("Magnesium · 300 mg")).outcome(FIRST_TRY);
1717        assert!(matches!(outcome, Outcome::Maybe(_)) && start.elapsed() < FIRST_TRY, "{outcome:?} after {:?}", start.elapsed());
1718        assert_eq!(sent_within(&notices, Duration::from_secs(15)), [("Magnesium · 300 mg".to_string(), 0)], "asked once, never again");
1719        assert!(lates.try_recv().is_err());
1720
1721        // No server, then one: not gone out, then shown once it is there; none once no longer to remind.
1722        quiet.store(true, Ordering::Relaxed);
1723        std::thread::sleep(Duration::from_millis(500));
1724        let outcome = dose("Magnesium · 300 mg", Duration::from_secs(60), &pressed, &late, words("Magnesium · 300 mg")).outcome(FIRST_TRY);
1725        assert!(matches!(outcome, Outcome::Unshown(_)), "no server: {outcome:?}");
1726        std::thread::sleep(Duration::from_secs(6));
1727        let back = Arc::new(AtomicBool::new(false));
1728        assert!(notice_server(&address, "answers", told.clone(), Arc::clone(&back)));
1729        assert_eq!(lates.recv_timeout(Duration::from_secs(15)).as_deref(), Ok("Magnesium · 300 mg"), "shown once the server is there");
1730        let _ = presses.recv_timeout(Duration::from_secs(5));
1731        assert_eq!(sent_within(&notices, Duration::from_millis(500)), [("Check first: Magnesium · 300 mg".to_string(), 0)]);
1732        back.store(true, Ordering::Relaxed);
1733        std::thread::sleep(Duration::from_millis(500));
1734        let not_any_more = words("Magnesium · 300 mg");
1735        let outcome = dose("Magnesium · 300 mg", Duration::from_secs(60), &pressed, &late, Arc::clone(&not_any_more)).outcome(FIRST_TRY);
1736        assert!(matches!(outcome, Outcome::Unshown(_)));
1737        *not_any_more.lock().unwrap() = None;
1738        let again = Arc::new(AtomicBool::new(false));
1739        assert!(notice_server(&address, "answers", told.clone(), Arc::clone(&again)));
1740        assert!(sent_within(&notices, Duration::from_secs(12)).is_empty(), "marked meanwhile: never shown");
1741        assert!(lates.try_recv().is_err());
1742
1743        // The lasting notification, its connection stalled at first: shown once the bus answers.
1744        StandIns::signal(pid, "-STOP");
1745        let lasting = ongoing("Write to the bank", "Since 14:02, for 25 min", vec![("stop".to_string(), "Stop".to_string())], Box::new(|_: &str| {}));
1746        std::thread::sleep(Duration::from_secs(8));
1747        StandIns::signal(pid, "-CONT");
1748        assert_eq!(sent_within(&notices, Duration::from_secs(20)).first().map(|(title, _)| title.as_str()), Some("Write to the bank"), "not lost for the session");
1749        lasting.close(Duration::from_secs(3));
1750        again.store(true, Ordering::Relaxed);
1751    }
1752
1753    /// A dose's reminder whose fate the tick could not know (review of battery
1754    /// part C, the fourth review's H1 timelines), against a stand-in bus and
1755    /// server:
1756    /// - the server shows it at once and answers three seconds late: maybe
1757    ///   (sent, not answered within its two seconds); its late answer heard
1758    ///   on the connection kept: recorded shown (`Again::shown`), said again
1759    ///   in place with the doubt first (the same number), and its "Taken",
1760    ///   pressed after, counts once;
1761    /// - the same, the dose marked before the late answer: taken away by its
1762    ///   number, never recorded a reminder;
1763    /// - the bus stopped while the try was under way (after the connection,
1764    ///   before the server's answers): maybe; let go, shown, late: the doubt
1765    ///   first in place, "Taken" counts;
1766    /// - sent after the tick stopped waiting: the doubt first at once, one
1767    ///   notice, recorded shown, "Taken" counts; marked before it went: never
1768    ///   sent, nor tried again.
1769    /// Sets this process's session bus address: run alone, by name:
1770    /// `cargo test --release -p sioul-sync notify::tests::reminders_whose_fate_was_not_known_say_the_doubt_once_known_shown -- --ignored --exact --nocapture`.
1771    #[test]
1772    #[ignore = "starts a stand-in dbus-daemon and sets the session bus address: run alone, by name"]
1773    fn reminders_whose_fate_was_not_known_say_the_doubt_once_known_shown() {
1774        use crate::bus::StandIns;
1775        use std::sync::atomic::{AtomicBool, Ordering};
1776        use std::sync::mpsc::channel;
1777        use std::sync::{Arc, Mutex};
1778        use std::time::Instant;
1779        let mut stand_ins = StandIns::new("stand-in-fates");
1780        let (pid, address) = stand_ins.start("session");
1781        // SAFETY: run alone, by name; nothing else in this process reads it meanwhile.
1782        unsafe { std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &address) };
1783        let (told, notices) = channel::<(Instant, String, u32)>();
1784        let (pressed, presses) = channel::<String>();
1785        let (late, lates) = channel::<String>();
1786        let words = || Arc::new(Mutex::new(Some("Check first: Levothyroxine · 75 µg".to_string())));
1787
1788        // The server answers three seconds late.
1789        let slow = Arc::new(AtomicBool::new(false));
1790        assert!(notice_server(&address, "late", told.clone(), Arc::clone(&slow)));
1791        let start = Instant::now();
1792        let outcome = dose("Levothyroxine · 75 µg", Duration::from_secs(60), &pressed, &late, words()).outcome(FIRST_TRY);
1793        assert!(matches!(outcome, Outcome::Maybe(_)) && start.elapsed() < Duration::from_secs(3), "sent, not answered in time: maybe ({outcome:?} after {:?})", start.elapsed());
1794        assert_eq!(lates.recv_timeout(Duration::from_secs(5)).as_deref(), Ok("Levothyroxine · 75 µg"), "its late answer heard: recorded shown");
1795        let sent = sent_within(&notices, Duration::from_secs(2));
1796        assert_eq!(sent.len(), 2, "{sent:?}");
1797        assert_eq!(sent[0], ("Levothyroxine · 75 µg".to_string(), 0));
1798        assert!(sent[1].0 == "Check first: Levothyroxine · 75 µg" && sent[1].1 != 0, "said again in place, the doubt first: {sent:?}");
1799        assert_eq!(presses.recv_timeout(Duration::from_secs(5)).as_deref(), Ok("Levothyroxine · 75 µg"), "its Taken, pressed after, counts");
1800        assert!(presses.recv_timeout(Duration::from_secs(4)).is_err(), "once");
1801        slow.store(true, Ordering::Relaxed);
1802        std::thread::sleep(Duration::from_millis(500));
1803
1804        // The same, the dose marked before the late answer: taken away, never recorded a reminder.
1805        let slow = Arc::new(AtomicBool::new(false));
1806        assert!(notice_server(&address, "late", told.clone(), Arc::clone(&slow)));
1807        let marked = words();
1808        let start = Instant::now();
1809        let outcome = dose("Levothyroxine · 75 µg", Duration::from_secs(60), &pressed, &late, Arc::clone(&marked)).outcome(FIRST_TRY);
1810        assert!(matches!(outcome, Outcome::Maybe(_)) && start.elapsed() < Duration::from_secs(3), "{outcome:?} after {:?}", start.elapsed());
1811        *marked.lock().unwrap() = None;
1812        let sent = sent_within(&notices, Duration::from_secs(3));
1813        assert_eq!(sent.first(), Some(&("Levothyroxine · 75 µg".to_string(), 0)), "{sent:?}");
1814        assert!(sent.iter().any(|(title, id)| title == CLOSED && *id != 0), "taken away by its number: {sent:?}");
1815        assert!(!sent.iter().any(|(title, _)| title.starts_with("Check first")), "{sent:?}");
1816        assert!(lates.recv_timeout(Duration::from_secs(1)).is_err(), "never recorded a reminder");
1817        assert!(presses.recv_timeout(Duration::from_secs(1)).is_err(), "its button gone with it");
1818        slow.store(true, Ordering::Relaxed);
1819        std::thread::sleep(Duration::from_millis(500));
1820
1821        // The bus stopped while the try is under way: at the server's GetCapabilities, for six seconds.
1822        let stalling = Arc::new(AtomicBool::new(false));
1823        assert!(notice_server_on(&address, "stall", pid, told.clone(), Arc::clone(&stalling)));
1824        let start = Instant::now();
1825        let outcome = dose("Levothyroxine · 75 µg", Duration::from_secs(60), &pressed, &late, words()).outcome(FIRST_TRY);
1826        assert!(matches!(outcome, Outcome::Maybe(_)) && start.elapsed() < FIRST_TRY, "sent into the stall: maybe ({outcome:?} after {:?})", start.elapsed());
1827        assert_eq!(lates.recv_timeout(Duration::from_secs(10)).as_deref(), Ok("Levothyroxine · 75 µg"), "let go: shown, late");
1828        let sent = sent_within(&notices, Duration::from_secs(3));
1829        assert_eq!(sent.first(), Some(&("Levothyroxine · 75 µg".to_string(), 0)), "{sent:?}");
1830        assert!(sent.iter().any(|(title, replaces)| title == "Check first: Levothyroxine · 75 µg" && *replaces != 0), "the doubt first, in place: {sent:?}");
1831        assert_eq!(presses.recv_timeout(Duration::from_secs(5)).as_deref(), Ok("Levothyroxine · 75 µg"), "its Taken counts");
1832        stalling.store(true, Ordering::Relaxed);
1833        std::thread::sleep(Duration::from_millis(500));
1834
1835        // Sent after the tick stopped waiting (the bus stopped a moment while
1836        // it connects, the tick gone meanwhile): the doubt first at once, once.
1837        let quick = Arc::new(AtomicBool::new(false));
1838        assert!(notice_server(&address, "answers", told.clone(), Arc::clone(&quick)));
1839        StandIns::signal(pid, "-STOP");
1840        drop(dose("Levothyroxine · 75 µg", Duration::from_secs(60), &pressed, &late, words()));
1841        std::thread::sleep(Duration::from_millis(300));
1842        StandIns::signal(pid, "-CONT");
1843        assert_eq!(lates.recv_timeout(Duration::from_secs(5)).as_deref(), Ok("Levothyroxine · 75 µg"), "recorded shown");
1844        assert_eq!(sent_within(&notices, Duration::from_secs(2)), [("Check first: Levothyroxine · 75 µg".to_string(), 0)], "the doubt first at once, one notice");
1845        assert_eq!(presses.recv_timeout(Duration::from_secs(5)).as_deref(), Ok("Levothyroxine · 75 µg"), "its Taken counts");
1846        assert!(presses.recv_timeout(Duration::from_secs(1)).is_err(), "once");
1847
1848        // Marked before it went, the tick gone: never sent, nor tried again.
1849        StandIns::signal(pid, "-STOP");
1850        let gone = words();
1851        let dropped = dose("Levothyroxine · 75 µg", Duration::from_secs(60), &pressed, &late, Arc::clone(&gone));
1852        *gone.lock().unwrap() = None;
1853        drop(dropped);
1854        std::thread::sleep(Duration::from_millis(300));
1855        StandIns::signal(pid, "-CONT");
1856        assert!(sent_within(&notices, Duration::from_secs(8)).is_empty(), "never sent");
1857        assert!(lates.try_recv().is_err());
1858        quick.store(true, Ordering::Relaxed);
1859    }
1860
1861    /// A relay before the stand-in bus at `bus`, each answer of which comes
1862    /// `delay` late once its client is connected (after its "BEGIN"), as a
1863    /// bus on a machine swapping does (the fourth review's slow bus): its
1864    /// address. Ends with `stop`.
1865    fn slow_relay(bus: &str, delay: Duration, stop: std::sync::Arc<std::sync::atomic::AtomicBool>) -> String {
1866        use std::io::{Read, Write};
1867        use std::os::unix::net::{UnixListener, UnixStream};
1868        use std::sync::Arc;
1869        use std::sync::atomic::{AtomicBool, Ordering};
1870        let target = crate::bus::unix_path(bus).expect("a unix:path= address");
1871        let socket = target.with_file_name("slow-relay");
1872        let _ = std::fs::remove_file(&socket);
1873        let listener = UnixListener::bind(&socket).unwrap();
1874        listener.set_nonblocking(true).unwrap();
1875        std::thread::spawn(move || {
1876            while !stop.load(Ordering::Relaxed) {
1877                let Ok((client, _)) = listener.accept() else {
1878                    std::thread::sleep(Duration::from_millis(20));
1879                    continue;
1880                };
1881                let _ = client.set_nonblocking(false);
1882                let Ok(upstream) = UnixStream::connect(&target) else { continue };
1883                let begun = Arc::new(AtomicBool::new(false));
1884                // The client's words go at once; its "BEGIN" ends its authentication.
1885                let (mut from_client, mut to_bus, seen) = (client.try_clone().unwrap(), upstream.try_clone().unwrap(), Arc::clone(&begun));
1886                std::thread::spawn(move || {
1887                    let (mut buffer, mut said) = ([0u8; 65_536], Vec::new());
1888                    while let Ok(n) = from_client.read(&mut buffer) {
1889                        if n == 0 {
1890                            break;
1891                        }
1892                        if !seen.load(Ordering::SeqCst) {
1893                            said.extend_from_slice(&buffer[..n]);
1894                            if said.windows(7).any(|w| w == b"BEGIN\r\n") {
1895                                seen.store(true, Ordering::SeqCst);
1896                            }
1897                        }
1898                        if to_bus.write_all(&buffer[..n]).is_err() {
1899                            break;
1900                        }
1901                    }
1902                    let _ = to_bus.shutdown(std::net::Shutdown::Both);
1903                });
1904                // The bus's words, in order, each `delay` late once connected.
1905                let (mut from_bus, mut to_client) = (upstream, client);
1906                let (queued, queue) = std::sync::mpsc::channel::<(std::time::Instant, Vec<u8>)>();
1907                std::thread::spawn(move || {
1908                    while let Ok((at, bytes)) = queue.recv() {
1909                        std::thread::sleep(at.saturating_duration_since(std::time::Instant::now()));
1910                        if to_client.write_all(&bytes).is_err() {
1911                            break;
1912                        }
1913                    }
1914                    let _ = to_client.shutdown(std::net::Shutdown::Both);
1915                });
1916                std::thread::spawn(move || {
1917                    let mut buffer = [0u8; 65_536];
1918                    while let Ok(n) = from_bus.read(&mut buffer) {
1919                        if n == 0 {
1920                            break;
1921                        }
1922                        let at = std::time::Instant::now() + if begun.load(Ordering::SeqCst) { delay } else { Duration::ZERO };
1923                        if queued.send((at, buffer[..n].to_vec())).is_err() {
1924                            break;
1925                        }
1926                    }
1927                });
1928            }
1929        });
1930        format!("unix:path={}", crate::bus::escaped(&socket))
1931    }
1932
1933    /// A slow session bus: every answer 3.4 s late once connected (the
1934    /// fourth review's relay: Hello and both matches late, each within the
1935    /// waits of before). The first try, its matches waited a second each so
1936    /// that it fits in the tick's wait, does not go out: not shown, said
1937    /// within `FIRST_TRY`, nothing sent. The try after it waits as a slow
1938    /// bus needs (`TRY_AGAIN`): sent, its answer late (maybe, then shown),
1939    /// shown once, the doubt first, recorded shown; its "Taken" counts once.
1940    /// Sets this process's session bus address: run alone, by name:
1941    /// `cargo test --release -p sioul-sync notify::tests::a_slow_bus_shows_the_reminder_late_the_doubt_first -- --ignored --exact --nocapture`.
1942    #[test]
1943    #[ignore = "starts a stand-in dbus-daemon and sets the session bus address: run alone, by name"]
1944    fn a_slow_bus_shows_the_reminder_late_the_doubt_first() {
1945        use crate::bus::StandIns;
1946        use std::sync::atomic::{AtomicBool, Ordering};
1947        use std::sync::mpsc::channel;
1948        use std::sync::{Arc, Mutex};
1949        use std::time::Instant;
1950        let mut stand_ins = StandIns::new("stand-in-slow");
1951        let (_pid, bus) = stand_ins.start("session");
1952        let stop = Arc::new(AtomicBool::new(false));
1953        let address = slow_relay(&bus, Duration::from_millis(3_400), Arc::clone(&stop));
1954        // SAFETY: run alone, by name; nothing else in this process reads it meanwhile.
1955        unsafe { std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &address) };
1956        let (told, notices) = channel::<(Instant, String, u32)>();
1957        // The server on the bus itself: only Sioul's side is slow.
1958        assert!(notice_server(&bus, "answers", told, Arc::clone(&stop)), "the stand-in server");
1959        let (pressed, presses) = channel::<String>();
1960        let (late, lates) = channel::<String>();
1961        let start = Instant::now();
1962        let words = Arc::new(Mutex::new(Some("Check first: Levothyroxine · 75 µg".to_string())));
1963        let outcome = dose("Levothyroxine · 75 µg", Duration::from_secs(120), &pressed, &late, words).outcome(FIRST_TRY);
1964        assert!(matches!(outcome, Outcome::Unshown(_)) && start.elapsed() < FIRST_TRY, "its matches not answered in time, nothing sent: {outcome:?} after {:?}", start.elapsed());
1965        assert_eq!(lates.recv_timeout(Duration::from_secs(45)).as_deref(), Ok("Levothyroxine · 75 µg"), "shown by the try after it, recorded");
1966        assert_eq!(sent_within(&notices, Duration::from_secs(1)), [("Check first: Levothyroxine · 75 µg".to_string(), 0)], "once, the doubt first");
1967        assert_eq!(presses.recv_timeout(Duration::from_secs(10)).as_deref(), Ok("Levothyroxine · 75 µg"), "its Taken counts");
1968        assert!(presses.recv_timeout(Duration::from_secs(5)).is_err(), "once");
1969        stop.store(true, Ordering::Relaxed);
1970    }
1971
1972    /// What a dose's reminder records and keeps to, against a stand-in bus
1973    /// and server (review of battery part C, the fifth review):
1974    /// - a try the bus refuses (no server): recorded going, then taken back;
1975    /// - its tries over at their end: promised never again (`over`);
1976    /// - a phone's alarm due: none during the quiet, promised before it
1977    ///   begins (`pause`), the tries resumed after it, the doubt first;
1978    /// - shown, its dose then marked elsewhere: taken away by its number (L2);
1979    /// - "Taken" pressed before a late answer, the notice kept by the server:
1980    ///   recorded shown, never said again over the press, which counts once (L3);
1981    /// - said again in place, the server giving it a number of its own
1982    ///   (GNOME): followed, closed by that number (L3);
1983    /// - the lasting notification taken away before its late answer: closed
1984    ///   once it comes (L4).
1985    /// Sets this process's session bus address: run alone, by name:
1986    /// `cargo test --release -p sioul-sync notify::tests::reminders_keep_their_word -- --ignored --exact --nocapture`.
1987    #[test]
1988    #[ignore = "starts a stand-in dbus-daemon and sets the session bus address: run alone, by name"]
1989    fn reminders_keep_their_word() {
1990        use crate::bus::StandIns;
1991        use std::sync::atomic::{AtomicBool, Ordering};
1992        use std::sync::mpsc::channel;
1993        use std::sync::{Arc, Mutex};
1994        use std::time::Instant;
1995        let mut stand_ins = StandIns::new("stand-in-word");
1996        let (_pid, address) = stand_ins.start("session");
1997        // SAFETY: run alone, by name; nothing else in this process reads it meanwhile.
1998        unsafe { std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &address) };
1999        let (told, notices) = channel::<(Instant, String, u32)>();
2000        let (pressed, presses) = channel::<String>();
2001        let (shown, shows) = channel::<String>();
2002        let (log, logged) = channel::<String>();
2003        let words = |title: &str| Arc::new(Mutex::new(Some(format!("Check first: {title}"))));
2004        let record = |name: &str, settled: &Arc<AtomicBool>| Box::new(StandIn { name: name.into(), late: words(name), shown: shown.clone(), log: Some(log.clone()), recorded: false, settled: Arc::clone(settled) });
2005        let drain = |within: Duration| -> Vec<String> {
2006            let until = Instant::now() + within;
2007            let mut seen = Vec::new();
2008            while let Ok(item) = logged.recv_timeout(until.saturating_duration_since(Instant::now())) {
2009                seen.push(item);
2010            }
2011            seen
2012        };
2013        let unsettled: Arc<AtomicBool> = Arc::default();
2014
2015        // No server: the first try goes to the bus, which says no: taken back; its end comes: over.
2016        let start = Instant::now();
2017        let outcome = dose_as("Iron", Again { end: start + Duration::from_secs(7), quiet: None, record: record("Iron", &unsettled) }, &pressed).outcome(FIRST_TRY);
2018        assert!(matches!(outcome, Outcome::Unshown(_)), "{outcome:?}");
2019        let seen = drain(Duration::from_secs(9));
2020        assert_eq!(seen.first().map(String::as_str), Some("going First"), "{seen:?}");
2021        assert_eq!(seen.get(1).map(String::as_str), Some("refused"), "{seen:?}");
2022        assert_eq!(seen.get(2).map(String::as_str), Some("unshown"), "{seen:?}");
2023        assert_eq!(seen.last().map(String::as_str), Some("over"), "{seen:?}");
2024        assert!(seen.iter().filter(|s| s.starts_with("going Late(Retry)")).count() >= 1, "tried again, the doubt first: {seen:?}");
2025
2026        // A phone's alarm due from 8 to 16 s: promised, none meanwhile (a server there from 9 s), then shown, the doubt first.
2027        let start = Instant::now();
2028        let unix = 1_800_000_016;
2029        let again = Again { end: start + Duration::from_secs(40), quiet: Some((start + Duration::from_secs(8), start + Duration::from_secs(16), unix)), record: record("Zinc", &unsettled) };
2030        assert!(matches!(dose_as("Zinc", again, &pressed).outcome(FIRST_TRY), Outcome::Unshown(_)));
2031        std::thread::sleep(Duration::from_secs(9).saturating_sub(start.elapsed()));
2032        let up = Arc::new(AtomicBool::new(false));
2033        assert!(notice_server(&address, "answers", told.clone(), Arc::clone(&up)));
2034        assert_eq!(shows.recv_timeout(Duration::from_secs(20)).as_deref(), Ok("Zinc"), "shown after the quiet");
2035        assert!(start.elapsed() >= Duration::from_secs(16), "none during the quiet: {:?}", start.elapsed());
2036        let seen = drain(Duration::from_millis(300));
2037        assert!(seen.contains(&format!("pause {unix}")), "promised before it began: {seen:?}");
2038        assert_eq!(sent_within(&notices, Duration::from_millis(300)).first().map(|(title, _)| title.as_str()), Some("Check first: Zinc"));
2039        let _ = presses.recv_timeout(Duration::from_secs(3));
2040        up.store(true, Ordering::Relaxed);
2041        std::thread::sleep(Duration::from_millis(500));
2042
2043        // Shown, then marked elsewhere: taken away by its number.
2044        let quiet_server = Arc::new(AtomicBool::new(false));
2045        assert!(notice_server(&address, "shows", told.clone(), Arc::clone(&quiet_server)));
2046        let settled = Arc::new(AtomicBool::new(false));
2047        let outcome = dose_as("Folate", Again { end: Instant::now() + Duration::from_secs(60), quiet: None, record: record("Folate", &settled) }, &pressed).outcome(FIRST_TRY);
2048        assert_eq!(outcome, Outcome::Shown);
2049        let first = sent_within(&notices, Duration::from_millis(500));
2050        assert_eq!(first.len(), 1, "{first:?}");
2051        settled.store(true, Ordering::SeqCst);
2052        let closed = sent_within(&notices, Duration::from_secs(4));
2053        assert!(closed.iter().any(|(title, id)| title == CLOSED && *id != 0), "taken away: {closed:?}");
2054        quiet_server.store(true, Ordering::Relaxed);
2055        std::thread::sleep(Duration::from_millis(500));
2056        let _ = shows.try_iter().count();
2057
2058        // "Taken" pressed before a late answer, the notice kept: recorded shown, never said again, the press once.
2059        let pressing = Arc::new(AtomicBool::new(false));
2060        assert!(notice_server(&address, "press-first", told.clone(), Arc::clone(&pressing)));
2061        let outcome = dose("Iodine", Duration::from_secs(60), &pressed, &shown, words("Iodine")).outcome(FIRST_TRY);
2062        assert!(matches!(outcome, Outcome::Maybe(_)), "{outcome:?}");
2063        assert_eq!(shows.recv_timeout(Duration::from_secs(5)).as_deref(), Ok("Iodine"), "recorded shown at its late answer");
2064        assert_eq!(presses.recv_timeout(Duration::from_secs(3)).as_deref(), Ok("Iodine"), "its Taken counts");
2065        assert!(presses.recv_timeout(Duration::from_secs(2)).is_err(), "once");
2066        let sent = sent_within(&notices, Duration::from_millis(500));
2067        assert_eq!(sent, [("Iodine".to_string(), 0)], "never said again over the press");
2068        pressing.store(true, Ordering::Relaxed);
2069        std::thread::sleep(Duration::from_millis(500));
2070
2071        // Said again in place, given a number of its own (GNOME): followed, closed by it.
2072        let renumbering = Arc::new(AtomicBool::new(false));
2073        assert!(notice_server(&address, "renumber", told.clone(), Arc::clone(&renumbering)));
2074        let settled = Arc::new(AtomicBool::new(false));
2075        let outcome = dose_as("Calcium", Again { end: Instant::now() + Duration::from_secs(60), quiet: None, record: record("Calcium", &settled) }, &pressed).outcome(FIRST_TRY);
2076        assert!(matches!(outcome, Outcome::Maybe(_)), "{outcome:?}");
2077        assert_eq!(shows.recv_timeout(Duration::from_secs(5)).as_deref(), Ok("Calcium"));
2078        let sent = sent_within(&notices, Duration::from_secs(1));
2079        let replaced = sent.iter().find(|(title, replaces)| title == "Check first: Calcium" && *replaces != 0).map(|(_, replaces)| *replaces);
2080        assert!(replaced.is_some(), "said again in place: {sent:?}");
2081        settled.store(true, Ordering::SeqCst);
2082        let closed = sent_within(&notices, Duration::from_secs(4));
2083        assert!(closed.iter().any(|(title, id)| title == CLOSED && Some(*id) == replaced.map(|r| r + 100)), "closed by its new number: {closed:?}");
2084        renumbering.store(true, Ordering::Relaxed);
2085        std::thread::sleep(Duration::from_millis(500));
2086
2087        // The lasting notification closed before its late answer: closed once the answer comes.
2088        let late_server = Arc::new(AtomicBool::new(false));
2089        assert!(notice_server(&address, "late", told.clone(), Arc::clone(&late_server)));
2090        let lasting = ongoing("Write to the bank", "Since 14:02, for 25 min", vec![("stop".to_string(), "Stop".to_string())], Box::new(|_: &str| {}));
2091        std::thread::sleep(Duration::from_millis(500));
2092        lasting.close(Duration::from_millis(500));
2093        let seen = sent_within(&notices, Duration::from_secs(6));
2094        assert_eq!(seen.first().map(|(title, _)| title.as_str()), Some("Write to the bank"), "{seen:?}");
2095        assert!(seen.iter().any(|(title, id)| title == CLOSED && *id != 0), "closed by its number once answered: {seen:?}");
2096        late_server.store(true, Ordering::Relaxed);
2097    }
2098
2099    /// A notice taken by the server, then refused late, or the server gone
2100    /// with it unanswered (the sixth review's M2, L2): it may have shown, its
2101    /// record says so still (never taken back), and it is tried again, the
2102    /// doubt first, while its grace lasts, by the server there then.
2103    /// Sets this process's session bus address: run alone, by name:
2104    /// `cargo test --release -p sioul-sync notify::tests::reminders_go_on_after_a_late_refusal_or_a_lost_server -- --ignored --exact --nocapture`.
2105    #[test]
2106    #[ignore = "starts a stand-in dbus-daemon and sets the session bus address: run alone, by name"]
2107    fn reminders_go_on_after_a_late_refusal_or_a_lost_server() {
2108        use crate::bus::StandIns;
2109        use std::sync::atomic::{AtomicBool, Ordering};
2110        use std::sync::mpsc::channel;
2111        use std::sync::{Arc, Mutex};
2112        use std::time::Instant;
2113        let mut stand_ins = StandIns::new("stand-in-lost");
2114        let (_pid, address) = stand_ins.start("session");
2115        // SAFETY: run alone, by name; nothing else in this process reads it meanwhile.
2116        unsafe { std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &address) };
2117        let (told, notices) = channel::<(Instant, String, u32)>();
2118        let (pressed, presses) = channel::<String>();
2119        let (shown, shows) = channel::<String>();
2120        let (log, logged) = channel::<String>();
2121        let record = |name: &str| Box::new(StandIn { name: name.into(), late: Arc::new(Mutex::new(Some(format!("Check first: {name}")))), shown: shown.clone(), log: Some(log.clone()), recorded: false, settled: Arc::default() });
2122        let again = |name: &str| Again { end: Instant::now() + Duration::from_secs(40), quiet: None, record: record(name) };
2123        for (mode, name) in [("late-error", "Iron"), ("exits-late", "Zinc"), ("exits-fast", "Folate")] {
2124            let stop = Arc::new(AtomicBool::new(false));
2125            assert!(notice_server(&address, mode, told.clone(), Arc::clone(&stop)), "{mode}");
2126            let outcome = dose_as(name, again(name), &pressed).outcome(FIRST_TRY);
2127            assert!(matches!(outcome, Outcome::Maybe(_)), "{mode}: sent, its fate not known: {outcome:?}");
2128            // A server there again for what comes next (the one gone, or the one refusing, answers now).
2129            std::thread::sleep(Duration::from_secs(4));
2130            let back = Arc::new(AtomicBool::new(false));
2131            if mode != "late-error" {
2132                assert!(notice_server(&address, "answers", told.clone(), Arc::clone(&back)), "{mode}: a server back");
2133            }
2134            assert_eq!(shows.recv_timeout(Duration::from_secs(20)).as_deref(), Ok(name), "{mode}: shown again, late");
2135            let sent = sent_within(&notices, Duration::from_millis(500));
2136            assert_eq!(sent.first(), Some(&(name.to_string(), 0)), "{mode}: {sent:?}");
2137            assert_eq!(sent.last().map(|(title, _)| title.as_str()), Some(&*format!("Check first: {name}")), "{mode}: the doubt first: {sent:?}");
2138            let seen: Vec<String> = logged.try_iter().collect();
2139            assert!(!seen.contains(&"refused".to_string()), "{mode}: it may have shown: never taken back: {seen:?}");
2140            assert!(seen.contains(&"going Late(Unconfirmed)".to_string()) && !seen.contains(&"going Late(Retry)".to_string()), "{mode}: not confirmed in time, never \"could not be shown\" (the seventh review's L2): {seen:?}");
2141            let _ = presses.recv_timeout(Duration::from_secs(3));
2142            stop.store(true, Ordering::Relaxed);
2143            back.store(true, Ordering::Relaxed);
2144            std::thread::sleep(Duration::from_millis(600));
2145        }
2146    }
2147
2148    /// The focus session's lasting notification against a server that shows
2149    /// it at once and answers three seconds late, on a bus of the test's own
2150    /// (dbus-run-session; skipped where a server is there already): never sent
2151    /// again while not answered (shown twice, neither taken away: review of
2152    /// battery part C, the fourth review's L1); a change made meanwhile goes in
2153    /// place of the number the late answer gave; closed by that number.
2154    #[test]
2155    fn a_lasting_notice_answered_late_is_never_shown_twice() {
2156        use std::sync::atomic::{AtomicBool, Ordering};
2157        let _alone = crate::bus::NOTIFICATION_SERVER.lock().unwrap_or_else(|e| e.into_inner());
2158        let Some(address) = crate::bus::session_address() else {
2159            eprintln!("No bus of the test's own (dbus-run-session): skipped.");
2160            return;
2161        };
2162        let (told, notices) = std::sync::mpsc::channel();
2163        let stop = std::sync::Arc::new(AtomicBool::new(false));
2164        // Never in a server's place: a stand-in of a test before given a moment to leave.
2165        let until = std::time::Instant::now() + Duration::from_secs(2);
2166        while !notice_server(&address, "late", told.clone(), std::sync::Arc::clone(&stop)) {
2167            if std::time::Instant::now() >= until {
2168                eprintln!("No bus of the test's own (dbus-run-session), or a notification server on it already: skipped.");
2169                return;
2170            }
2171            std::thread::sleep(Duration::from_millis(100));
2172        }
2173        let lasting = ongoing("Write to the bank", "Since 14:02, for 25 min", vec![("stop".to_string(), "Stop".to_string())], Box::new(|_: &str| {}));
2174        std::thread::sleep(Duration::from_secs(1));
2175        lasting.update("Write to the bank", "Paused, 1 min so far", vec![("resume".to_string(), "Go on".to_string())]);
2176        let sent = sent_within(&notices, Duration::from_secs(9));
2177        assert_eq!(sent.first(), Some(&("Write to the bank".to_string(), 0)), "{sent:?}");
2178        assert!(sent.iter().skip(1).all(|(_, replaces)| *replaces != 0), "never a second with no number to replace: {sent:?}");
2179        assert!(sent.len() >= 2, "the change went, in place: {sent:?}");
2180        lasting.close(Duration::from_secs(5));
2181        stop.store(true, Ordering::Relaxed);
2182    }
2183
2184    /// Codes, reminders and mail, against a notification server of the test's
2185    /// own on a bus of its own (run under dbus-run-session; skipped where a
2186    /// server is there already: nothing here may reach the desktop's). Each
2187    /// goes as notify-rust's `Notification` sent it (`send`): "Sioul", the
2188    /// icon, the title, the body as a server reading markup takes it, the
2189    /// buttons, no sound, Sioul's desktop file, ten minutes; its urgency said
2190    /// once (a mail at normal urgency, unless someone Always through is in
2191    /// it). A button pressed at the server runs its action, here, once.
2192    #[test]
2193    fn notifications_go_as_before_and_hear_their_button() {
2194        use dbus::blocking::LocalConnection;
2195        use dbus::channel::{MatchingReceiver, Sender as _};
2196        use dbus::message::MatchRule;
2197        use std::sync::mpsc::channel;
2198
2199        /// What the stand-in server was sent.
2200        #[derive(Debug, PartialEq)]
2201        struct Sent {
2202            app: String,
2203            replaces: u32,
2204            icon: String,
2205            title: String,
2206            body: String,
2207            buttons: Vec<String>,
2208            hints: Vec<String>,
2209            sound_off: bool,
2210            entry: String,
2211            urgency: u8,
2212            timeout: i32,
2213        }
2214        let _alone = crate::bus::NOTIFICATION_SERVER.lock().unwrap_or_else(|e| e.into_inner());
2215        let (told, sent) = channel::<Sent>();
2216        let (ready, started) = channel::<bool>();
2217        let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
2218        let stopped = std::sync::Arc::clone(&stop);
2219        std::thread::spawn(move || {
2220            let Some(bus) = stand_in_bus() else {
2221                let _ = ready.send(false);
2222                return;
2223            };
2224            let mut next = 20u32;
2225            bus.start_receive(
2226                MatchRule::new_method_call(),
2227                Box::new(move |call: dbus::Message, bus: &LocalConnection| {
2228                    let mut press = None;
2229                    let reply = match call.member().as_deref() {
2230                        Some("GetCapabilities") => call.method_return().append1(vec!["actions", "body", "body-markup"]),
2231                        Some("Notify") => {
2232                            let (app, replaces, icon, title, body, buttons, hints, timeout): (String, u32, String, String, String, Vec<String>, dbus::arg::PropMap, i32) = call.read_all().unwrap();
2233                            next += 1;
2234                            // The last button, pressed once shown.
2235                            if buttons.len() >= 2 {
2236                                press = Some(buttons[buttons.len() - 2].clone());
2237                            }
2238                            let _ = told.send(Sent {
2239                                app,
2240                                replaces,
2241                                icon,
2242                                title,
2243                                body,
2244                                buttons,
2245                                hints: hint_names(&call),
2246                                sound_off: dbus::arg::prop_cast::<bool>(&hints, "suppress-sound").copied().unwrap_or(false),
2247                                entry: dbus::arg::prop_cast::<String>(&hints, "desktop-entry").cloned().unwrap_or_default(),
2248                                urgency: dbus::arg::prop_cast::<u8>(&hints, "urgency").copied().unwrap_or(9),
2249                                timeout,
2250                            });
2251                            call.method_return().append1(next)
2252                        }
2253                        _ => return true,
2254                    };
2255                    let _ = bus.send(reply);
2256                    if let Some(key) = press {
2257                        let mut signal = dbus::Message::new_signal(SERVER_PATH, SERVER, "ActionInvoked").unwrap().append2(next, key);
2258                        // To the connection that showed it alone, as Plasma does.
2259                        signal.set_destination(call.sender().map(|s| s.into_static()));
2260                        let _ = bus.send(signal);
2261                    }
2262                    true
2263                }),
2264            );
2265            let _ = ready.send(true);
2266            let until = std::time::Instant::now() + Duration::from_secs(30);
2267            while !stopped.load(std::sync::atomic::Ordering::Relaxed) && std::time::Instant::now() < until {
2268                let _ = bus.process(Duration::from_millis(100));
2269            }
2270        });
2271        if !started.recv_timeout(Duration::from_secs(5)).unwrap_or(false) {
2272            eprintln!("No bus of the test's own (dbus-run-session), or a notification server on it already: skipped.");
2273            return;
2274        }
2275        let wait = Duration::from_secs(5);
2276        let as_before = |icon: &str, title: &str, body: &str, buttons: &[&str], urgency: u8| Sent {
2277            app: "Sioul".into(),
2278            replaces: 0,
2279            icon: icon.into(),
2280            title: title.into(),
2281            body: body.into(),
2282            buttons: buttons.iter().map(|b| b.to_string()).collect(),
2283            hints: vec!["desktop-entry".into(), "suppress-sound".into(), "urgency".into()],
2284            sound_off: true,
2285            entry: DESKTOP_ENTRY.into(),
2286            urgency,
2287            timeout: 600_000,
2288        };
2289        // A code with its button: pressed at the server, copied here.
2290        let (copied, copies) = channel::<()>();
2291        code("Code from example.org", "482913 <b>now</b> & then", Some(("Copy".into(), Box::new(move || {
2292            let _ = copied.send(());
2293        })))).unwrap();
2294        assert_eq!(sent.recv_timeout(wait).unwrap(), as_before("dialog-password", "Code from example.org", "482913 &lt;b&gt;now&lt;/b&gt; &amp; then", &["copy", "Copy"], CRITICAL));
2295        assert_eq!(copies.recv_timeout(wait), Ok(()));
2296        // Mail: normal urgency, said once; critical when someone Always through is in it.
2297        mail("New mail", "From Noa", false, None).unwrap();
2298        assert_eq!(sent.recv_timeout(wait).unwrap(), as_before("mail-unread", "New mail", "From Noa", &[], NORMAL));
2299        mail("New mail", "From Noa", true, None).unwrap();
2300        assert_eq!(sent.recv_timeout(wait).unwrap(), as_before("mail-unread", "New mail", "From Noa", &[], CRITICAL));
2301        // A reminder; and one with choices, the one pressed run alone.
2302        remind("Magnesium", "300 mg", None).unwrap();
2303        assert_eq!(sent.recv_timeout(wait).unwrap(), as_before("appointment-soon", "Magnesium", "300 mg", &[], CRITICAL));
2304        let (chose, choices) = channel::<&str>();
2305        let other = chose.clone();
2306        remind_choices("The night", "Close the day?", vec![("Close the day".into(), Box::new(move || {
2307                let _ = chose.send("close");
2308            })), ("Options…".into(), Box::new(move || {
2309                let _ = other.send("options");
2310            }))]).unwrap();
2311        assert_eq!(sent.recv_timeout(wait).unwrap(), as_before("appointment-soon", "The night", "Close the day?", &["choice-0", "Close the day", "choice-1", "Options…"], CRITICAL));
2312        assert_eq!(choices.recv_timeout(wait), Ok("options"));
2313        assert!(choices.recv_timeout(Duration::from_millis(300)).is_err(), "one choice run");
2314        stop.store(true, std::sync::atomic::Ordering::Relaxed);
2315    }
2316
2317    /// The time running, against a notification server of the test's own on a
2318    /// bus of its own: run under dbus-run-session. Where a server is there
2319    /// already (the desktop's), skipped: nothing here may reach it.
2320    #[test]
2321    fn a_lasting_notification_changes_in_place_hears_its_buttons_and_goes() {
2322        use dbus::blocking::LocalConnection;
2323        use dbus::channel::{MatchingReceiver, Sender as _};
2324        use dbus::message::MatchRule;
2325        use std::sync::mpsc::channel;
2326
2327        /// What the stand-in server was asked.
2328        #[derive(Debug, PartialEq)]
2329        enum Asked {
2330            Notify { replaces: u32, title: String, body: String, actions: Vec<String>, resident: bool, urgency: u8, timeout: i32 },
2331            Close(u32),
2332        }
2333        let _alone = crate::bus::NOTIFICATION_SERVER.lock().unwrap_or_else(|e| e.into_inner());
2334        let (told, asked) = channel::<Asked>();
2335        let (ready, started) = channel::<bool>();
2336        std::thread::spawn(move || {
2337            // Never in a server's place: no replacing it, no waiting in line.
2338            let Some(bus) = stand_in_bus() else {
2339                let _ = ready.send(false);
2340                return;
2341            };
2342            let finished = std::rc::Rc::new(std::cell::Cell::new(false));
2343            let over = std::rc::Rc::clone(&finished);
2344            let mut notified = 0;
2345            bus.start_receive(
2346                MatchRule::new_method_call(),
2347                Box::new(move |call: dbus::Message, bus: &LocalConnection| {
2348                    let mut presses: Vec<(u32, &str)> = Vec::new();
2349                    let reply = match call.member().as_deref() {
2350                        Some("GetCapabilities") => call.method_return().append1(vec!["actions", "body", "body-markup", "persistence"]),
2351                        Some("Notify") => {
2352                            let (_, replaces, _, title, body, actions, hints, timeout): (String, u32, String, String, String, Vec<String>, dbus::arg::PropMap, i32) = call.read_all().unwrap();
2353                            let resident = dbus::arg::prop_cast::<bool>(&hints, "resident").copied().unwrap_or(false);
2354                            let urgency = dbus::arg::prop_cast::<u8>(&hints, "urgency").copied().unwrap_or(9);
2355                            let _ = told.send(Asked::Notify { replaces, title, body, actions, resident, urgency, timeout });
2356                            notified += 1;
2357                            // Pressed once shown; then another notification's button, and this one's.
2358                            presses = match notified {
2359                                1 => vec![(7, "pause")],
2360                                2 => vec![(8, "stop"), (7, "resume")],
2361                                _ => Vec::new(),
2362                            };
2363                            call.method_return().append1(7u32)
2364                        }
2365                        Some("CloseNotification") => {
2366                            let (id,): (u32,) = call.read_all().unwrap();
2367                            let _ = told.send(Asked::Close(id));
2368                            over.set(true);
2369                            call.method_return()
2370                        }
2371                        _ => return true,
2372                    };
2373                    let _ = bus.send(reply);
2374                    for (id, key) in presses {
2375                        let mut press = dbus::Message::new_signal(SERVER_PATH, SERVER, "ActionInvoked").unwrap().append2(id, key);
2376                        // To the connection that showed it alone, as Plasma does.
2377                        press.set_destination(call.sender().map(|s| s.into_static()));
2378                        let _ = bus.send(press);
2379                    }
2380                    true
2381                }),
2382            );
2383            let _ = ready.send(true);
2384            let until = std::time::Instant::now() + Duration::from_secs(20);
2385            while !finished.get() && std::time::Instant::now() < until {
2386                let _ = bus.process(Duration::from_millis(100));
2387            }
2388        });
2389        if !started.recv_timeout(Duration::from_secs(5)).unwrap_or(false) {
2390            eprintln!("No bus of the test's own (dbus-run-session), or a notification server on it already: skipped.");
2391            return;
2392        }
2393        let wait = Duration::from_secs(5);
2394        let buttons = |first: &str, label: &str| vec![(first.to_string(), label.to_string()), ("stop".to_string(), "Stop".to_string()), ("default".to_string(), "Open".to_string())];
2395        let (pressed, presses) = channel::<String>();
2396        let shown = ongoing("Write to the bank", "Since 14:02, for 25 min", buttons("pause", "Pause"), Box::new(move |key: &str| drop(pressed.send(key.to_string()))));
2397        let first = Asked::Notify {
2398            replaces: 0,
2399            title: "Write to the bank".into(),
2400            body: "Since 14:02, for 25 min".into(),
2401            actions: ["pause", "Pause", "stop", "Stop", "default", "Open"].map(String::from).to_vec(),
2402            resident: true,
2403            urgency: 1,
2404            // The server keeps it once its popup goes: its own time.
2405            timeout: -1,
2406        };
2407        assert_eq!(asked.recv_timeout(wait).unwrap(), first);
2408        // A button pressed at the server, heard here.
2409        assert_eq!(presses.recv_timeout(wait).unwrap(), "pause");
2410        // Changed in place; another notification's button is not this one's.
2411        shown.update("Write to the bank", "Paused, 3 min so far", buttons("resume", "Go on"));
2412        match asked.recv_timeout(wait).unwrap() {
2413            Asked::Notify { replaces, body, actions, .. } => assert_eq!((replaces, body.as_str(), actions[0].as_str()), (7, "Paused, 3 min so far", "resume")),
2414            other => panic!("{other:?}"),
2415        }
2416        assert_eq!(presses.recv_timeout(wait).unwrap(), "resume");
2417        // Taken away before Sioul goes on.
2418        shown.close(wait);
2419        assert_eq!(asked.recv_timeout(wait).unwrap(), Asked::Close(7));
2420        assert!(presses.try_recv().is_err(), "nothing else was pressed");
2421    }
2422}