Skip to main content

sioul_core/
everywhere.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! Do-not-disturb on every device (docs/do-not-disturb.md): why it holds now,
5//! the switch pressed on any device, and the people who may reach you then.
6//!
7//! - **The reasons**, each read from the one place that already says it, and
8//!   shared as it is: the pause and Free time (`quiet.toml`), the night and
9//!   naps (Health), a focus session (`time/running.toml`), and the switch
10//!   (`state/do-not-disturb.toml`, the only new record). Every device reads
11//!   the same files and comes to the same answer. The latest change wins: the
12//!   switch pressed off after a reason began holds that reason off until it
13//!   ends; a reason beginning after the press holds again.
14//! - **The switch**: each device writes its own table only, its last press
15//!   stamped with a hybrid clock (never before a press already known), so a
16//!   press made after seeing another always comes after it, whatever the
17//!   clocks. A press asks one of three states (`State`, the owner's decisions
18//!   6 to 12 of 18 October 2026): Reachable, As usual, Do not disturb. Two
19//!   presses that did not know of each other, less than a minute apart: the
20//!   quieter state wins (Do not disturb, then As usual, then Reachable); one
21//!   more press settles it. A Reachable press writes `on = false`, so that an
22//!   older Sioul reads do-not-disturb off.
23//! - **Reachable**: while it lasts, never past the night's start (waking, at
24//!   night: `reachable_end`), neither a focus session, nor the night, nor
25//!   Free time turns do-not-disturb on; at its end, what an As usual before
26//!   it held off stays off (`held_off`); a pause pressed after it, or that
27//!   knew of it, ends it, and it holds no pause off. A device gone from the
28//!   sharing reads as As usual (`Switch::counted`). Its press releases what the phone holds, as
29//!   "Let what your phone holds through" pressed on a computer does (a
30//!   device's `release` stamp, by device: `Switch::release_stamps`).
31//! - **The list**, "Who may reach you during do-not-disturb"
32//!   (`config/dnd-people.toml`): names, numbers and addresses, kept in the
33//!   sharing itself, since the address books of two devices may not be in step.
34//! - **Both ways**: each device's own do-not-disturb, turned on or off by
35//!   anything there and heard as it happens, presses the switch when it
36//!   disagrees with what holds there (`heard`); the press says where it came
37//!   from (`via`). A device whose switch "on" came from its own system alone
38//!   adds no mode of Sioul's over it (`held_by_system`), so that the end of
39//!   what turned it on stays visible.
40
41use crate::config::{config_dir, state_dir};
42use serde::{Deserialize, Serialize};
43use std::collections::BTreeMap;
44use std::path::{Path, PathBuf};
45
46/// The switch's file, in the state folder; shared with the settings ("state/do-not-disturb.toml").
47pub const SWITCH_FILE: &str = "do-not-disturb.toml";
48/// The list's file, in the configuration folder; shared with the senders ("config/dnd-people.toml").
49pub const PEOPLE_FILE: &str = "dnd-people.toml";
50/// Two presses on two devices closer than this (milliseconds), neither knowing
51/// of the other: "on" wins. Clocks of devices that set their time from the
52/// network differ by less than a second; this covers a clock left a little off.
53pub const MARGIN_MS: i64 = 60_000;
54/// A focus session holds do-not-disturb this long past the time chosen
55/// (seconds): a session forgotten never keeps you unreachable for long.
56pub const FOCUS_GRACE: i64 = 30 * 60;
57/// …and a session without a time chosen, this long (Sioul's open-ended ceiling).
58pub const FOCUS_OPEN: i64 = 180 * 60;
59/// Other devices heard from within this many days count for "on every device".
60pub const LIVE_DAYS: i64 = 7;
61/// Reachable never lasts more than this after it was pressed (seconds),
62/// whatever its press says: the longest any of its lengths takes, a press by
63/// day "until tonight" (review of part 3, F2).
64pub const REACHABLE_MOST: i64 = 24 * 3600;
65/// A press stamped further ahead of this device's clock than this
66/// (milliseconds) is not followed by its hybrid clock (`Switch::next_stamp`),
67/// and reads as not pressed yet (`read_switch`): a garbled stamp, or a clock a
68/// year ahead, never wins for ever (review of part 3, R-8). A clock a few
69/// hours wrong (a time zone set wrong) still works with the others.
70pub const CLOCK_AHEAD_MS: i64 = 24 * 3600 * 1000;
71/// A release stamped further ahead than this (milliseconds) is not followed
72/// by the hybrid clock (R-3): a release asks nothing of the presses' order.
73pub const RELEASE_FOLLOWED_MS: i64 = 3600 * 1000;
74/// What a device's Reachable press is asked as among the release stamps
75/// (`Switch::release_stamps`): its id and this, apart from its own release.
76pub const REACHABLE_RELEASE: &str = "#reachable";
77/// The devices gone from the sharing as this device last found them, beside
78/// the sharing's memory (`share/`): ids only, never shared (`gone_devices`).
79pub const GONE_FILE: &str = "gone-devices.json";
80/// A computer whose news is this old (seconds), its table saying it is
81/// silenced, is waiting for its news: its Sioul may have stopped without a
82/// word (a crash), and its system's do-not-disturb (Plasma's) ended with it. A
83/// running computer exchanges each minute and writes its news at least every
84/// fifteen minutes (sioul-sync's `write_seen`). A phone's modes stay while
85/// Sioul is closed: its table holds.
86pub const COMPUTER_QUIET: i64 = 30 * 60;
87
88fn yes() -> bool {
89    true
90}
91
92fn is_false(value: &bool) -> bool {
93    !*value
94}
95
96fn is_zero(value: &i64) -> bool {
97    *value == 0
98}
99
100/// One of the three states a press asks (the owner's decisions 6 to 12 of 18
101/// October 2026), from the least quiet: Reachable (« Joignable »), the people
102/// who write and call reach you at once; As usual (« Comme d'habitude »), the
103/// matrix by the time; Do not disturb (« Ne pas déranger »).
104#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
105#[serde(rename_all = "kebab-case")]
106pub enum State {
107    Reachable,
108    #[default]
109    Usual,
110    Dnd,
111}
112
113impl State {
114    pub const ALL: [State; 3] = [State::Reachable, State::Usual, State::Dnd];
115
116    pub fn id(self) -> &'static str {
117        match self {
118            State::Reachable => "reachable",
119            State::Usual => "usual",
120            State::Dnd => "dnd",
121        }
122    }
123
124    pub fn read(word: &str) -> Option<State> {
125        State::ALL.into_iter().find(|s| s.id() == word.trim())
126    }
127
128    /// The state an older press said by its `on` alone.
129    pub fn of(on: bool) -> State {
130        if on { State::Dnd } else { State::Usual }
131    }
132}
133
134/// A press heard from the device's own do-not-disturb, as `Device::via` and `Press::via` say it.
135pub const VIA_SYSTEM: &str = "system";
136
137/// What turns do-not-disturb on (`[dnd]` in the configuration, shared with the settings).
138#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
139pub struct DndSettings {
140    /// The switch in the status line.
141    #[serde(default = "yes")]
142    pub button: bool,
143    /// While a focus session counts.
144    #[serde(default)]
145    pub focus: bool,
146    /// During the pause and Free time: the system's do-not-disturb too, beside
147    /// Sioul's own notifications, which the pauses hold either way.
148    #[serde(default = "yes")]
149    pub pauses: bool,
150    /// While you sleep: the night from winding down, and naps.
151    #[serde(default)]
152    pub sleep: bool,
153    /// The people on the list got through (on a phone, as starred contacts);
154    /// otherwise nobody. Always through's do-not-disturb cells now
155    /// (`attention`), read from here once to seed them.
156    #[serde(default = "yes")]
157    pub people: bool,
158    /// Android: this device keeps in step in the background (its own, never shared).
159    #[serde(default = "yes")]
160    pub background: bool,
161}
162
163impl Default for DndSettings {
164    fn default() -> Self {
165        DndSettings { button: true, focus: false, pauses: true, sleep: false, people: true, background: true }
166    }
167}
168
169/// Why do-not-disturb holds, in the order the first one is said.
170#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
171#[serde(rename_all = "kebab-case")]
172pub enum Why {
173    Paused,
174    FreeTime,
175    Sleep,
176    Focus,
177    Manual,
178}
179
180impl Why {
181    pub fn id(self) -> &'static str {
182        match self {
183            Why::Paused => "paused",
184            Why::FreeTime => "free-time",
185            Why::Sleep => "sleep",
186            Why::Focus => "focus",
187            Why::Manual => "manual",
188        }
189    }
190
191    pub fn parse(id: &str) -> Option<Why> {
192        [Why::Paused, Why::FreeTime, Why::Sleep, Why::Focus, Why::Manual].into_iter().find(|w| w.id() == id)
193    }
194
195    /// Silenced by the switch's own mode (sleep, focus, the switch), not a pause's.
196    pub fn global(self) -> bool {
197        matches!(self, Why::Sleep | Why::Focus | Why::Manual)
198    }
199}
200
201// ---------------------------------------------------------------- the switch
202
203/// One device's table in the switch's file, written by that device alone.
204#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
205pub struct Device {
206    /// "phone" or "computer".
207    #[serde(default)]
208    pub kind: String,
209    /// A computer's host name; "" for a phone.
210    #[serde(default)]
211    pub name: String,
212    /// Its last press of the switch (milliseconds, hybrid clock); 0: never pressed there.
213    #[serde(default)]
214    pub pressed: i64,
215    /// What that press asked.
216    #[serde(default)]
217    pub on: bool,
218    /// The end chosen with it (milliseconds); 0: until turned off.
219    #[serde(default)]
220    pub until: i64,
221    /// The latest press of another device known here when pressing (milliseconds).
222    #[serde(default)]
223    pub seen: i64,
224    /// What holds here now, as this device reads the reasons ("" none).
225    #[serde(default)]
226    pub why: String,
227    /// Its system's do-not-disturb is on for it.
228    #[serde(default)]
229    pub silenced: bool,
230    /// Its system's answer, in its words (the do-not-disturb layer's report).
231    #[serde(default)]
232    pub line: String,
233    /// When this table was written (milliseconds).
234    #[serde(default)]
235    pub at: i64,
236    /// Where its last press came from: "" the switch (in the window, the
237    /// tile), `VIA_SYSTEM` the device's own do-not-disturb, heard as it changed.
238    #[serde(default, skip_serializing_if = "String::is_empty")]
239    pub via: String,
240    /// Do-not-disturb holds, but the person turned this device's own
241    /// do-not-disturb off, unheard, and Sioul leaves it off until it next turns
242    /// it on: `line` says so.
243    #[serde(default, skip_serializing_if = "is_false")]
244    pub turned_off: bool,
245    /// Do-not-disturb does not hold, but this device's system is still
246    /// silenced (Sioul could not turn it off, or it came on unheard): `line` says so.
247    #[serde(default, skip_serializing_if = "is_false")]
248    pub system_on: bool,
249    /// Sioul quit on this computer (`closing`): it follows nothing until it
250    /// starts again; `silenced` says what its system still holds without it.
251    #[serde(default, skip_serializing_if = "is_false")]
252    pub closed: bool,
253    /// The state its last press asked (`State`): "reachable", "usual",
254    /// "dnd"; none from an older Sioul, which said it by `on` alone, read so
255    /// too for a word this version does not know (`state`).
256    #[serde(default, skip_serializing_if = "String::is_empty")]
257    pub mode: String,
258    /// Reachable "until you turn it off": `until` is its latest end (the
259    /// night's start, or waking), said as such.
260    #[serde(default, skip_serializing_if = "is_false")]
261    pub open: bool,
262    /// "Let what your phone holds through", pressed on this device
263    /// (milliseconds, hybrid clock); 0 never: the phone lets through what it
264    /// held before it, once (`Switch::release_stamps`).
265    #[serde(default, skip_serializing_if = "is_zero")]
266    pub release: i64,
267    /// A Reachable press: the As usual (or the switch's off) it found holding
268    /// when pressed, by that press's stamp (milliseconds; 0 none): once
269    /// Reachable is over, a reason begun before it stays held off, as it was
270    /// under that As usual (review of part 3, F8). An older press has none.
271    #[serde(default, skip_serializing_if = "is_zero")]
272    pub held_off: i64,
273    /// Read so here only, never written: a device gone from the sharing
274    /// (`Switch::counted`), whose last press ends what came before it and
275    /// holds nothing.
276    #[serde(skip)]
277    pub gone: bool,
278    /// What a later version writes here, kept as it is: written back unchanged.
279    #[serde(flatten)]
280    pub other: toml::Table,
281}
282
283impl Device {
284    /// The state its last press asked: its `mode`; an older press's, or a
285    /// word this version does not know, by its `on`.
286    pub fn state(&self) -> State {
287        State::read(&self.mode).unwrap_or(State::of(self.on))
288    }
289
290    /// Sioul quits on this computer: its table as it then holds. Its system's
291    /// do-not-disturb ended with Sioul (Plasma's) unless it outlives it
292    /// (`still`: GNOME's switch, a Mac's Focus); nothing more is followed here
293    /// until Sioul starts again.
294    pub fn closing(&mut self, still: bool) {
295        self.closed = true;
296        self.silenced = still;
297        self.turned_off = false;
298        self.system_on = false;
299        self.line = String::new();
300    }
301}
302
303/// The switch's file: a table per device.
304#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
305pub struct Switch {
306    #[serde(default)]
307    pub device: BTreeMap<String, Device>,
308    /// What a later version writes here, kept as it is.
309    #[serde(flatten)]
310    pub other: toml::Table,
311}
312
313/// The press that holds, as every device reads the switch.
314#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct Press {
316    /// Milliseconds, the pressing device's hybrid clock.
317    pub at: i64,
318    pub on: bool,
319    /// Milliseconds; 0: until turned off.
320    pub until: i64,
321    /// The device it was pressed on.
322    pub from: String,
323    /// Where it came from on that device: "" the switch, `VIA_SYSTEM` its own do-not-disturb.
324    pub via: String,
325    /// The state it asked; `on` is Do not disturb's.
326    pub state: State,
327    /// Reachable until you turn it off: `until` is its latest end.
328    pub open: bool,
329    /// Reachable's: the As usual it found holding (`Device::held_off`).
330    pub held_off: i64,
331    /// A device gone from the sharing pressed it: read as As usual, it ends
332    /// what came before it, and holds off, releases and brings back nothing.
333    pub gone: bool,
334}
335
336impl Switch {
337    pub fn default_path() -> PathBuf {
338        state_dir().join(SWITCH_FILE)
339    }
340
341    /// The file as it is; none there is no press anywhere yet. One that does
342    /// not read is an error: never taken for no device at all.
343    pub fn read(path: &Path) -> Result<Switch, String> {
344        match std::fs::read_to_string(path) {
345            Ok(text) => toml::from_str(&text).map_err(|e| format!("{}: {e}", path.display())),
346            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Switch::default()),
347            Err(e) => Err(format!("{}: {e}", path.display())),
348        }
349    }
350
351    /// For a question (the status line, an apply): what cannot be read is no press.
352    pub fn load(path: &Path) -> Switch {
353        Switch::read(path).unwrap_or_default()
354    }
355
356    /// The latest press anywhere wins; one that did not know of a quieter
357    /// press made on another device less than `MARGIN_MS` before it gives way
358    /// to it: the quietest of those (Do not disturb, then As usual, then
359    /// Reachable), as "on" won before there were three states.
360    pub fn latest(&self) -> Option<Press> {
361        let presses: Vec<(&String, &Device)> = self.device.iter().filter(|(_, d)| d.pressed > 0).collect();
362        let (from, last) = presses.iter().max_by_key(|(id, d)| (d.pressed, (*id).clone()))?;
363        let mut winner = (*from, *last);
364        // A quieter press this one did not know of, close before it: a tie, the quieter wins;
365        // never one that a later press knew of and overrode (review of part 3, L1).
366        let overridden = |d: &Device| presses.iter().any(|(_, later)| later.pressed > d.pressed && later.seen >= d.pressed);
367        let tied = presses
368            .iter()
369            .filter(|(id, d)| *id != *from && d.state() > last.state() && last.pressed.saturating_sub(d.pressed) <= MARGIN_MS && last.seen < d.pressed && !overridden(d))
370            .max_by_key(|(id, d)| (d.state(), d.pressed, (*id).clone()));
371        if let Some((id, d)) = tied {
372            winner = (*id, *d);
373        }
374        let d = winner.1;
375        let state = d.state();
376        let reachable = state == State::Reachable;
377        Some(Press { at: d.pressed, on: state == State::Dnd, until: d.until, from: winner.0.clone(), via: d.via.clone(), state, open: reachable && d.open, held_off: if reachable { d.held_off } else { 0 }, gone: d.gone })
378    }
379
380    /// The switch as this device reads it, the devices that count (`counts`,
381    /// by id; this device's always) as they are; a device gone from the
382    /// sharing (it left, you forgot it, another replaces it) read as an As
383    /// usual at its last press, and its Expecting a call as "Screen calls
384    /// again" at its own, nothing more: it still ends what came
385    /// before it, so that a do-not-disturb it ended never comes back (review
386    /// of part 3, F1), and it holds off, releases and brings back nothing,
387    /// nor is it a device that follows (no table time).
388    pub fn counted(&self, here: &str, counts: &dyn Fn(&str) -> bool) -> Switch {
389        let device = self
390            .device
391            .iter()
392            .map(|(id, d)| {
393                if id.as_str() == here || counts(id) {
394                    (id.clone(), d.clone())
395                } else {
396                    let mut read = Device { pressed: d.pressed, seen: d.seen, mode: State::Usual.id().to_string(), gone: true, ..Device::default() };
397                    // Its Expecting a call too: "Screen calls again" at its stamp, which ends what
398                    // came before it and opens nothing; nor does it screen calls (review of part 3, R-1).
399                    let calls = crate::calls::part_of(d);
400                    if calls.pressed > 0 {
401                        crate::calls::set_part(&mut read, crate::calls::DevicePart { pressed: calls.pressed, ..crate::calls::DevicePart::default() });
402                    }
403                    (id.clone(), read)
404                }
405            })
406            .collect();
407        Switch { device, other: self.other.clone() }
408    }
409
410    /// What the phone is asked to let through, held before then, by the
411    /// device that asks (milliseconds, that device's hybrid clock): each
412    /// device's "Let what your phone holds through", and the Reachable press
413    /// that is the press that holds. The phone applies each device's stamps
414    /// once, apart: a stamp from one device, however far ahead, never stands
415    /// in the way of another's (review of part 3, F3).
416    pub fn release_stamps(&self) -> Vec<(String, i64)> {
417        let mut out: Vec<(String, i64)> = self.device.iter().filter(|(_, d)| d.release > 0 && !d.gone).map(|(id, d)| (id.clone(), d.release)).collect();
418        // Its Reachable press apart from its release, so that neither masks the other (check of a23cf0c, C-1).
419        if let Some(p) = self.latest().filter(|p| p.state == State::Reachable && !p.gone) {
420            out.push((format!("{}{REACHABLE_RELEASE}", p.from), p.at));
421        }
422        out.sort();
423        out
424    }
425
426    /// The stamp of a press or a release made here at `now_ms`: after every
427    /// press known within `CLOCK_AHEAD_MS` of this clock and every release
428    /// known within `RELEASE_FOLLOWED_MS` (a hybrid clock): a release from a
429    /// clock far ahead never drags the presses along, which would then hold
430    /// off for hours what begins meanwhile (review of part 3, F3 and R-3); a
431    /// press a day ahead or more (a clock gone wrong, a garbled table,
432    /// `i64::MAX`) is not followed either. Never overflows.
433    pub fn next_stamp(&self, now_ms: i64) -> i64 {
434        let (presses, releases) = (now_ms.saturating_add(CLOCK_AHEAD_MS), now_ms.saturating_add(RELEASE_FOLLOWED_MS));
435        let pressed = self.device.values().map(|d| d.pressed).filter(|stamp| *stamp <= presses);
436        let released = self.device.values().map(|d| d.release).filter(|stamp| *stamp <= releases);
437        let known = pressed.chain(released).max().unwrap_or(0);
438        now_ms.max(known.saturating_add(1))
439    }
440
441    /// "Let what your phone holds through", pressed here: a stamp after every
442    /// press and release known (a hybrid clock). Returns it.
443    pub fn release(&mut self, here: &str, now_ms: i64) -> i64 {
444        let stamp = self.next_stamp(now_ms);
445        let own = self.device.entry(here.to_string()).or_default();
446        own.release = stamp;
447        own.at = own.at.max(stamp);
448        stamp
449    }
450
451    /// The latest press known here, of any device but `except` (milliseconds).
452    pub fn latest_known(&self, except: Option<&str>) -> i64 {
453        self.device.iter().filter(|(id, _)| Some(id.as_str()) != except).map(|(_, d)| d.pressed).max().unwrap_or(0)
454    }
455
456    /// A press here, stamped after every press known (a hybrid clock): `until`
457    /// in milliseconds, 0 for none. Returns its stamp.
458    pub fn press(&mut self, here: &str, on: bool, until: i64, now_ms: i64) -> i64 {
459        self.press_via(here, on, until, now_ms, "")
460    }
461
462    /// A press here, as `press`, saying where it came from (`VIA_SYSTEM`: this
463    /// device's own do-not-disturb, heard as it changed; "": the switch).
464    pub fn press_via(&mut self, here: &str, on: bool, until: i64, now_ms: i64, via: &str) -> i64 {
465        self.press_state(here, State::of(on), until, false, now_ms, via)
466    }
467
468    /// A press of one of the three states here, as `press_via`: `until` its
469    /// end (milliseconds; 0 none, never for As usual); `open` for Reachable
470    /// "until you turn it off", `until` then its latest end. A Reachable press
471    /// writes `on = false`: an older Sioul reads do-not-disturb off.
472    pub fn press_state(&mut self, here: &str, state: State, until: i64, open: bool, now_ms: i64, via: &str) -> i64 {
473        // After every press and every release known, so that a release seen never shadows this press (F3).
474        let stamp = self.next_stamp(now_ms);
475        let seen = self.latest_known(Some(here));
476        // Reachable carries the As usual it found holding, or the one the Reachable it follows carried (F8);
477        // the caller may read it from the readers' reading instead (`held_off_of`, R-5).
478        let held_off = held_off_of(self.latest().as_ref(), state);
479        let own = self.device.entry(here.to_string()).or_default();
480        own.held_off = held_off;
481        own.pressed = stamp;
482        own.on = state == State::Dnd;
483        own.mode = state.id().to_string();
484        own.until = if state == State::Usual { 0 } else { until.max(0) };
485        own.open = state == State::Reachable && open;
486        own.seen = seen;
487        own.at = stamp;
488        own.via = via.to_string();
489        stamp
490    }
491
492    /// Written whole, beside it then put in place: never half a file.
493    pub fn save(&self, path: &Path) -> Result<(), String> {
494        write_whole(path, &toml::to_string(self).map_err(|e| e.to_string())?)
495    }
496}
497
498/// What a press of `state` carries of the press that held before it
499/// (`Device::held_off`): for Reachable, the As usual it found (its stamp), or
500/// the one the Reachable it follows carried; else nothing. A gone device's
501/// As usual holds nothing off, so it gives nothing (review of part 3, R-5).
502pub fn held_off_of(before: Option<&Press>, state: State) -> i64 {
503    match before {
504        Some(p) if state == State::Reachable && !p.gone => match p.state {
505            State::Usual => p.at,
506            State::Reachable => p.held_off,
507            State::Dnd => 0,
508        },
509        _ => 0,
510    }
511}
512
513/// This device's own table changed under the file's lock, the file read
514/// again first: the other devices' tables, brought by the sharing meanwhile,
515/// stay as they are. `change` says whether it changed anything; nothing is
516/// written otherwise. A file that does not read is left alone, an error.
517pub fn change_own(path: &Path, here: &str, change: impl FnOnce(&mut Switch) -> bool) -> Result<Switch, String> {
518    crate::filelock::with_lock(path, || {
519        let mut switch = Switch::read(path)?;
520        if !here.is_empty() && change(&mut switch) {
521            // Only this device's table may have changed: the others' are never written from here.
522            switch.save(path)?;
523        }
524        Ok(switch)
525    })
526}
527
528/// A file written beside its place under a hidden name, then renamed.
529fn write_whole(path: &Path, text: &str) -> Result<(), String> {
530    let fail = |e: std::io::Error| format!("{}: {e}", path.display());
531    if let Some(parent) = path.parent() {
532        std::fs::create_dir_all(parent).map_err(fail)?;
533    }
534    let name = path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
535    let fresh = path.with_file_name(format!(".{name}.{}.new", std::process::id()));
536    std::fs::write(&fresh, text).map_err(fail)?;
537    std::fs::rename(&fresh, path).map_err(|e| {
538        let _ = std::fs::remove_file(&fresh);
539        fail(e)
540    })
541}
542
543// ---------------------------------------------------------------- the reasons
544
545/// What each reason's source says now (Unix seconds).
546#[derive(Debug, Clone, Default, PartialEq, Eq)]
547pub struct Sources {
548    /// The pause, since.
549    pub paused: Option<i64>,
550    /// Free time: since, and when it ends by itself (the night).
551    pub free: Option<(i64, i64)>,
552    /// Asleep, the night from winding down or a nap: since, until waking.
553    pub sleep: Option<(i64, i64)>,
554    /// A focus session counting: its start, and when it stops holding do-not-disturb (`focus`).
555    pub focus: Option<(i64, i64)>,
556    /// When the pause was last pressed, on or over (Unix seconds): a pause
557    /// pressed after Reachable ends it.
558    pub pause_pressed: Option<i64>,
559    /// The latest press of the switch the device that pressed the pause knew
560    /// of then (milliseconds, `Overrides::pause_saw`): a pause that knew of
561    /// Reachable ends it, whatever its clock says (review of part 3, L2).
562    pub pause_saw: Option<i64>,
563    /// The nights as Health keeps them around now, from winding down to
564    /// waking (Unix seconds), naps aside: Reachable's latest end (`reachable_end`).
565    pub nights: Vec<(i64, i64)>,
566}
567
568/// What the reasons' sources say at `now`, from the pauses' overrides and
569/// Health's blocks: every reader of the switch builds them so (the window,
570/// the listener, the calls' table, `sioul remind --watch`, `sioul watch`,
571/// the MCP), a pause ending Reachable in all of them (review of part 3, F7).
572pub fn sources_at(overrides: &crate::quiet::Overrides, blocks: &crate::quiet::Blocks, now: &jiff::Zoned) -> Sources {
573    let stamp = now.timestamp().as_second();
574    let free = overrides.free_from().map(|since| (since, crate::pause::free_until(since, blocks, now.time_zone())));
575    let sleep = blocks.at(stamp, &["sleep", "nap"]).map(|k| (k.start, k.end));
576    let focus = crate::timelog::running().and_then(|r| focus(&r, stamp));
577    let nights = blocks.kept.iter().filter(|k| k.kind == "sleep").map(|k| (k.start, k.end)).collect();
578    Sources { paused: overrides.paused_from(), free, sleep, focus, pause_pressed: overrides.paused_since, pause_saw: overrides.pause_saw, nights }
579}
580
581/// The same from the files alone, Health's blocks without the day's events:
582/// for what runs without the window (`attention::dnd_from_files`, the MCP).
583pub fn sources_from_files(now: &jiff::Zoned) -> Sources {
584    sources_at(&crate::quiet::Overrides::load(&crate::quiet::Overrides::default_path()), &crate::quiet::Blocks::read(now, &[]), now)
585}
586
587/// The devices gone from the sharing by a sealed fact (they left, you forgot
588/// them, another replaces them), as this device's window last found them:
589/// beside the sharing's memory, ids only, never shared.
590pub fn gone_path() -> PathBuf {
591    state_dir().join("share").join(GONE_FILE)
592}
593
594pub fn gone_devices() -> std::collections::BTreeSet<String> {
595    std::fs::read(gone_path()).ok().and_then(|bytes| serde_json::from_slice(&bytes).ok()).unwrap_or_default()
596}
597
598/// The switch as every reader on this device reads it: the file, the devices
599/// gone from the sharing read as `Switch::counted` says (`here`: this
600/// device's id, kept as it is; "" where it is not known, the gone being
601/// others always). The window, the listener, the calls' table, `sioul
602/// remind --watch`, `sioul watch` and the MCP read it so (F1, F7).
603pub fn read_switch(here: &str) -> Switch {
604    let gone = gone_devices();
605    Switch::load(&Switch::default_path()).counted(here, &|id| !gone.contains(id)).as_of(now_ms())
606}
607
608fn now_ms() -> i64 {
609    std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| i64::try_from(d.as_millis()).unwrap_or(i64::MAX))
610}
611
612impl Switch {
613    /// The switch as read at `now_ms` on this device: a press stamped more
614    /// than `CLOCK_AHEAD_MS` ahead of this clock (a garbled stamp, a clock a
615    /// year ahead) reads as not pressed yet, so that it never wins for ever
616    /// (review of part 3, R-8); its Expecting a call too.
617    pub fn as_of(mut self, now_ms: i64) -> Switch {
618        let bound = now_ms.saturating_add(CLOCK_AHEAD_MS);
619        for d in self.device.values_mut() {
620            if d.pressed > bound {
621                d.pressed = 0;
622            }
623            let mut calls = crate::calls::part_of(d);
624            if calls.pressed > bound {
625                calls.pressed = 0;
626                calls.on = false;
627                crate::calls::set_part(d, calls);
628            }
629        }
630        self
631    }
632}
633
634/// A focus session as do-not-disturb reads it: counting (not paused), from
635/// its start until 30 minutes past the time chosen (three hours without one);
636/// none otherwise.
637pub fn focus(running: &crate::timelog::Running, now: i64) -> Option<(i64, i64)> {
638    if running.paused_at.is_some() || running.start <= 0 {
639        return None;
640    }
641    let lasts = if running.planned > 0 { i64::from(running.planned) * 60 + FOCUS_GRACE } else { FOCUS_OPEN };
642    let end = running.start + running.paused.max(0) + lasts;
643    (now < end).then_some((running.start, end))
644}
645
646/// A reason that holds now.
647#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
648pub struct Held {
649    pub why: Why,
650    /// Unix seconds.
651    pub since: i64,
652    /// When it ends by itself (Unix seconds): the switch's end, waking, Free
653    /// time's end, a focus session's limit; none for the pause and an open switch.
654    pub until: Option<i64>,
655    /// The device the switch was pressed on; "" for the other reasons.
656    pub from: String,
657}
658
659/// Why do-not-disturb holds now: every reason that does, the one said first
660/// first; and Reachable, while it lasts.
661#[derive(Debug, Clone, Default, PartialEq, Eq)]
662pub struct Now {
663    pub holds: Vec<Held>,
664    /// Reachable now: since and until when it lasts (Unix seconds), from
665    /// which device, "until you turn it off" or not.
666    pub reachable: Option<Reach>,
667}
668
669/// Reachable while it lasts (`Now::reachable`).
670#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
671pub struct Reach {
672    /// Unix seconds.
673    pub since: i64,
674    /// When it ends by itself (Unix seconds); none: until turned off (an older device's press only).
675    pub until: Option<i64>,
676    /// The device it was pressed on.
677    pub from: String,
678    /// "Until you turn it off", ending at the latest at `until`.
679    pub open: bool,
680}
681
682impl Now {
683    pub fn on(&self) -> bool {
684        !self.holds.is_empty()
685    }
686
687    /// The reason said: the first that holds.
688    pub fn why(&self) -> Option<Why> {
689        self.holds.first().map(|h| h.why)
690    }
691
692    pub fn has(&self, why: Why) -> bool {
693        self.holds.iter().any(|h| h.why == why)
694    }
695
696    /// Whether the switch's own mode holds (sleep, focus, the switch), beside the pauses'.
697    pub fn global(&self) -> bool {
698        self.holds.iter().any(|h| h.why.global())
699    }
700
701    /// The end said: when every reason that holds ends by itself, the last of
702    /// them; a focus session's limit is never said (it is no promise).
703    pub fn until(&self) -> Option<i64> {
704        if self.holds.is_empty() || self.holds.iter().any(|h| h.until.is_none() || h.why == Why::Focus) {
705            return None;
706        }
707        self.holds.iter().filter_map(|h| h.until).max()
708    }
709
710    /// The next time a reason that holds ends by itself (Unix seconds), to look again then.
711    pub fn next_end(&self) -> Option<i64> {
712        self.holds.iter().filter_map(|h| h.until).min()
713    }
714
715    /// The first reason that holds and is not a pause's (sleep, focus, the switch).
716    pub fn global_why(&self) -> Option<Why> {
717        self.holds.iter().map(|h| h.why).find(|w| w.global())
718    }
719
720    /// Whether Sioul's own notifications wait and mail comes only from the
721    /// list: the switch or a focus session (sleep and the pauses keep their own
722    /// rules, under which nothing is told).
723    pub fn gates(&self) -> bool {
724        self.has(Why::Focus) || self.has(Why::Manual)
725    }
726}
727
728/// A change of this device's own do-not-disturb, heard as it happened, against
729/// what holds here (`holds`: do-not-disturb on, for any reason): the press it
730/// is, `Some(on)`, when the two disagree; none when they agree, which is what
731/// Sioul's own changes look like when they come back (docs/do-not-disturb.md,
732/// "Both ways"). A change found later, not heard as it happened, is never a
733/// press: its time is unknown, and another device may have pressed since.
734pub fn heard(holds: bool, system_on: bool) -> Option<bool> {
735    (holds != system_on).then_some(system_on)
736}
737
738/// Whether the switch's mode is held on this device by its own system alone:
739/// the switch's "on", taken from this device's own do-not-disturb (`here`,
740/// `VIA_SYSTEM`), is the only reason for that mode (the pause and Free time
741/// have modes of their own). Sioul then adds no mode of its own there: the
742/// system's cause silences the device already, and a mode of Sioul's would
743/// hide its end (Android shows an application only the effective state).
744pub fn held_by_system(now: &Now, press: Option<&Press>, here: &str) -> bool {
745    let mut global = now.holds.iter().filter(|h| h.why.global());
746    let only_manual = matches!((global.next(), global.next()), (Some(h), None) if h.why == Why::Manual);
747    only_manual && press.is_some_and(|p| p.on && p.from == here && p.via == VIA_SYSTEM)
748}
749
750/// Why do-not-disturb holds at `now` (Unix seconds), from the reasons'
751/// sources, the settings and the switch's latest press. A press "off" made
752/// after a reason began holds that reason off until it ends; the pause first,
753/// then Free time (never with the pause), sleep, focus, the switch.
754pub fn now(settings: &DndSettings, sources: &Sources, press: Option<&Press>, now: i64) -> Now {
755    // Reachable, while it lasts, never past its latest end (F2): never during a pause, and
756    // ended by a pause pressed after it, or that knew of it whatever its clock (L2).
757    let reachable = press.filter(|p| p.state == State::Reachable && !p.gone).and_then(|p| {
758        let end = reachable_end(p.at / 1000, (p.until > 0).then_some(p.until / 1000), &sources.nights);
759        let paused_after = sources.pause_pressed.is_some_and(|at| at.saturating_mul(1000) >= p.at) || sources.pause_saw.is_some_and(|saw| saw >= p.at);
760        (now < end && sources.paused.is_none() && !paused_after).then(|| Reach { since: p.at / 1000, until: Some(end), from: p.from.clone(), open: p.open })
761    });
762    // Pressed off after it began (As usual, or the switch's off): held off until it
763    // ends. A press "on" never holds a reason off; Reachable holds the times off while
764    // it lasts (below), then what the As usual it found held off (`held_off`, F8): what
765    // still holds and was not held off turns on again. A gone device's holds nothing.
766    let pressed_off_after = |since: i64| {
767        press.is_some_and(|p| {
768            let off = match p.state {
769                _ if p.gone => 0,
770                State::Reachable => p.held_off,
771                State::Usual => p.at,
772                State::Dnd => 0,
773            };
774            off > since.saturating_mul(1000)
775        })
776    };
777    let mut holds = Vec::new();
778    let mut add = |why: Why, since: i64, until: Option<i64>| {
779        // While Reachable lasts, neither Free time, nor the night, nor a focus session turns do-not-disturb on.
780        if reachable.is_some() && matches!(why, Why::FreeTime | Why::Sleep | Why::Focus) {
781            return;
782        }
783        if !pressed_off_after(since) {
784            holds.push(Held { why, since, until, from: String::new() });
785        }
786    };
787    if settings.pauses {
788        if let Some(since) = sources.paused {
789            add(Why::Paused, since, None);
790        } else if let Some((since, end)) = sources.free.filter(|(since, end)| *since <= now && now < *end) {
791            add(Why::FreeTime, since, Some(end));
792        }
793    }
794    if settings.sleep
795        && let Some((since, until)) = sources.sleep.filter(|(_, until)| now < *until)
796    {
797        add(Why::Sleep, since, Some(until));
798    }
799    if settings.focus
800        && let Some((since, end)) = sources.focus.filter(|(_, end)| now < *end)
801    {
802        add(Why::Focus, since, Some(end));
803    }
804    if let Some(p) = press.filter(|p| p.on) {
805        let until = (p.until > 0).then_some(p.until / 1000);
806        if until.is_none_or(|u| now < u) {
807            holds.push(Held { why: Why::Manual, since: p.at / 1000, until, from: p.from.clone() });
808        }
809    }
810    holds.sort_by_key(|h| h.why);
811    Now { holds, reachable }
812}
813
814/// The lengths Reachable is pressed for (decision 8 of 18 October 2026).
815#[derive(Debug, Clone, Copy, PartialEq, Eq)]
816pub enum Length {
817    /// Half an hour, an hour, two hours: minutes.
818    Minutes(i64),
819    /// Until the hours you are in end ("until 17:00").
820    HoursEnd,
821    /// Until tonight: the night's start.
822    Tonight,
823    /// Until you turn it off: by day, the night's start at the latest; at night, waking.
824    Off,
825}
826
827/// When Reachable pressed `now` for `length` ends (Unix seconds), and whether
828/// it is "until you turn it off": `hours_end` the end of the hours you are
829/// in, `night` the night's start (by day: the next one), `waking` the end of
830/// the night you are in (none by day). Pressed at night, "until tonight" and
831/// "until you turn it off" last until waking. None when it cannot end: no
832/// night known, or an end already past.
833pub fn reachable_until(length: Length, now: i64, hours_end: Option<i64>, night: Option<i64>, waking: Option<i64>) -> Option<(i64, bool)> {
834    // Its latest end, whatever its length (F2): the night's start by day, waking at night.
835    let latest = waking.or(night);
836    let end = match (length, waking) {
837        (Length::Minutes(minutes), _) => Some(now.saturating_add(minutes.max(1).saturating_mul(60))),
838        (Length::HoursEnd, _) => hours_end,
839        (Length::Tonight | Length::Off, Some(waking)) => Some(waking),
840        (Length::Tonight | Length::Off, None) => night,
841    }?;
842    let end = latest.map_or(end, |latest| end.min(latest));
843    (end > now).then_some((end, length == Length::Off))
844}
845
846/// When Reachable pressed at `at` ends at the latest (Unix seconds),
847/// whatever its press says, as every reader reads it (decision 8; review of
848/// part 3, F2): its own end (`until`; none: an older, a later or a garbled
849/// press), and never past the first night's start after it when pressed by
850/// day, the waking of the night it was pressed in when pressed at night
851/// (`nights`, as Health keeps them), nor `REACHABLE_MOST` after it.
852pub fn reachable_end(at: i64, until: Option<i64>, nights: &[(i64, i64)]) -> i64 {
853    let night = nights.iter().find(|(start, end)| *start <= at && at < *end).map(|(_, end)| *end).or_else(|| nights.iter().map(|(start, _)| *start).filter(|start| *start > at).min());
854    let most = at.saturating_add(REACHABLE_MOST);
855    [until, night].into_iter().flatten().fold(most, i64::min)
856}
857
858// ---------------------------------------------------------------- where it holds
859
860/// How another device follows.
861#[derive(Debug, Clone, PartialEq, Eq)]
862pub enum Follows {
863    /// Do-not-disturb holds there and its system is silenced.
864    Silenced,
865    /// It holds there, but its system could not be silenced: its own words.
866    Cannot(String),
867    /// Its news has not come since do-not-disturb began: on its way, at the
868    /// sharing's pace (about a minute with a Nextcloud server).
869    Behind,
870    /// It runs a Sioul that does not know do-not-disturb: no entry in the
871    /// devices' registry, no table: it cannot follow until it is updated.
872    Older,
873    /// Sioul is closed there (a computer, as it quit): it follows nothing
874    /// until it starts again.
875    Closed,
876}
877
878/// Each other device that counts (heard from in the last `LIVE_DAYS` days, by
879/// the sharing: `heard`, its id and when, Unix seconds), and how it follows
880/// while do-not-disturb holds here, since `since` (milliseconds): a table
881/// written before that (by more than `MARGIN_MS`, for clocks that disagree)
882/// says what held there before, not now. `registered` says whether a device
883/// has its entry in the devices' registry (a Sioul that knows do-not-disturb).
884/// A computer as Sioul quit there is closed; a computer said silenced but not
885/// heard from for `COMPUTER_QUIET` is waiting for its news (a crash says nothing).
886pub fn others(switch: &Switch, here: &str, heard: &[(String, i64)], now: i64, since: i64, registered: &dyn Fn(&str) -> bool) -> Vec<(String, Follows)> {
887    let live = |at: i64| now - at <= LIVE_DAYS * 86_400;
888    let mut ids: Vec<String> = heard.iter().filter(|(id, at)| id != here && live(*at)).map(|(id, _)| id.clone()).collect();
889    // A device whose table here is recent counts too, even if the sharing's notes lag.
890    ids.extend(switch.device.iter().filter(|(id, d)| *id != here && d.at > 0 && live(d.at / 1000)).map(|(id, _)| id.clone()));
891    ids.sort();
892    ids.dedup();
893    ids.into_iter()
894        .map(|id| {
895            // Its latest news: the sharing's note of it, or its table, whichever is later (seconds).
896            let news = heard.iter().filter(|(other, _)| *other == id).map(|(_, at)| *at).chain(switch.device.get(&id).map(|d| d.at / 1000)).max().unwrap_or(0);
897            let follows = match switch.device.get(&id) {
898                Some(d) if d.closed => Follows::Closed,
899                Some(d) if d.why.is_empty() || d.at.saturating_add(MARGIN_MS) < since => Follows::Behind,
900                Some(d) if d.silenced && d.kind == "computer" && now - news > COMPUTER_QUIET => Follows::Behind,
901                Some(d) if d.silenced => Follows::Silenced,
902                Some(d) => Follows::Cannot(d.line.clone()),
903                None if !registered(&id) => Follows::Older,
904                None => Follows::Behind,
905            };
906            (id, follows)
907        })
908        .collect()
909}
910
911/// What the status line and the details say of do-not-disturb.
912#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
913pub struct Said {
914    /// One sentence: "Do not disturb, on every device, until 15:00."; "" when off.
915    pub line: String,
916    /// Why, in a few words: "While you focus on a task."
917    pub why: String,
918    /// A line per device: this one first, then the others.
919    pub details: Vec<String>,
920    /// Every device that counts follows, and this one is silenced.
921    pub everywhere: bool,
922    /// This device's system is silenced.
923    pub here: bool,
924}
925
926/// This device, as `said` words it.
927#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
928pub struct Here<'a> {
929    /// Its system's do-not-disturb is on for Sioul's.
930    pub silenced: bool,
931    /// Its system's answer, in its words.
932    pub line: &'a str,
933    /// The person turned its own do-not-disturb off, unheard, and Sioul leaves
934    /// it off until it next turns it on.
935    pub turned_off: bool,
936}
937
938/// The words of do-not-disturb as it holds now: where (`here`, this device
939/// and its system's answer; `others`, as `others` says), until when (`until`,
940/// already worded: "15:00", "tomorrow at 07:00"), why. `name_of` names a
941/// device by its id, inside a sentence ("your phone", a computer's name,
942/// "another device").
943pub fn said(now_: &Now, here: Here<'_>, others: &[(String, Follows)], until: &str, name_of: &dyn Fn(&str) -> String, tr: &crate::i18n::Translator) -> Said {
944    let (here_silenced, here_line) = (here.silenced, here.line);
945    let Some(why) = now_.why() else { return Said::default() };
946    let mut args = crate::i18n::args();
947    args.set("until", until.to_string());
948    let with_until = |key: &str| if until.is_empty() { tr.text(key, None) } else { tr.text(&format!("{key}-until"), Some(&args)) };
949    let following = others.iter().filter(|(_, f)| *f == Follows::Silenced).count();
950    let everywhere = here_silenced && following == others.len();
951    // Every device not silenced yet is one whose news has not come: on its way,
952    // not left out. One that cannot be silenced, or an older Sioul, is said as such.
953    let not_following: Vec<&Follows> = others.iter().map(|(_, f)| f).filter(|f| **f != Follows::Silenced).collect();
954    let waiting = !not_following.is_empty() && not_following.iter().all(|f| **f == Follows::Behind);
955    let line = if here.turned_off {
956        with_until("dnd-turned-off-here")
957    } else if !here_silenced {
958        if following > 0 { with_until("dnd-elsewhere") } else { with_until("dnd-own-only") }
959    } else if others.is_empty() {
960        with_until("dnd-here-only")
961    } else if everywhere {
962        with_until("dnd-everywhere")
963    } else if waiting {
964        with_until("dnd-waiting")
965    } else if following == 0 {
966        with_until("dnd-here-only")
967    } else {
968        with_until("dnd-not-everywhere")
969    };
970    // "Here: silenced.", "On your phone: waiting for its news."
971    let device = |key: &str, place: String, line: Option<&str>| {
972        let mut args = crate::i18n::args();
973        args.set("device", place);
974        if let Some(line) = line {
975            args.set("line", line.to_string());
976        }
977        tr.text(key, Some(&args))
978    };
979    let on = |id: &str| {
980        let mut args = crate::i18n::args();
981        args.set("name", name_of(id));
982        tr.text("dnd-device-on", Some(&args))
983    };
984    let here_name = tr.text("dnd-device-here", None);
985    let mut details = vec![if here_silenced { device("dnd-device-silenced", here_name, None) } else { device("dnd-device-cannot", here_name, Some(here_line)) }];
986    for (id, follows) in others {
987        details.push(match follows {
988            Follows::Silenced => device("dnd-device-silenced", on(id), None),
989            Follows::Cannot(words) if !words.is_empty() => device("dnd-device-cannot", on(id), Some(words)),
990            Follows::Cannot(_) => device("dnd-device-unsilenced", on(id), None),
991            Follows::Behind => device("dnd-device-behind", on(id), None),
992            Follows::Older => device("dnd-device-older", on(id), None),
993            Follows::Closed => device("dnd-device-closed", on(id), None),
994        });
995    }
996    let why_words = match why {
997        Why::Manual => {
998            let from = now_.holds.iter().find(|h| h.why == Why::Manual).map(|h| h.from.clone()).unwrap_or_default();
999            if from.is_empty() {
1000                tr.text("dnd-why-manual", None)
1001            } else {
1002                let mut args = crate::i18n::args();
1003                args.set("device", name_of(&from));
1004                tr.text("dnd-why-manual-from", Some(&args))
1005            }
1006        }
1007        other => tr.text(&format!("dnd-why-{}", other.id()), None),
1008    };
1009    Said { line, why: why_words, details, everywhere, here: here_silenced }
1010}
1011
1012// ---------------------------------------------------------------- the list
1013
1014/// Someone who may reach you during do-not-disturb.
1015#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1016pub struct Person {
1017    /// Made once, the same on every device: one entry each in the sharing.
1018    pub id: String,
1019    #[serde(default)]
1020    pub name: String,
1021    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1022    pub phones: Vec<String>,
1023    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1024    pub emails: Vec<String>,
1025    /// The contact card they came from (its UID), to find it again.
1026    #[serde(default, skip_serializing_if = "String::is_empty")]
1027    pub contact: String,
1028    /// What a later version writes here, kept as it is: written back unchanged,
1029    /// so that this device never sends another version's person as changed.
1030    #[serde(flatten)]
1031    pub other: toml::Table,
1032}
1033
1034/// "Who may reach you during do-not-disturb".
1035#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1036pub struct People {
1037    #[serde(default, rename = "person", skip_serializing_if = "Vec::is_empty")]
1038    pub people: Vec<Person>,
1039    /// What a later version writes here, kept as it is.
1040    #[serde(flatten)]
1041    pub other: toml::Table,
1042}
1043
1044/// An address as compared: its address part ("Jane <jane@example.org>"), trimmed, in lower case.
1045fn address_key(text: &str) -> String {
1046    let text = text.trim();
1047    let inner = text.rsplit_once('<').map_or(text, |(_, rest)| rest.trim_end_matches('>'));
1048    inner.trim().trim_start_matches("mailto:").to_lowercase()
1049}
1050
1051/// A new id: the time and a hash keyed at random for this process.
1052pub fn new_id() -> String {
1053    use std::hash::{BuildHasher, Hasher};
1054    use std::sync::atomic::{AtomicU64, Ordering};
1055    static COUNT: AtomicU64 = AtomicU64::new(0);
1056    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
1057    let mut hasher = std::collections::hash_map::RandomState::new().build_hasher();
1058    hasher.write_u128(now.as_nanos());
1059    hasher.write_u64(COUNT.fetch_add(1, Ordering::Relaxed));
1060    hasher.write_u32(std::process::id());
1061    format!("{:x}{:016x}", now.as_secs(), hasher.finish())
1062}
1063
1064impl People {
1065    pub fn default_path() -> PathBuf {
1066        config_dir().join(PEOPLE_FILE)
1067    }
1068
1069    /// The list as it is; none there is nobody yet. One that does not read is
1070    /// an error, never an empty list (an edit would otherwise take everyone out
1071    /// on every device).
1072    pub fn read(path: &Path) -> Result<People, String> {
1073        match std::fs::read_to_string(path) {
1074            Ok(text) => toml::from_str(&text).map_err(|e| format!("{}: {e}", path.display())),
1075            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(People::default()),
1076            Err(e) => Err(format!("{}: {e}", path.display())),
1077        }
1078    }
1079
1080    /// For a question (a mail to tell, a number to look up): what cannot be read is nobody.
1081    pub fn load(path: &Path) -> People {
1082        People::read(path).unwrap_or_default()
1083    }
1084
1085    pub fn save(&self, path: &Path) -> Result<(), String> {
1086        write_whole(path, &toml::to_string(self).map_err(|e| e.to_string())?)
1087    }
1088
1089    /// Whether mail from this address may come during do-not-disturb.
1090    pub fn admits_address(&self, address: &str) -> bool {
1091        let key = address_key(address);
1092        !key.is_empty() && key.contains('@') && self.people.iter().any(|p| p.emails.iter().any(|e| address_key(e) == key))
1093    }
1094
1095    /// Whether this number is someone's on the list (numbers compared as
1096    /// `phones::key` reads them, `region` for those written without a country).
1097    pub fn admits_number(&self, number: &str, region: Option<&crate::phones::Region>) -> bool {
1098        let key = crate::phones::key(number, region);
1099        crate::phones::is_whole(&key) && self.people.iter().any(|p| p.phones.iter().any(|n| crate::phones::key(n, region) == key))
1100    }
1101
1102    /// A person added, or merged into the one already there with one of their
1103    /// addresses, numbers or their card: their id.
1104    pub fn add(&mut self, mut person: Person, region: Option<&crate::phones::Region>) -> String {
1105        person.emails.retain(|e| !address_key(e).is_empty());
1106        person.phones.retain(|n| !n.trim().is_empty());
1107        let same = |p: &Person| {
1108            (!person.contact.is_empty() && p.contact == person.contact)
1109                || person.emails.iter().any(|e| p.emails.iter().any(|x| address_key(x) == address_key(e)))
1110                || person.phones.iter().any(|n| {
1111                    let key = crate::phones::key(n, region);
1112                    crate::phones::is_whole(&key) && p.phones.iter().any(|x| crate::phones::key(x, region) == key)
1113                })
1114        };
1115        if let Some(known) = self.people.iter_mut().find(|p| same(p)) {
1116            for email in person.emails {
1117                if !known.emails.iter().any(|x| address_key(x) == address_key(&email)) {
1118                    known.emails.push(email.trim().to_string());
1119                }
1120            }
1121            for number in person.phones {
1122                let key = crate::phones::key(&number, region);
1123                if !known.phones.iter().any(|x| crate::phones::key(x, region) == key) {
1124                    known.phones.push(number.trim().to_string());
1125                }
1126            }
1127            if known.name.trim().is_empty() {
1128                known.name = person.name;
1129            }
1130            if known.contact.is_empty() {
1131                known.contact = person.contact;
1132            }
1133            return known.id.clone();
1134        }
1135        if person.id.is_empty() {
1136            person.id = new_id();
1137        }
1138        person.emails = person.emails.iter().map(|e| e.trim().to_string()).collect();
1139        person.phones = person.phones.iter().map(|n| n.trim().to_string()).collect();
1140        let id = person.id.clone();
1141        self.people.push(person);
1142        id
1143    }
1144
1145    /// Someone taken off the list; false when they were not on it.
1146    pub fn remove(&mut self, id: &str) -> bool {
1147        let before = self.people.len();
1148        self.people.retain(|p| p.id != id);
1149        self.people.len() != before
1150    }
1151}
1152
1153/// Someone blocked comes off Always through (docs/attention.md, Q3: blocked
1154/// beats Always through, and putting someone on one list takes them off the
1155/// other): their address and number taken off the people on the list, the
1156/// person of their card (`card`, its UID) taken off whole; a person left with
1157/// nothing to reach them by goes. Whether the list changed.
1158pub fn take_off(people: &mut People, addresses: &[String], numbers: &[String], card: &str, region: Option<&crate::phones::Region>) -> bool {
1159    let keys: Vec<String> = numbers.iter().map(|n| crate::phones::key(n, region)).filter(|k| crate::phones::is_whole(k)).collect();
1160    let card = card.trim();
1161    let mut gone: Vec<String> = Vec::new();
1162    let mut changed = false;
1163    for person in &mut people.people {
1164        let before = (person.emails.len(), person.phones.len());
1165        person.emails.retain(|e| !addresses.iter().any(|a| address_key(a) == address_key(e)));
1166        person.phones.retain(|n| !keys.contains(&crate::phones::key(n, region)));
1167        let whole = !card.is_empty() && person.contact == card;
1168        if whole || before != (person.emails.len(), person.phones.len()) {
1169            changed = true;
1170            if whole || (person.emails.is_empty() && person.phones.is_empty()) {
1171                gone.push(person.id.clone());
1172            }
1173        }
1174    }
1175    people.people.retain(|p| !gone.contains(&p.id));
1176    changed
1177}
1178
1179/// The list changed under its lock, read again first: what the sharing
1180/// brought meanwhile stays. A file that does not read is left alone, an error.
1181pub fn change_people(path: &Path, change: impl FnOnce(&mut People)) -> Result<People, String> {
1182    crate::filelock::with_lock(path, || {
1183        let mut people = People::read(path)?;
1184        let before = people.clone();
1185        change(&mut people);
1186        if people != before {
1187            people.save(path)?;
1188        }
1189        Ok(people)
1190    })
1191}
1192
1193/// Someone from a contact card: their name, numbers and addresses.
1194pub fn from_contact(contact: &crate::contacts::Contact) -> Person {
1195    Person {
1196        id: String::new(),
1197        name: contact.name.trim().to_string(),
1198        phones: contact.phones.iter().map(|p| p.value.trim().to_string()).filter(|v| !v.is_empty()).collect(),
1199        emails: contact.emails.iter().map(|e| e.value.trim().to_string()).filter(|v| !v.is_empty()).collect(),
1200        contact: contact.uid.clone(),
1201        other: toml::Table::new(),
1202    }
1203}
1204
1205/// People from the Safe list: each address, with its card's name and numbers
1206/// when a card has it; everyone whose card is in a category on it
1207/// (`category:Friends`); a card on it (`contact:<UID>`); a number on it
1208/// (`tel:+33…`). Patterns (`@example.org`, `*@example.org`, `tel:+3319900*`)
1209/// name nobody: counted, left.
1210pub fn from_safe(entries: &[String], contacts: &[crate::contacts::Contact]) -> (Vec<Person>, usize) {
1211    let (mut found, mut patterns) = (Vec::new(), 0);
1212    for entry in entries {
1213        let entry = entry.trim();
1214        if let Some(category) = crate::porch::category_of(entry) {
1215            found.extend(contacts.iter().filter(|c| crate::contacts::in_category(c, category)).map(from_contact));
1216        } else if let Some(uid) = crate::porch::card_of(entry) {
1217            found.extend(contacts.iter().filter(|c| c.uid.trim() == uid).map(from_contact));
1218        } else if let Some(number) = crate::porch::number_of(entry).filter(|n| !n.contains('*')) {
1219            found.push(Person { phones: vec![number.to_string()], ..Person::default() });
1220        } else if entry.starts_with('@') || !entry.contains('@') || entry.contains('*') {
1221            patterns += 1;
1222        } else {
1223            match crate::contacts::by_address(contacts, entry) {
1224                Some(card) => found.push(from_contact(card)),
1225                None => found.push(Person { name: String::new(), emails: vec![entry.to_string()], ..Person::default() }),
1226            }
1227        }
1228    }
1229    (found, patterns)
1230}
1231
1232#[cfg(test)]
1233mod tests {
1234    use super::*;
1235    use crate::i18n::Translator;
1236
1237    fn device(pressed: i64, on: bool, seen: i64) -> Device {
1238        Device { pressed, on, seen, ..Device::default() }
1239    }
1240
1241    /// This device as `said` reads it: silenced or not, with its system's words.
1242    fn at(silenced: bool, line: &str) -> Here<'_> {
1243        Here { silenced, line, turned_off: false }
1244    }
1245
1246    fn switch(devices: &[(&str, Device)]) -> Switch {
1247        Switch { device: devices.iter().map(|(id, d)| (id.to_string(), d.clone())).collect(), ..Switch::default() }
1248    }
1249
1250    #[test]
1251    fn the_latest_press_wins_whatever_device_made_it() {
1252        // The desktop turned it on, the phone off later having seen it: off.
1253        let s = switch(&[("desk", device(1_000_000, true, 0)), ("phone", device(1_200_000, false, 1_000_000))]);
1254        let latest = s.latest().unwrap();
1255        assert!(!latest.on && latest.from == "phone");
1256        // Then the desktop on again: on.
1257        let s = switch(&[("desk", device(1_300_000, true, 1_200_000)), ("phone", device(1_200_000, false, 1_000_000))]);
1258        assert!(s.latest().unwrap().on);
1259        // Nobody pressed: nothing.
1260        assert_eq!(Switch::default().latest(), None);
1261    }
1262
1263    #[test]
1264    fn a_press_after_seeing_another_comes_after_it_whatever_the_clocks() {
1265        let mut s = switch(&[("phone", device(5_000_000, true, 0))]);
1266        // This computer's clock is two minutes behind the phone's: its press
1267        // still comes after the one it saw.
1268        let stamp = s.press("desk", false, 0, 5_000_000 - 120_000);
1269        assert_eq!(stamp, 5_000_001);
1270        assert_eq!(s.device["desk"].seen, 5_000_000);
1271        let latest = s.latest().unwrap();
1272        assert!(!latest.on && latest.from == "desk");
1273    }
1274
1275    #[test]
1276    fn two_presses_that_did_not_see_each_other_within_a_minute_turn_it_on() {
1277        // On at the desktop, off on the phone 40 s later without having seen it: on.
1278        let s = switch(&[("desk", device(10_000_000, true, 0)), ("phone", device(10_040_000, false, 0))]);
1279        let latest = s.latest().unwrap();
1280        assert!(latest.on && latest.from == "desk");
1281        // Two minutes apart: the later one.
1282        let s = switch(&[("desk", device(10_000_000, true, 0)), ("phone", device(10_120_000, false, 0))]);
1283        assert!(!s.latest().unwrap().on);
1284        // Within the minute but seen: the later one.
1285        let s = switch(&[("desk", device(10_000_000, true, 0)), ("phone", device(10_040_000, false, 10_000_000))]);
1286        assert!(!s.latest().unwrap().on);
1287        // Off first, on after, within the minute: on, as the later.
1288        let s = switch(&[("desk", device(10_000_000, false, 0)), ("phone", device(10_040_000, true, 0))]);
1289        assert!(s.latest().unwrap().on);
1290    }
1291
1292    fn pressed(pressed: i64, state: State, seen: i64) -> Device {
1293        Device { pressed, on: state == State::Dnd, mode: state.id().into(), seen, ..Device::default() }
1294    }
1295
1296    fn reachable(at_s: i64, until_s: i64) -> Press {
1297        Press { at: at_s * 1000, on: false, until: until_s * 1000, from: "phone".into(), via: String::new(), state: State::Reachable, open: false, held_off: 0, gone: false }
1298    }
1299
1300    /// Part 3 of "What reaches you" (decisions 6 to 12): presses on two and
1301    /// three devices, in every order, within the minute's margin and not
1302    /// knowing each other: the quieter state wins (Do not disturb, then As
1303    /// usual, then Reachable); a minute apart, or seen, the later.
1304    #[test]
1305    fn the_quieter_of_three_states_wins_within_the_minute() {
1306        for a in State::ALL {
1307            for b in State::ALL {
1308                let s = switch(&[("desk", pressed(10_000_000, a, 0)), ("phone", pressed(10_030_000, b, 0))]);
1309                assert_eq!(s.latest().unwrap().state, a.max(b), "{a:?} then {b:?}, unseen");
1310                let seen = switch(&[("desk", pressed(10_000_000, a, 0)), ("phone", pressed(10_030_000, b, 10_000_000))]);
1311                assert_eq!(seen.latest().unwrap().state, b, "{a:?} then {b:?}, seen");
1312                let far = switch(&[("desk", pressed(10_000_000, a, 0)), ("phone", pressed(10_090_000, b, 0))]);
1313                assert_eq!(far.latest().unwrap().state, b, "{a:?} then {b:?}, past the minute");
1314                for c in State::ALL {
1315                    let three = switch(&[("desk", pressed(10_000_000, a, 0)), ("laptop", pressed(10_020_000, b, 0)), ("phone", pressed(10_040_000, c, 0))]);
1316                    let latest = three.latest().unwrap();
1317                    assert_eq!(latest.state, a.max(b).max(c), "{a:?}, {b:?}, {c:?}");
1318                    assert_eq!(latest.on, latest.state == State::Dnd);
1319                }
1320            }
1321        }
1322        // The winner's own end and device.
1323        let s = switch(&[("desk", Device { until: 12_000_000, ..pressed(10_000_000, State::Dnd, 0) }), ("phone", pressed(10_030_000, State::Reachable, 0))]);
1324        let latest = s.latest().unwrap();
1325        assert_eq!((latest.from.as_str(), latest.until, latest.state), ("desk", 12_000_000, State::Dnd));
1326    }
1327
1328    /// An older Sioul's press says its state by `on` alone; a word this
1329    /// version does not know reads so too. A Reachable press writes `on =
1330    /// false`, read as do-not-disturb off by an older Sioul; its words are
1331    /// written and read back, the rest kept.
1332    #[test]
1333    fn an_older_press_reads_by_its_on_and_reachable_writes_off() {
1334        let old: Switch = toml::from_str("[device.a]\npressed = 5\non = true\n[device.b]\npressed = 3\non = false\nmode = \"later-state\"\n").unwrap();
1335        assert_eq!((old.device["a"].state(), old.device["b"].state()), (State::Dnd, State::Usual));
1336        let mut s = Switch::default();
1337        s.press_state("phone", State::Reachable, 7_200_000, true, 3_600_000, "");
1338        let own = &s.device["phone"];
1339        assert_eq!((own.on, own.mode.as_str(), own.until, own.open), (false, "reachable", 7_200_000, true));
1340        let text = toml::to_string(&s).unwrap();
1341        assert!(text.contains("mode = \"reachable\"") && text.contains("open = true"), "{text}");
1342        let back: Switch = toml::from_str(&text).unwrap();
1343        assert_eq!((back.latest().unwrap().state, back.latest().unwrap().open), (State::Reachable, true));
1344        // As usual has no end; the plain switch writes its state too.
1345        s.press_state("phone", State::Usual, 9_000_000, true, 3_700_000, "");
1346        assert_eq!((s.device["phone"].until, s.device["phone"].open, s.device["phone"].mode.as_str()), (0, false, "usual"));
1347        s.press("desk", true, 0, 3_800_000);
1348        assert_eq!(s.device["desk"].mode, "dnd");
1349    }
1350
1351    /// Reachable while it lasts: neither the night, nor a focus session, nor
1352    /// Free time turns do-not-disturb on, before or during it; it holds no
1353    /// pause off and none holds during a pause; a pause pressed after it ends
1354    /// it for good; past its end, what began meanwhile holds.
1355    #[test]
1356    fn reachable_holds_off_the_times_and_a_pause_ends_it() {
1357        let settings = DndSettings { sleep: true, focus: true, pauses: true, ..DndSettings::default() };
1358        let press = reachable(1_000, 8_000);
1359        let before = Sources { sleep: Some((500, 9_000)), focus: Some((600, 9_000)), free: Some((700, 9_000)), ..Sources::default() };
1360        let held = now(&settings, &before, Some(&press), 2_000);
1361        assert!(!held.on() && held.reachable.as_ref().is_some_and(|r| r.since == 1_000 && r.until == Some(8_000)), "{held:?}");
1362        let during = Sources { sleep: Some((1_500, 9_000)), focus: Some((1_600, 9_000)), free: Some((1_700, 9_000)), ..Sources::default() };
1363        assert!(!now(&settings, &during, Some(&press), 2_000).on());
1364        // Past its end: what began meanwhile holds, and what began before it and still holds turns on again.
1365        let after = now(&settings, &during, Some(&press), 8_000);
1366        assert!(after.reachable.is_none() && after.has(Why::Sleep) && after.has(Why::Focus), "{after:?}");
1367        let again = now(&settings, &before, Some(&press), 8_000);
1368        assert!(again.has(Why::Sleep) && again.has(Why::Focus) && again.has(Why::FreeTime), "{again:?}");
1369        // A pause now: no Reachable, and the pause holds, though begun before the press.
1370        let paused = Sources { paused: Some(500), pause_pressed: Some(500), ..Sources::default() };
1371        let held = now(&settings, &paused, Some(&press), 2_000);
1372        assert!(held.reachable.is_none() && held.has(Why::Paused));
1373        // A pause pressed after it, over now: Reachable ended for good.
1374        let over = Sources { pause_pressed: Some(1_500), ..Sources::default() };
1375        assert!(now(&settings, &over, Some(&press), 2_000).reachable.is_none());
1376        // A pause before it, over: Reachable holds.
1377        let earlier = Sources { pause_pressed: Some(900), ..Sources::default() };
1378        assert!(now(&settings, &earlier, Some(&press), 2_000).reachable.is_some());
1379        // As usual pressed during the pause holds it off, as the switch's off does.
1380        let usual = Press { state: State::Usual, ..press.clone() };
1381        assert!(!now(&settings, &Sources { paused: Some(500), ..Sources::default() }, Some(&usual), 2_000).on());
1382        // The phone's own do-not-disturb heard on while Reachable: a press of Do not disturb; off: no press.
1383        assert_eq!((heard(false, true), heard(false, false)), (Some(true), None));
1384    }
1385
1386    /// Reachable's lengths (decision 8): half an hour to two hours; until
1387    /// the hours end; until tonight; until you turn it off, by day ending at
1388    /// the night's start at the latest, said so; pressed at night, until
1389    /// waking.
1390    #[test]
1391    fn reachable_ends_as_its_length_says() {
1392        let (now_s, hours_end, night, waking) = (36_000, 61_200, 81_000, 111_600);
1393        assert_eq!(reachable_until(Length::Minutes(30), now_s, Some(hours_end), Some(night), None), Some((now_s + 1_800, false)));
1394        assert_eq!(reachable_until(Length::Minutes(120), now_s, Some(hours_end), Some(night), None), Some((now_s + 7_200, false)));
1395        assert_eq!(reachable_until(Length::HoursEnd, now_s, Some(hours_end), Some(night), None), Some((hours_end, false)));
1396        assert_eq!(reachable_until(Length::Tonight, now_s, Some(hours_end), Some(night), None), Some((night, false)));
1397        assert_eq!(reachable_until(Length::Off, now_s, Some(hours_end), Some(night), None), Some((night, true)), "by day: at the latest the night's start");
1398        let asleep = 90_000;
1399        assert_eq!(reachable_until(Length::Off, asleep, None, Some(night + 86_400), Some(waking)), Some((waking, true)), "at night: until waking");
1400        assert_eq!(reachable_until(Length::Tonight, asleep, None, Some(night + 86_400), Some(waking)), Some((waking, false)));
1401        assert_eq!(reachable_until(Length::Off, now_s, None, None, None), None, "no night known: no end, not offered");
1402        assert_eq!(reachable_until(Length::HoursEnd, now_s, Some(now_s - 1), Some(night), None), None);
1403    }
1404
1405    /// The release asked of the phone, by device: a Reachable press while it
1406    /// is the one that holds, or "Let what your phone holds through" on a
1407    /// computer, stamped after every press and release known; a device gone
1408    /// from the sharing releases nothing.
1409    #[test]
1410    fn releases_are_asked_by_device_and_the_gone_release_nothing() {
1411        let mut s = switch(&[("phone", pressed(1_000_000, State::Usual, 0))]);
1412        assert!(s.release_stamps().is_empty());
1413        let stamp = s.release("desk", 900_000);
1414        assert_eq!((stamp, s.release_stamps()), (1_000_001, vec![("desk".to_string(), 1_000_001)]), "after every press known, whatever the clock");
1415        s.press_state("laptop", State::Reachable, 9_000_000, false, 2_000_000, "");
1416        assert_eq!(s.release_stamps(), vec![("desk".to_string(), 1_000_001), (format!("laptop{REACHABLE_RELEASE}"), 2_000_000)]);
1417        // A quieter press unseen within the minute holds: no release from the Reachable one.
1418        s.device.insert("phone".into(), pressed(1_970_000, State::Dnd, 0));
1419        assert_eq!((s.latest().unwrap().state, s.release_stamps()), (State::Dnd, vec![("desk".to_string(), 1_000_001)]));
1420        // The desk gone from the sharing: its release passed over.
1421        assert!(s.counted("phone", &|id| id != "desk").release_stamps().is_empty());
1422        assert!(s.counted("phone", &|_| false).device.contains_key("phone"), "this device always");
1423    }
1424
1425    /// Review of part 3, F1: the phone pressed Do not disturb until turned
1426    /// off three days ago; the old laptop pressed As usual two days ago,
1427    /// having seen it. The old laptop gone from the sharing (forgotten,
1428    /// replaced, left), its press still ends the phone's: As usual, read as
1429    /// such, never the Do not disturb nobody chose now. A gone device's
1430    /// Reachable or Do not disturb is As usual too, holds nothing off, and
1431    /// is no device that follows.
1432    #[test]
1433    fn a_gone_device_s_press_still_ends_what_came_before_it() {
1434        const DAY: i64 = 86_400_000;
1435        let t = 1_790_000_000_000;
1436        let settings = DndSettings { focus: true, ..DndSettings::default() };
1437        let s = switch(&[("phone", pressed(t - 3 * DAY, State::Dnd, 0)), ("old-laptop", pressed(t - 2 * DAY, State::Usual, t - 3 * DAY))]);
1438        let gone = s.counted("phone", &|id| id != "old-laptop");
1439        let latest = gone.latest().unwrap();
1440        assert_eq!((latest.from.as_str(), latest.state, latest.gone), ("old-laptop", State::Usual, true));
1441        assert!(!now(&settings, &Sources::default(), Some(&latest), t / 1000).on(), "no Do not disturb back");
1442        // Its last press was Reachable, or Do not disturb, a minute ago: As usual, nothing held off nor released.
1443        for state in [State::Reachable, State::Dnd] {
1444            let mut late = switch(&[("phone", pressed(t - 3 * DAY, State::Usual, 0)), ("old-laptop", Device { until: t + DAY, release: t, at: t, ..pressed(t - MARGIN_MS, state, t - 3 * DAY) })]);
1445            late.device.get_mut("old-laptop").unwrap().open = true;
1446            let read = late.counted("phone", &|id| id != "old-laptop");
1447            let p = read.latest().unwrap();
1448            let focus = Sources { focus: Some((t / 1000 - 3600, t / 1000 + 3600)), ..Sources::default() };
1449            let held = now(&settings, &focus, Some(&p), t / 1000);
1450            assert!(held.reachable.is_none() && held.has(Why::Focus) && !held.has(Why::Manual), "{state:?}: {held:?}");
1451            assert!(read.release_stamps().is_empty());
1452            assert!(others(&read, "phone", &[], t / 1000, t, &|_| true).is_empty(), "no device that follows");
1453        }
1454    }
1455
1456    /// Review of part 3, F2: Reachable never lasts past the night's start
1457    /// when pressed by day, nor past waking at night, nor a day after it was
1458    /// pressed, whatever its press says (a length of two hours 45 minutes
1459    /// before the night; a press with no end; one a year ahead).
1460    #[test]
1461    fn reachable_never_outlives_its_latest_end() {
1462        let settings = DndSettings { sleep: true, focus: true, ..DndSettings::default() };
1463        let night = (90_000, 90_000 + 8 * 3600);
1464        let nights = vec![night, (night.0 + 86_400, night.1 + 86_400)];
1465        // Two hours, 45 minutes before the night: until the night's start, said so by its end.
1466        let at = night.0 - 45 * 60;
1467        assert_eq!(reachable_until(Length::Minutes(120), at, None, Some(night.0), None), Some((night.0, false)));
1468        // At night, two hours half an hour before waking: until waking.
1469        assert_eq!(reachable_until(Length::Minutes(120), night.1 - 1800, None, Some(night.0 + 86_400), Some(night.1)), Some((night.1, false)));
1470        // A press written before the cap, for two hours: the night's do-not-disturb holds at its start.
1471        let older = reachable(at, at + 7200);
1472        let asleep = Sources { sleep: Some(night), nights: nights.clone(), ..Sources::default() };
1473        let held = now(&settings, &asleep, Some(&older), night.0 + 1800);
1474        assert!(held.reachable.is_none() && held.has(Why::Sleep), "{held:?}");
1475        assert_eq!(now(&settings, &Sources { nights: nights.clone(), ..Sources::default() }, Some(&older), at + 60).reachable.and_then(|r| r.until), Some(night.0), "its end said as the night's start");
1476        // No end (a later or garbled press): the night's start; with no night known, a day.
1477        let open = reachable(at, 0);
1478        assert!(now(&settings, &asleep, Some(&open), night.0 + 60).reachable.is_none());
1479        assert!(now(&settings, &Sources::default(), Some(&open), at + REACHABLE_MOST - 1).reachable.is_some());
1480        assert!(now(&settings, &Sources::default(), Some(&open), at + REACHABLE_MOST).reachable.is_none());
1481        // A year ahead: ten nights on, asleep, do-not-disturb holds.
1482        let far = reachable(at, at + 365 * 86_400);
1483        let tenth = (night.0 + 10 * 86_400, night.1 + 10 * 86_400);
1484        let held = now(&settings, &Sources { sleep: Some(tenth), nights: vec![tenth], ..Sources::default() }, Some(&far), tenth.0 + 60);
1485        assert!(held.reachable.is_none() && held.on(), "{held:?}");
1486        assert_eq!(reachable_end(at, Some(i64::MAX), &[]), at + REACHABLE_MOST, "never overflows");
1487        assert_eq!(reachable_end(i64::MAX - 10, None, &[]), i64::MAX);
1488    }
1489
1490    /// Review of part 3, F8: Reachable's end goes back to what held before
1491    /// it was pressed. As usual pressed during a focus session (or at night)
1492    /// held it off; Reachable pressed after, on the same device or another,
1493    /// carries that hold-off, so that once Reachable is over the session (or
1494    /// the night) stays held off until it ends. Without an As usual before,
1495    /// a reason begun before Reachable holds again at its end (choice 1).
1496    #[test]
1497    fn reachable_s_end_keeps_what_as_usual_held_off() {
1498        let t = 1_790_000_000_000;
1499        const MIN: i64 = 60_000;
1500        let settings = DndSettings { focus: true, sleep: true, ..DndSettings::default() };
1501        let focus = Sources { focus: Some((t / 1000, t / 1000 + 4 * 3600)), ..Sources::default() };
1502        for other in ["phone", "desk"] {
1503            let mut s = switch(&[("phone", pressed(t + 30 * MIN, State::Usual, 0))]);
1504            let at = |s: &Switch, m: i64| now(&settings, &focus, s.latest().as_ref(), (t + m * MIN) / 1000);
1505            assert!(!at(&s, 45).on());
1506            s.press_state(other, State::Reachable, t + 90 * MIN, false, t + 60 * MIN, "");
1507            assert_eq!(s.device[other].held_off, t + 30 * MIN);
1508            let during = at(&s, 75);
1509            assert!(!during.on() && during.reachable.is_some());
1510            assert!(!at(&s, 95).on(), "Reachable over, on {other}: the session stays held off");
1511            // Reachable pressed again, its length changed: the hold-off carried on.
1512            s.press_state(other, State::Reachable, t + 120 * MIN, false, t + 100 * MIN, "");
1513            assert_eq!(s.device[other].held_off, t + 30 * MIN);
1514        }
1515        // At night: As usual at 23:00, Reachable 23:30 to 00:00: the night stays held off.
1516        let night = Sources { sleep: Some((t / 1000 - 1800, t / 1000 + 8 * 3600)), nights: vec![(t / 1000 - 1800, t / 1000 + 8 * 3600)], ..Sources::default() };
1517        let mut s = switch(&[("phone", pressed(t, State::Usual, 0))]);
1518        s.press_state("phone", State::Reachable, t + 60 * MIN, false, t + 30 * MIN, "");
1519        assert!(!now(&settings, &night, s.latest().as_ref(), (t + 65 * MIN) / 1000).on());
1520        // No As usual before: the session begun before holds again at Reachable's end (choice 1).
1521        let mut plain = Switch::default();
1522        plain.press_state("phone", State::Reachable, t + 90 * MIN, false, t + 60 * MIN, "");
1523        assert_eq!(plain.device["phone"].held_off, 0);
1524        assert!(now(&settings, &focus, plain.latest().as_ref(), (t + 95 * MIN) / 1000).has(Why::Focus));
1525        // After Do not disturb, nothing held off either.
1526        let mut after_dnd = switch(&[("desk", pressed(t, State::Dnd, 0))]);
1527        after_dnd.press_state("phone", State::Reachable, t + 90 * MIN, false, t + 60 * MIN, "");
1528        assert_eq!(after_dnd.device["phone"].held_off, 0);
1529    }
1530
1531    /// Review of part 3, F3: release stamps never block one another. A
1532    /// computer ten minutes ahead releases; the phone's Reachable pressed
1533    /// after it, having seen it, comes after it and releases too; another
1534    /// device's, which had not seen it, is applied apart. A stamp at the end
1535    /// of time (a garbled table) is passed over, said once, and never drags
1536    /// the clock along: the next releases apply, nothing overflows.
1537    #[test]
1538    fn release_stamps_never_block_one_another() {
1539        use crate::appnotes::Ledger;
1540        let t = 1_790_000_000_000;
1541        const MIN: i64 = 60_000;
1542        let now_s = t / 1000;
1543        let mut s = switch(&[("phone", pressed(t - 86_400_000, State::Usual, 0))]);
1544        let ahead = s.release("laptop", t + 10 * MIN);
1545        let mut ledger = Ledger::default();
1546        assert!(ledger.apply_releases(&s.release_stamps(), now_s).0);
1547        let pressed_at = s.press_state("phone", State::Reachable, t + 62 * MIN, false, t + 2 * MIN, "");
1548        assert!(pressed_at > ahead, "the phone's press after the release it saw");
1549        assert!(ledger.apply_releases(&s.release_stamps(), now_s + 120).0, "and it releases");
1550        // A tablet that had not seen the laptop's release: its own Reachable, apart.
1551        let mut unseen = switch(&[("tablet", Device { pressed: t + 3 * MIN, mode: State::Reachable.id().into(), until: t + 60 * MIN, ..Device::default() })]);
1552        unseen.device.insert("laptop".into(), s.device["laptop"].clone());
1553        assert!(ledger.apply_releases(&unseen.release_stamps(), now_s + 180).0, "never shadowed by another device's stamp");
1554        // The end of time.
1555        let mut odd = switch(&[("phone", pressed(t - 86_400_000, State::Usual, 0)), ("odd", Device { release: i64::MAX, ..pressed(t - 86_400_000, State::Usual, 0) })]);
1556        let mut ledger = Ledger::default();
1557        assert_eq!(ledger.apply_releases(&odd.release_stamps(), now_s), (false, vec!["odd".to_string()]), "passed over, said");
1558        assert_eq!(ledger.apply_releases(&odd.release_stamps(), now_s + 1), (false, vec![]), "said once");
1559        let desk = odd.release("desk", t + MIN);
1560        assert_eq!(desk, t + MIN, "the clock never follows a stamp a day ahead");
1561        assert!(ledger.apply_releases(&odd.release_stamps(), now_s + 60).0);
1562        let press = odd.press_state("phone", State::Reachable, t + 62 * MIN, false, t + 2 * MIN, "");
1563        assert!(press < i64::MAX && press > desk);
1564        assert!(ledger.apply_releases(&odd.release_stamps(), now_s + 120).0);
1565        assert_eq!(odd.next_stamp(i64::MAX), i64::MAX, "saturates");
1566    }
1567
1568    /// Review of part 3, L1: three devices within the minute: A presses Do
1569    /// not disturb, B Reachable having seen it, C As usual having seen
1570    /// neither. A's press, which B overrode knowingly, never comes back: C's
1571    /// As usual holds.
1572    #[test]
1573    fn a_press_overridden_knowingly_never_comes_back() {
1574        let t = 1_790_000_000_000;
1575        let s = switch(&[("A", pressed(t, State::Dnd, t - 86_400_000)), ("B", Device { until: t + 3_600_000, ..pressed(t + 10_000, State::Reachable, t) }), ("C", pressed(t + 20_000, State::Usual, t - 86_400_000))]);
1576        assert_eq!(s.latest().map(|p| (p.from, p.state)), Some(("C".to_string(), State::Usual)));
1577        // Without B, A and C did not know each other: the quieter, A's, as before.
1578        let two = switch(&[("A", pressed(t, State::Dnd, t - 86_400_000)), ("C", pressed(t + 20_000, State::Usual, t - 86_400_000))]);
1579        assert_eq!(two.latest().map(|p| p.from), Some("A".to_string()));
1580    }
1581
1582    /// Review of part 3, L2: a pause pressed a minute after Reachable on a
1583    /// device whose clock is two minutes behind, which knew of Reachable,
1584    /// ends it; one that knew of no such press, its clock before it, does not.
1585    #[test]
1586    fn a_pause_that_knew_of_reachable_ends_it_whatever_its_clock() {
1587        let settings = DndSettings::default();
1588        let press = reachable(10_000, 20_000);
1589        let behind = Sources { pause_pressed: Some(10_000 - 60), pause_saw: Some(press.at), ..Sources::default() };
1590        assert!(now(&settings, &behind, Some(&press), 11_000).reachable.is_none());
1591        let before = Sources { pause_pressed: Some(10_000 - 60), pause_saw: Some(press.at - 1), ..Sources::default() };
1592        assert!(now(&settings, &before, Some(&press), 11_000).reachable.is_some());
1593    }
1594
1595    /// Re-check of part 3, R-1: a gone device's Expecting a call reads as
1596    /// "Screen calls again" at its stamp. The phone expected a call until
1597    /// turned off; the old laptop, since gone, said screen again: ended, as
1598    /// before it went. The old laptop expected a call until turned off, then
1599    /// went: nothing expected. A gone phone screens nothing.
1600    #[test]
1601    fn a_gone_device_s_expecting_a_call_opens_nothing() {
1602        const DAY: i64 = 86_400_000;
1603        let t = 1_790_000_000_000;
1604        let gone = |id: &str| id != "old-laptop";
1605        let mut raw = switch(&[("phone", pressed(t - 3 * DAY, State::Usual, 0)), ("old-laptop", pressed(t - 2 * DAY, State::Usual, 0))]);
1606        crate::calls::press(&mut raw, "phone", true, 0, t - 3 * DAY);
1607        crate::calls::press(&mut raw, "old-laptop", false, 0, t - 2 * DAY);
1608        let read = crate::calls::through_of(&raw.counted("phone", &gone)).unwrap();
1609        assert!(!read.holds(t), "its screen again still ends the phone's: {read:?}");
1610        let mut other = switch(&[("phone", pressed(t - 3 * DAY, State::Usual, 0))]);
1611        crate::calls::press(&mut other, "old-laptop", true, 0, t - 2 * DAY);
1612        assert!(crate::calls::through_of(&other.counted("phone", &gone)).is_none_or(|e| !e.holds(t)), "nothing expected");
1613        let mut screening = switch(&[("old-laptop", pressed(t - 2 * DAY, State::Usual, 0))]);
1614        crate::calls::set_screens(&mut screening, "old-laptop", true);
1615        assert!(crate::calls::screens_anywhere(&screening) && !crate::calls::screens_anywhere(&screening.counted("phone", &gone)));
1616    }
1617
1618    /// Check of a23cf0c, C-1: a laptop whose clock was two hours ahead
1619    /// releases; set right, its Reachable three minutes later and its "Let
1620    /// what your phone holds through" two minutes after both apply, each
1621    /// once, never shadowed by its own earlier stamp; another device's too.
1622    #[test]
1623    fn a_device_s_clock_set_right_never_shadows_its_own_releases() {
1624        use crate::appnotes::Ledger;
1625        let t = 1_790_000_000_000;
1626        const MIN: i64 = 60_000;
1627        let mut s = switch(&[("phone", pressed(t - 86_400_000, State::Usual, 0))]);
1628        s.release("laptop", t + 120 * MIN);
1629        let mut ledger = Ledger::default();
1630        assert!(ledger.apply_releases(&s.release_stamps(), t / 1000).0, "applied, at the phone's now");
1631        assert!(!ledger.apply_releases(&s.release_stamps(), t / 1000 + 60).0, "once");
1632        s.press_state("laptop", State::Reachable, t + 63 * MIN, false, t + 3 * MIN, "");
1633        assert!(ledger.apply_releases(&s.release_stamps(), t / 1000 + 180).0, "its Reachable, its clock set right");
1634        let release = s.release("laptop", t + 5 * MIN);
1635        assert!(release < t + 120 * MIN, "the clock never follows a release hours ahead");
1636        assert!(ledger.apply_releases(&s.release_stamps(), t / 1000 + 300).0, "its release, set right");
1637        assert!(!ledger.apply_releases(&s.release_stamps(), t / 1000 + 360).0, "each once");
1638        s.press_state("desk", State::Reachable, t + 66 * MIN, false, t + 6 * MIN, "");
1639        assert!(ledger.apply_releases(&s.release_stamps(), t / 1000 + 360).0, "another device's");
1640    }
1641
1642    /// Re-check of part 3, R-3: a release from a clock 23 hours ahead never
1643    /// drags the presses along: As usual pressed now is stamped now, and a
1644    /// focus session begun an hour later, or tonight, holds; a release within
1645    /// the hour is followed, a press within the day too.
1646    #[test]
1647    fn a_release_far_ahead_never_drags_the_presses() {
1648        let t = 1_790_000_000_000;
1649        let mut s = switch(&[("phone", pressed(t - 86_400_000, State::Usual, 0))]);
1650        s.release("odd", t + 23 * 3_600_000);
1651        let usual = s.press_state("phone", State::Usual, 0, false, t, "");
1652        assert_eq!(usual, t);
1653        let settings = DndSettings { focus: true, sleep: true, ..DndSettings::default() };
1654        let later = Sources { focus: Some((t / 1000 + 3600, t / 1000 + 4 * 3600)), ..Sources::default() };
1655        assert!(now(&settings, &later, s.latest().as_ref(), t / 1000 + 7200).on(), "the session begun an hour later holds");
1656        let night = Sources { sleep: Some((t / 1000 + 10 * 3600, t / 1000 + 18 * 3600)), ..Sources::default() };
1657        assert!(now(&settings, &night, s.latest().as_ref(), t / 1000 + 11 * 3600).on(), "tonight holds");
1658        let mut near = switch(&[("laptop", Device { release: t + 30 * 60_000, ..pressed(t - 86_400_000, State::Usual, 0) })]);
1659        assert_eq!(near.press_state("phone", State::Usual, 0, false, t, ""), t + 30 * 60_000 + 1, "a release within the hour: after it");
1660        let mut ahead = switch(&[("laptop", pressed(t + 5 * 3_600_000, State::Dnd, 0))]);
1661        assert_eq!(ahead.press_state("phone", State::Usual, 0, false, t, ""), t + 5 * 3_600_000 + 1, "a press hours ahead (a time zone set wrong): after it");
1662    }
1663
1664    /// Re-check of part 3, R-8: a press stamped more than a day ahead of
1665    /// this clock (a garbled stamp, a clock a year ahead) reads as not pressed
1666    /// yet, so that it never wins for ever; its Expecting a call too.
1667    #[test]
1668    fn a_press_far_ahead_never_wins_for_ever() {
1669        let t = 1_790_000_000_000;
1670        let mut s = switch(&[("phone", pressed(t - 86_400_000, State::Dnd, 0)), ("odd", pressed(t + 365 * 86_400_000, State::Usual, 0))]);
1671        crate::calls::press(&mut s, "odd", true, 0, t + 365 * 86_400_000);
1672        assert_eq!(s.latest().map(|p| p.from), Some("odd".to_string()), "as the file says");
1673        let read = s.clone().as_of(t);
1674        assert_eq!(read.latest().map(|p| (p.from, p.state)), Some(("phone".to_string(), State::Dnd)));
1675        assert!(crate::calls::through_of(&read).is_none());
1676        // A day on, a press made then by the phone comes after the phone's own.
1677        let mut later = switch(&[("odd", pressed(i64::MAX, State::Dnd, 0))]);
1678        assert!(later.clone().as_of(t).latest().is_none());
1679        assert_eq!(later.press_state("phone", State::Usual, 0, false, t, ""), t);
1680    }
1681
1682    /// Re-check of part 3, R-5: a gone device's As usual holds nothing off,
1683    /// so a Reachable pressed after it carries nothing of it.
1684    #[test]
1685    fn a_gone_device_s_as_usual_gives_reachable_nothing_to_carry() {
1686        let t = 1_790_000_000_000;
1687        let raw = switch(&[("old-laptop", pressed(t, State::Usual, 0))]);
1688        assert_eq!(held_off_of(raw.latest().as_ref(), State::Reachable), t, "as the file says");
1689        assert_eq!(held_off_of(raw.counted("phone", &|id| id != "old-laptop").latest().as_ref(), State::Reachable), 0);
1690    }
1691
1692    #[test]
1693    fn until_ends_it_by_each_devices_clock() {
1694        let settings = DndSettings::default();
1695        let press = Press { at: 1_000_000, on: true, until: 1_600_000, from: "desk".into(), via: String::new(), state: State::of(true), open: false, held_off: 0, gone: false };
1696        let held = now(&settings, &Sources::default(), Some(&press), 1_500);
1697        assert_eq!(held.why(), Some(Why::Manual));
1698        assert_eq!(held.until(), Some(1_600));
1699        assert!(!now(&settings, &Sources::default(), Some(&press), 1_600).on());
1700    }
1701
1702    #[test]
1703    fn each_trigger_is_a_setting_and_the_pause_comes_first() {
1704        let sources = Sources { paused: Some(100), free: Some((50, 900)), sleep: Some((80, 2000)), focus: Some((90, 1000)), pause_pressed: Some(100), ..Sources::default() };
1705        let none = DndSettings { pauses: false, sleep: false, focus: false, ..DndSettings::default() };
1706        assert!(!now(&none, &sources, None, 500).on());
1707        let all = DndSettings { pauses: true, sleep: true, focus: true, ..DndSettings::default() };
1708        let held = now(&all, &sources, None, 500);
1709        // Free time never with the pause.
1710        assert_eq!(held.holds.iter().map(|h| h.why).collect::<Vec<_>>(), vec![Why::Paused, Why::Sleep, Why::Focus]);
1711        assert_eq!(held.why(), Some(Why::Paused));
1712        assert!(held.global() && held.global_why() == Some(Why::Sleep));
1713        // The pause holds without an end: none said.
1714        assert_eq!(held.until(), None);
1715        // Free time alone, with its end.
1716        let free = Sources { free: Some((50, 900)), ..Sources::default() };
1717        let held = now(&all, &free, None, 500);
1718        assert_eq!((held.why(), held.until()), (Some(Why::FreeTime), Some(900)));
1719        assert!(!held.global());
1720        assert!(!now(&all, &free, None, 900).on(), "Free time over at its end");
1721    }
1722
1723    #[test]
1724    fn pressed_off_after_a_reason_began_holds_it_off_until_it_ends() {
1725        let settings = DndSettings { focus: true, sleep: true, ..DndSettings::default() };
1726        let focus = Sources { focus: Some((1_000, 5_000)), ..Sources::default() };
1727        let off_after = Press { at: 2_000_000, on: false, until: 0, from: "phone".into(), via: String::new(), state: State::of(false), open: false, held_off: 0, gone: false };
1728        assert!(!now(&settings, &focus, Some(&off_after), 3_000).on(), "the person turned it off during the session");
1729        // A session begun after the press holds again.
1730        let later = Sources { focus: Some((2_500, 6_000)), ..Sources::default() };
1731        assert!(now(&settings, &later, Some(&off_after), 3_000).has(Why::Focus));
1732        // A press "on" never holds a reason off; an "off" before it began neither.
1733        let on = Press { on: true, state: State::Dnd, ..off_after.clone() };
1734        let held = now(&settings, &focus, Some(&on), 3_000);
1735        assert!(held.has(Why::Focus) && held.has(Why::Manual));
1736        let off_before = Press { at: 500_000, ..off_after };
1737        assert!(now(&settings, &focus, Some(&off_before), 3_000).has(Why::Focus));
1738    }
1739
1740    #[test]
1741    fn mail_and_sioul_s_own_notifications_are_gated_by_the_switch_and_focus_only() {
1742        let settings = DndSettings { focus: true, sleep: true, ..DndSettings::default() };
1743        let press = Press { at: 1_000_000, on: true, until: 0, from: "desk".into(), via: String::new(), state: State::of(true), open: false, held_off: 0, gone: false };
1744        assert!(now(&settings, &Sources::default(), Some(&press), 2_000).gates());
1745        assert!(now(&settings, &Sources { focus: Some((1_000, 9_000)), ..Sources::default() }, None, 2_000).gates());
1746        // Sleep and the pauses keep their own rules.
1747        assert!(!now(&settings, &Sources { sleep: Some((1_000, 9_000)), ..Sources::default() }, None, 2_000).gates());
1748        assert!(!now(&settings, &Sources { paused: Some(1_000), ..Sources::default() }, None, 2_000).gates());
1749        assert!(!now(&settings, &Sources::default(), None, 2_000).gates());
1750    }
1751
1752    #[test]
1753    fn a_forgotten_focus_session_lets_you_be_reached_again() {
1754        let mut running = crate::timelog::Running { task: "t".into(), start: 1_000, planned: 25, paused_at: None, paused: 0 };
1755        assert_eq!(focus(&running, 1_100), Some((1_000, 1_000 + 25 * 60 + FOCUS_GRACE)));
1756        assert_eq!(focus(&running, 1_000 + 25 * 60 + FOCUS_GRACE), None);
1757        // Its own pauses push the limit on; paused, it does not hold.
1758        running.paused = 600;
1759        assert_eq!(focus(&running, 1_100).map(|(_, end)| end), Some(1_000 + 600 + 25 * 60 + FOCUS_GRACE));
1760        running.paused_at = Some(1_200);
1761        assert_eq!(focus(&running, 1_300), None);
1762        // Without a time chosen: three hours.
1763        let open = crate::timelog::Running { task: "t".into(), start: 1_000, ..crate::timelog::Running::default() };
1764        assert_eq!(focus(&open, 1_100).map(|(_, end)| end), Some(1_000 + FOCUS_OPEN));
1765    }
1766
1767    #[test]
1768    fn the_status_says_where_it_holds() {
1769        let tr = Translator::new("en");
1770        let settings = DndSettings::default();
1771        let press = Press { at: 1_000_000, on: true, until: 0, from: "phone".into(), via: String::new(), state: State::of(true), open: false, held_off: 0, gone: false };
1772        let held = now(&settings, &Sources::default(), Some(&press), 2_000);
1773        let name = |id: &str| if id == "phone" { "your phone".to_string() } else { id.to_string() };
1774        let all = [("phone".to_string(), Follows::Silenced)];
1775        let s = said(&held, at(true, ""), &all, "15:00", &name, &tr);
1776        assert_eq!(s.line, "Do not disturb, on every device, until 15:00.");
1777        assert!(s.everywhere && s.here);
1778        assert_eq!(s.why, "Turned on from your phone.");
1779        assert_eq!(s.details, vec!["Here: silenced.", "On your phone: silenced."]);
1780        let s = said(&held, at(true, ""), &[], "", &name, &tr);
1781        assert_eq!(s.line, "Do not disturb, here only.");
1782        // A device whose news has not come yet: on its way, never "here only".
1783        let behind = [("phone".to_string(), Follows::Behind), ("desk".to_string(), Follows::Silenced)];
1784        let s = said(&held, at(true, ""), &behind, "", &name, &tr);
1785        assert_eq!(s.line, "Do not disturb here; your other devices' news is on its way.");
1786        assert_eq!(s.details[1], "On your phone: waiting for its news.");
1787        let s = said(&held, at(true, ""), &[("phone".to_string(), Follows::Behind)], "15:00", &name, &tr);
1788        assert_eq!(s.line, "Do not disturb here until 15:00; your other devices' news is on its way.");
1789        // An older Sioul cannot follow: said as such, never waited for.
1790        let s = said(&held, at(true, ""), &[("phone".to_string(), Follows::Older)], "", &name, &tr);
1791        assert_eq!(s.line, "Do not disturb, here only.");
1792        assert_eq!(s.details[1], "On your phone: an older Sioul, which cannot follow until it is updated.");
1793        let cannot = [("phone".to_string(), Follows::Cannot("Your phone is not silenced: Sioul does not have Android's “Do Not Disturb access”.".into()))];
1794        let s = said(&held, at(true, ""), &cannot, "", &name, &tr);
1795        assert_eq!(s.line, "Do not disturb, here only.");
1796        assert_eq!(s.details[1], "On your phone: Your phone is not silenced: Sioul does not have Android's “Do Not Disturb access”.");
1797        // This device cannot be silenced (Windows): said with its own words.
1798        let s = said(&held, at(false, "This computer's notifications cannot be silenced by Sioul."), &all, "", &name, &tr);
1799        assert_eq!(s.line, "Do not disturb, on your other devices; this one keeps only Sioul's own notifications back.");
1800        assert!(!s.everywhere);
1801        // Off: nothing said.
1802        assert_eq!(said(&Now::default(), at(true, ""), &all, "", &name, &tr), Said::default());
1803        // In French, with its spaces.
1804        let fr = Translator::new("fr");
1805        let s = said(&held, at(true, ""), &all, "15:00", &name, &fr);
1806        assert_eq!(s.line, "Ne pas déranger, sur tous vos appareils, jusqu’à 15:00.");
1807        assert_eq!(s.details[0], "Ici\u{202f}: en silence.");
1808        assert_eq!(s.details[1], "Sur your phone\u{202f}: en silence.");
1809    }
1810
1811    #[test]
1812    fn others_count_when_heard_lately_and_follow_by_their_own_table() {
1813        let now_s = 1_000_000;
1814        let mut s = Switch::default();
1815        s.device.insert("phone".into(), Device { why: "manual".into(), silenced: true, at: now_s * 1000, ..Device::default() });
1816        s.device.insert("old".into(), Device { why: "manual".into(), silenced: true, at: (now_s - 30 * 86_400) * 1000, ..Device::default() });
1817        s.device.insert("laptop".into(), Device { why: "manual".into(), silenced: false, line: "Windows".into(), at: now_s * 1000, ..Device::default() });
1818        let heard = vec![("phone".to_string(), now_s - 60), ("tablet".to_string(), now_s - 3600), ("gone".to_string(), now_s - 10 * 86_400), ("desk".to_string(), now_s)];
1819        let registered = |_: &str| true;
1820        let followed = others(&s, "desk", &heard, now_s, (now_s - 60) * 1000, &registered);
1821        assert_eq!(followed, vec![("laptop".to_string(), Follows::Cannot("Windows".into())), ("phone".to_string(), Follows::Silenced), ("tablet".to_string(), Follows::Behind)]);
1822        // Do-not-disturb begun later than their tables were written, a minute's margin aside: their news is on its way.
1823        let later = others(&s, "desk", &heard, now_s, (now_s + 120) * 1000, &registered);
1824        assert!(later.iter().all(|(_, f)| *f == Follows::Behind), "{later:?}");
1825        let close = others(&s, "desk", &heard, now_s, (now_s + 30) * 1000, &registered);
1826        assert!(close.iter().any(|(_, f)| *f == Follows::Silenced), "{close:?}");
1827        // A device with no table and no entry in the devices' registry runs an older Sioul: it cannot follow.
1828        let older = others(&s, "desk", &heard, now_s, (now_s - 60) * 1000, &|id: &str| id != "tablet");
1829        assert!(older.contains(&("tablet".to_string(), Follows::Older)), "{older:?}");
1830    }
1831
1832    #[test]
1833    fn only_this_devices_table_is_written_and_a_broken_file_is_left_alone() {
1834        let dir = std::env::temp_dir().join(format!("sioul-everywhere-{}-{}", std::process::id(), line!()));
1835        let _ = std::fs::remove_dir_all(&dir);
1836        let path = dir.join(SWITCH_FILE);
1837        // Another device's table, as the sharing brought it.
1838        std::fs::create_dir_all(&dir).unwrap();
1839        std::fs::write(&path, "[device.\"phone-1\"]\nkind = \"phone\"\npressed = 7\non = true\n").unwrap();
1840        let s = change_own(&path, "desk-1", |s| {
1841            s.press("desk-1", false, 0, 1_000);
1842            true
1843        })
1844        .unwrap();
1845        assert_eq!(s.device.len(), 2);
1846        let again = Switch::read(&path).unwrap();
1847        assert_eq!(again.device["phone-1"].pressed, 7, "the other's table untouched");
1848        assert!(!again.device["desk-1"].on && again.device["desk-1"].seen == 7);
1849        // Broken: never written over.
1850        std::fs::write(&path, "[device.\"phone-1\"\nbroken").unwrap();
1851        assert!(change_own(&path, "desk-1", |_| true).is_err());
1852        assert!(std::fs::read_to_string(&path).unwrap().contains("broken"));
1853        let _ = std::fs::remove_dir_all(&dir);
1854    }
1855
1856    #[test]
1857    fn the_list_admits_its_people_by_address_and_number() {
1858        let fr = crate::phones::region_named("FR");
1859        let mut people = People::default();
1860        let alice = people.add(Person { name: "Alice".into(), phones: vec!["04 65 71 23 45".into()], emails: vec!["Alice@Example.org".into()], ..Person::default() }, fr);
1861        assert!(people.admits_address("alice@example.org"));
1862        assert!(people.admits_address("Alice Martin <ALICE@example.org>"));
1863        assert!(!people.admits_address("bob@example.org"));
1864        assert!(!people.admits_address(""));
1865        assert!(people.admits_number("+33 4 65 71 23 45", fr));
1866        assert!(people.admits_number("0465712345", fr));
1867        assert!(!people.admits_number("0465712346", fr));
1868        // A short code is nobody's.
1869        let mut short = People::default();
1870        short.add(Person { phones: vec!["3631".into()], ..Person::default() }, fr);
1871        assert!(!short.admits_number("3631", fr));
1872        // Added again with a new address: one person, merged.
1873        let again = people.add(Person { name: "A.".into(), phones: vec!["+33465712345".into()], emails: vec!["alice@work.example".into()], ..Person::default() }, fr);
1874        assert_eq!(again, alice);
1875        assert_eq!(people.people.len(), 1);
1876        assert_eq!(people.people[0].emails, vec!["Alice@Example.org", "alice@work.example"]);
1877        assert_eq!(people.people[0].name, "Alice", "the name kept");
1878        assert!(people.remove(&alice) && people.people.is_empty());
1879    }
1880
1881    #[test]
1882    fn someone_blocked_comes_off_the_list() {
1883        let fr = crate::phones::region_named("FR");
1884        let mut people = People::default();
1885        let alice = people.add(Person { name: "Alice".into(), phones: vec!["04 65 71 23 45".into()], emails: vec!["alice@example.org".into(), "alice@work.example".into()], ..Person::default() }, fr);
1886        people.add(Person { name: "Bob".into(), emails: vec!["bob@example.org".into()], contact: "uid-bob".into(), ..Person::default() }, fr);
1887        // One address blocked: that address off, the person kept by the rest.
1888        assert!(take_off(&mut people, &["Alice <ALICE@example.org>".into()], &[], "", fr));
1889        assert_eq!(people.people.iter().find(|p| p.id == alice).map(|p| p.emails.clone()), Some(vec!["alice@work.example".to_string()]));
1890        // Nothing of theirs: nothing changes.
1891        assert!(!take_off(&mut people, &["carol@example.org".into()], &["+33 1 99 00 00 01".into()], "", fr));
1892        // Their card blocked: the person off whole; the last ways to reach someone gone: the person too.
1893        assert!(take_off(&mut people, &[], &[], "uid-bob", fr));
1894        assert!(take_off(&mut people, &["alice@work.example".into()], &["+33465712345".into()], "", fr));
1895        assert!(people.people.is_empty());
1896    }
1897
1898    #[test]
1899    fn the_list_travels_one_person_at_a_time_and_is_never_read_empty() {
1900        let dir = std::env::temp_dir().join(format!("sioul-everywhere-{}-{}", std::process::id(), line!()));
1901        let _ = std::fs::remove_dir_all(&dir);
1902        let path = dir.join(PEOPLE_FILE);
1903        change_people(&path, |p| {
1904            p.add(Person { name: "Alice".into(), emails: vec!["alice@example.org".into()], ..Person::default() }, None);
1905        })
1906        .unwrap();
1907        let text = std::fs::read_to_string(&path).unwrap();
1908        assert!(text.contains("[[person]]") && text.contains("alice@example.org"), "{text}");
1909        // Another device's person, brought by the sharing between two edits here: kept.
1910        std::fs::write(&path, format!("{text}\n[[person]]\nid = \"b\"\nname = \"Bob\"\nphones = [\"+33536490002\"]\n")).unwrap();
1911        let people = change_people(&path, |p| {
1912            p.add(Person { name: "Carol".into(), emails: vec!["carol@example.org".into()], ..Person::default() }, None);
1913        })
1914        .unwrap();
1915        assert_eq!(people.people.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["Alice", "Bob", "Carol"]);
1916        // Unreadable: an error, never an empty list written over it.
1917        std::fs::write(&path, "[[person]\nbroken").unwrap();
1918        assert!(People::read(&path).is_err());
1919        assert!(change_people(&path, |p| p.people.clear()).is_err());
1920        assert!(std::fs::read_to_string(&path).unwrap().contains("broken"));
1921        assert!(People::load(&path).people.is_empty(), "a question gets nobody");
1922        let _ = std::fs::remove_dir_all(&dir);
1923    }
1924
1925    #[test]
1926    fn the_safe_list_brings_its_people_with_their_cards() {
1927        let card = |name: &str, email: &str, phone: &str, category: &str| crate::contacts::Contact {
1928            uid: format!("uid-{name}"),
1929            name: name.into(),
1930            emails: vec![crate::contacts::Labeled { label: String::new(), value: email.into() }],
1931            phones: vec![crate::contacts::Labeled { label: String::new(), value: phone.into() }],
1932            categories: if category.is_empty() { Vec::new() } else { vec![category.into()] },
1933            ..crate::contacts::Contact::default()
1934        };
1935        let contacts = vec![card("Alice", "alice@example.org", "+33 4 65 71 23 45", ""), card("Bob", "bob@example.org", "+33 1 99 00 00 01", "Famille"), card("Eve", "eve@example.org", "", "")];
1936        let safe = ["alice@example.org", "category:famille", "@example.com", "*@example.net", "dan@example.net", "contact:uid-Eve", "tel:+33 3 53 01 00 07", "tel:+33899*"].map(String::from).to_vec();
1937        let (people, patterns) = from_safe(&safe, &contacts);
1938        assert_eq!(patterns, 3, "a domain, a pattern, a prefix");
1939        assert_eq!(people.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["Alice", "Bob", "", "Eve", ""]);
1940        assert_eq!(people[0].phones, vec!["+33 4 65 71 23 45"]);
1941        assert_eq!(people[0].contact, "uid-Alice");
1942        assert_eq!(people[2].emails, vec!["dan@example.net"]);
1943        assert_eq!((people[3].contact.as_str(), people[4].phones.clone()), ("uid-Eve", vec!["+33 3 53 01 00 07".to_string()]));
1944    }
1945
1946    #[test]
1947    fn settings_have_their_defaults_and_old_files_read() {
1948        let config: crate::config::Config = toml::from_str("").unwrap();
1949        assert_eq!(config.dnd, DndSettings::default());
1950        assert!(config.dnd.button && config.dnd.pauses && config.dnd.people && !config.dnd.focus && !config.dnd.sleep);
1951        let config: crate::config::Config = toml::from_str("[dnd]\nfocus = true\n").unwrap();
1952        assert!(config.dnd.focus && config.dnd.pauses);
1953        // A switch's file with fields this version does not know still reads, and keeps them.
1954        let s: Switch = toml::from_str("[device.a]\npressed = 5\non = true\nfuture = \"x\"\n").unwrap();
1955        assert!(s.latest().unwrap().on);
1956        assert!(toml::to_string(&s).unwrap().contains("future = \"x\""), "a later version's field written back");
1957        let p: People = toml::from_str("[[person]]\nid = \"a\"\nname = \"A\"\nring = \"loud\"\n").unwrap();
1958        assert!(toml::to_string(&p).unwrap().contains("ring = \"loud\""));
1959    }
1960
1961    #[test]
1962    fn every_word_has_both_languages() {
1963        for language in ["en", "fr"] {
1964            let tr = Translator::new(language);
1965            for key in [
1966                "dnd-everywhere", "dnd-everywhere-until", "dnd-here-only", "dnd-here-only-until", "dnd-waiting", "dnd-waiting-until", "dnd-not-everywhere", "dnd-not-everywhere-until", "dnd-elsewhere", "dnd-elsewhere-until", "dnd-own-only", "dnd-own-only-until",
1967                "dnd-device-silenced", "dnd-device-cannot", "dnd-device-unsilenced", "dnd-device-behind", "dnd-device-older", "dnd-device-here", "dnd-device-on", "dnd-device-phone", "dnd-device-other",
1968                "dnd-why-manual", "dnd-why-manual-from", "dnd-why-focus", "dnd-why-sleep", "dnd-why-paused", "dnd-why-free-time",
1969                "dnd-turned-off-here", "dnd-turned-off-here-until", "dnd-device-closed",
1970            ] {
1971                let mut args = crate::i18n::args();
1972                args.set("until", "15:00");
1973                args.set("device", "X");
1974                args.set("line", "Y.");
1975                args.set("name", "Z");
1976                let words = tr.text(key, Some(&args));
1977                assert!(!words.is_empty() && words != key && !words.contains('{'), "{language}: {key}: {words}");
1978            }
1979        }
1980    }
1981
1982    #[test]
1983    fn a_change_of_the_system_presses_only_when_it_disagrees() {
1984        // The person's tile, a schedule, another app: on while nothing holds, a press "on".
1985        assert_eq!(heard(false, true), Some(true));
1986        // Off while do-not-disturb holds: a press "off".
1987        assert_eq!(heard(true, false), Some(false));
1988        // Sioul's own changes coming back agree with what holds: nothing.
1989        assert_eq!(heard(true, true), None);
1990        assert_eq!(heard(false, false), None);
1991    }
1992
1993    #[test]
1994    fn a_press_heard_from_the_system_says_so_and_holds_like_any() {
1995        let mut s = switch(&[("desk", device(1_000_000, true, 0))]);
1996        let stamp = s.press_via("phone", false, 0, 900_000, VIA_SYSTEM);
1997        // Stamped after every press known here, whatever the phone's clock.
1998        assert_eq!(stamp, 1_000_001);
1999        let latest = s.latest().unwrap();
2000        assert!(!latest.on && latest.from == "phone" && latest.via == VIA_SYSTEM);
2001        // The switch's own press says nothing of the kind, and clears the word.
2002        s.press("phone", true, 0, 2_000_000);
2003        assert_eq!(s.latest().unwrap().via, "");
2004        assert!(!toml::to_string(&s).unwrap().contains("via"), "an empty word is not written");
2005        // Written and read back; a file without the word reads as the switch's.
2006        s.press_via("desk", true, 0, 3_000_000, VIA_SYSTEM);
2007        let text = toml::to_string(&s).unwrap();
2008        assert!(text.contains("via = \"system\""), "{text}");
2009        let again: Switch = toml::from_str(&text).unwrap();
2010        assert_eq!(again.latest().unwrap().via, VIA_SYSTEM);
2011        let old: Switch = toml::from_str("[device.a]\npressed = 5\non = true\n").unwrap();
2012        assert_eq!(old.latest().unwrap().via, "");
2013        // A press "off" heard from the system holds off a reason begun before it, as any press.
2014        let settings = DndSettings { focus: true, ..DndSettings::default() };
2015        let focus = Sources { focus: Some((1_000, 9_000)), ..Sources::default() };
2016        let off = Press { at: 2_000_000, on: false, until: 0, from: "phone".into(), via: VIA_SYSTEM.into(), state: State::of(false), open: false, held_off: 0, gone: false };
2017        assert!(!now(&settings, &focus, Some(&off), 3_000).on());
2018    }
2019
2020    #[test]
2021    fn the_switch_held_by_the_system_alone_adds_no_mode() {
2022        let settings = DndSettings { sleep: true, focus: true, ..DndSettings::default() };
2023        let heard_here = Press { at: 1_000_000, on: true, until: 0, from: "phone".into(), via: VIA_SYSTEM.into(), state: State::of(true), open: false, held_off: 0, gone: false };
2024        let held = now(&settings, &Sources::default(), Some(&heard_here), 2_000);
2025        // The phone's own do-not-disturb turned the switch on: the phone adds no mode over it.
2026        assert!(held_by_system(&held, Some(&heard_here), "phone"));
2027        // The other devices silence themselves with their modes.
2028        assert!(!held_by_system(&held, Some(&heard_here), "desk"));
2029        // Pressed with the switch: Sioul's own mode everywhere.
2030        let pressed = Press { via: String::new(), ..heard_here.clone() };
2031        assert!(!held_by_system(&held, Some(&pressed), "phone"));
2032        // Another reason holds too (the night): Sioul's mode, so that it outlasts the system's cause.
2033        let night = now(&settings, &Sources { sleep: Some((1_500, 9_000)), ..Sources::default() }, Some(&heard_here), 2_000);
2034        assert!(!held_by_system(&night, Some(&heard_here), "phone"));
2035        // A pause has its own mode: the switch's alone is held by the system.
2036        let paused = now(&DndSettings::default(), &Sources { paused: Some(1_500), ..Sources::default() }, Some(&heard_here), 2_000);
2037        assert!(held_by_system(&paused, Some(&heard_here), "phone"));
2038        // Nothing holds: nothing to hold.
2039        assert!(!held_by_system(&Now::default(), Some(&heard_here), "phone"));
2040    }
2041
2042    #[test]
2043    fn a_nightly_schedule_turns_it_on_then_off_on_every_device() {
2044        let settings = DndSettings::default();
2045        let mut s = Switch::default();
2046        // 22:00: the phone's schedule turns its do-not-disturb on; nothing held it: a press "on".
2047        let night = 22 * 3_600;
2048        let before = now(&settings, &Sources::default(), s.latest().as_ref(), night);
2049        assert_eq!(heard(before.on(), true), Some(true));
2050        s.press_via("phone", true, 0, night * 1000, VIA_SYSTEM);
2051        // The computer reads the same file and holds it too, with its own mode.
2052        let held = now(&settings, &Sources::default(), s.latest().as_ref(), night + 60);
2053        assert!(held.on() && held.has(Why::Manual));
2054        assert!(!held_by_system(&held, s.latest().as_ref(), "desk"));
2055        assert!(held_by_system(&held, s.latest().as_ref(), "phone"));
2056        // 07:00: the schedule ends, the phone's system turns off while it held: a press "off".
2057        let morning = 31 * 3_600;
2058        let still = now(&settings, &Sources::default(), s.latest().as_ref(), morning);
2059        assert_eq!(heard(still.on(), false), Some(false));
2060        s.press_via("phone", false, 0, morning * 1000, VIA_SYSTEM);
2061        assert!(!now(&settings, &Sources::default(), s.latest().as_ref(), morning + 60).on(), "off on every device");
2062        // The next night, again.
2063        let next = night + 86_400;
2064        assert_eq!(heard(now(&settings, &Sources::default(), s.latest().as_ref(), next).on(), true), Some(true));
2065    }
2066
2067    #[test]
2068    fn a_press_made_off_line_meets_the_latest() {
2069        // The phone, off-line, hears its system turn on at 10:00 and presses; it has
2070        // not seen the computer's "off" made at 10:05. Back on line: the later press wins.
2071        let s = switch(&[
2072            ("phone", Device { pressed: 36_000_000, on: true, seen: 0, via: VIA_SYSTEM.into(), ..Device::default() }),
2073            ("desk", device(36_300_000, false, 0)),
2074        ]);
2075        assert!(!s.latest().unwrap().on, "the computer's later off");
2076        // Within a minute of each other, neither seeing the other: on wins, as for any press.
2077        let s = switch(&[
2078            ("phone", Device { pressed: 36_000_000, on: true, seen: 0, via: VIA_SYSTEM.into(), ..Device::default() }),
2079            ("desk", device(36_030_000, false, 0)),
2080        ]);
2081        let latest = s.latest().unwrap();
2082        assert!(latest.on && latest.from == "phone" && latest.via == VIA_SYSTEM);
2083    }
2084
2085    #[test]
2086    fn a_computer_closed_or_quiet_is_said_so() {
2087        let tr = Translator::new("en");
2088        let now_s = 1_000_000;
2089        let since = (now_s - 3_600) * 1000;
2090        let silenced = |at_s: i64| Device { kind: "computer".into(), name: "laptop".into(), why: "manual".into(), silenced: true, at: at_s * 1000, ..Device::default() };
2091        // Sioul quit on the laptop: its table as it then holds, read by another device.
2092        let mut quit = silenced(now_s - 600);
2093        quit.closing(false);
2094        assert!(quit.closed && !quit.silenced && quit.line.is_empty());
2095        let s = Switch { device: [("laptop".to_string(), quit.clone())].into(), ..Switch::default() };
2096        let text = toml::to_string(&s).unwrap();
2097        assert!(text.contains("closed = true"), "{text}");
2098        let read: Switch = toml::from_str(&text).unwrap();
2099        let heard = vec![("laptop".to_string(), now_s - 600)];
2100        let followed = others(&read, "phone", &heard, now_s, since, &|_| true);
2101        assert_eq!(followed, vec![("laptop".to_string(), Follows::Closed)]);
2102        let press = Press { at: since + 1, on: true, until: 0, from: "phone".into(), via: String::new(), state: State::of(true), open: false, held_off: 0, gone: false };
2103        let held = now(&DndSettings::default(), &Sources::default(), Some(&press), now_s);
2104        let name = |id: &str| id.to_string();
2105        let said_ = said(&held, at(true, ""), &followed, "", &name, &tr);
2106        assert_eq!(said_.details[1], "On laptop: Sioul is closed.");
2107        assert_eq!(said_.line, "Do not disturb, here only.");
2108        assert_eq!(said(&held, at(true, ""), &followed, "", &name, &Translator::new("fr")).details[1], "Sur laptop\u{202f}: Sioul est fermé.");
2109        // Closed before do-not-disturb began: still closed, never waited for.
2110        let mut early = silenced(now_s - 10 * 3_600);
2111        early.closing(false);
2112        let s = Switch { device: [("laptop".to_string(), early)].into(), ..Switch::default() };
2113        assert_eq!(others(&s, "phone", &heard, now_s, since, &|_| true)[0].1, Follows::Closed);
2114        // A crash says nothing: a computer said silenced, unheard for half an hour, is waiting for its news.
2115        let crashed = Switch { device: [("laptop".to_string(), silenced(now_s - 2_000))].into(), ..Switch::default() };
2116        let quiet = vec![("laptop".to_string(), now_s - 2_000)];
2117        assert_eq!(others(&crashed, "phone", &quiet, now_s, since, &|_| true)[0].1, Follows::Behind);
2118        // Heard ten minutes ago (a computer writes its news every fifteen at most): silenced.
2119        let lately = vec![("laptop".to_string(), now_s - 600)];
2120        assert_eq!(others(&crashed, "phone", &lately, now_s, since, &|_| true)[0].1, Follows::Silenced);
2121        // A phone's modes stay while Sioul is closed there: its table holds, however quiet.
2122        let phone = Device { kind: "phone".into(), ..silenced(now_s - 2_000) };
2123        let s = Switch { device: [("phone".to_string(), phone)].into(), ..Switch::default() };
2124        let quiet_phone = vec![("phone".to_string(), now_s - 2_000)];
2125        assert_eq!(others(&s, "laptop", &quiet_phone, now_s, since, &|_| true)[0].1, Follows::Silenced);
2126    }
2127
2128    #[test]
2129    fn a_device_turned_off_by_hand_says_so() {
2130        let tr = Translator::new("en");
2131        let press = Press { at: 1_000_000, on: true, until: 0, from: "desk".into(), via: String::new(), state: State::of(true), open: false, held_off: 0, gone: false };
2132        let held = now(&DndSettings::default(), &Sources::default(), Some(&press), 2_000);
2133        let name = |id: &str| id.to_string();
2134        let here = Here { silenced: false, line: "You turned do-not-disturb off on this phone.", turned_off: true };
2135        let s = said(&held, here, &[("desk".to_string(), Follows::Silenced)], "", &name, &tr);
2136        assert_eq!(s.line, "Do not disturb on your other devices; you turned it off here.");
2137        assert_eq!(s.details[0], "Here: You turned do-not-disturb off on this phone.");
2138        assert!(!s.here && !s.everywhere);
2139        let s = said(&held, here, &[], "15:00", &name, &Translator::new("fr"));
2140        assert_eq!(s.line, "Ne pas déranger jusqu\u{2019}à 15:00 sur vos autres appareils\u{202f}; vous l\u{2019}avez désactivé ici.");
2141    }
2142}