Skip to main content

sioul_core/
quiet.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! The time now, and what comes then (docs/areas.md). Five times: work and
5//! admin, the hours you set; meals and sleep, from Health; and leisure,
6//! every other time: evenings, days without hours, time off, a day closed
7//! early. Sleep first, then meals, then the hours. Two overrides, each until
8//! a time, kept in `$XDG_STATE_HOME/sioul/quiet.toml`: working late keeps
9//! work in view; done for the day brings leisure early, until work comes
10//! back. Outside work, work rests (quiet time); during sleep nothing
11//! disturbs, as usual: no notification but the doses you asked for. Two
12//! pauses come above them (docs/pauses.md, `pause`): the pause holds
13//! everything, whatever the time; Free time is leisure whatever the hour,
14//! sleep first, and may move the end of today's work later.
15//!
16//! What reaches you when, who and what, is one matrix (`attention`,
17//! docs/attention.md): its columns are these times, the pauses and two
18//! layers above them; the Porch's mail is shown as it says (`Attention::mail`).
19//!
20//! Detachment from work in the evening is what recovery needs most
21//! (Sonnentag & Fritz 2007, 2015); work cues in off-hours keep it from
22//! happening. Codes from verified senders still come at once: they are asked for.
23
24use crate::areas::{Area, TaskAreas, Time, Week, in_view};
25use crate::config::TimeOff;
26use crate::window::{self, AdminWindow};
27use jiff::civil::Date;
28use jiff::{Span, Zoned};
29use serde::{Deserialize, Serialize};
30use std::path::{Path, PathBuf};
31
32/// Why the time is what it is.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
34#[serde(rename_all = "kebab-case")]
35pub enum Reason {
36    /// Within working hours.
37    Working,
38    /// No working or admin hours are set: everything comes.
39    NoHours,
40    /// Working past the usual hours, by choice.
41    WorkingLate,
42    /// Work shown now, by choice, until you say otherwise or the next working day is over.
43    WorkNow,
44    /// After or before the day's hours.
45    Evening,
46    /// A day without working hours.
47    DayOff,
48    /// Holidays, sick leave.
49    TimeOff,
50    /// Stopped early, by choice.
51    DoneForTheDay,
52    /// Hours set for your own admin.
53    AdminTime,
54    /// A meal, from getting it ready to its end.
55    Meal,
56    /// The night, from bedtime to waking.
57    Sleep,
58    /// The night's first hour, winding down before bed: sleep's already.
59    WindingDown,
60    /// A nap, and the minutes to come back after it.
61    Nap,
62    /// Free time ("Temps libre"): leisure whatever the hour, by choice (docs/pauses.md).
63    FreeTime,
64    /// The pause ("En pause"): everything Sioul shows held, whatever the time (docs/pauses.md).
65    Paused,
66    /// After the usual end, while today's end of work moved by free time (docs/pauses.md).
67    Extended,
68}
69
70/// What now is for, and until when.
71#[derive(Debug, Clone, PartialEq)]
72pub struct Mode {
73    /// Work rests: these are not work's hours (leisure, meals, sleep, admin).
74    pub quiet: bool,
75    /// What now is for (docs/areas.md).
76    pub time: Time,
77    /// Which times your week holds.
78    pub week: Week,
79    pub reason: Reason,
80    /// When this time changes: its hours end, a meal or the night begins, you wake.
81    pub until: Option<Zoned>,
82    /// When work comes back, for what waits for it (a thought noted now): as
83    /// the hours stand, meals and sleep aside.
84    pub back: Option<Zoned>,
85    /// The time off's word, when it is time off.
86    pub label: String,
87}
88
89impl Mode {
90    /// Asleep: the night from winding down to waking, or a nap. Nothing
91    /// disturbs, as usual (`notify`); tasks, projects and time wait behind a sentence.
92    pub fn sleeps(&self) -> bool {
93        self.time == Time::Sleep
94    }
95
96    /// The pause: everything Sioul shows held (docs/pauses.md).
97    pub fn paused(&self) -> bool {
98        self.reason == Reason::Paused
99    }
100
101    /// Free time: leisure whatever the hour (docs/pauses.md).
102    pub fn free(&self) -> bool {
103        self.reason == Reason::FreeTime
104    }
105}
106
107/// The overrides, each until a time (Unix seconds).
108#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
109pub struct Overrides {
110    /// The file as it was read: saved, only what changed since is written (`filelock::save_merged`).
111    #[serde(skip)]
112    pub read: crate::filelock::Read,
113    /// Work stays in view until then.
114    #[serde(default)]
115    pub work_until: Option<i64>,
116    /// Quiet until then: done for the day.
117    #[serde(default)]
118    pub rest_until: Option<i64>,
119    /// "Work now", ticked: work shown until then, the end of the next working
120    /// day. Sioul takes it back when it closes.
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub work_now: Option<i64>,
123    /// When the day was closed: "back to today's plan" stays offered that day.
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub rest_from: Option<i64>,
126    /// The plan's first step when work comes back, in words, and its task.
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub first_step: Option<String>,
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub first_step_task: Option<String>,
131    /// The day it is for; it is not shown after.
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub first_step_day: Option<Date>,
134    /// Free time ("Temps libre"), pressed then; on while later than
135    /// `free_ended` (docs/pauses.md). Stamps, never a key taken away: the
136    /// sharing merges key by key, and a pause is never half on.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub free_since: Option<i64>,
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub free_ended: Option<i64>,
141    /// "Nothing at all" for this free time; unsaid, as set up.
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub free_nothing: Option<bool>,
144    /// The day free time took working time on, and how much, in minutes.
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub free_day: Option<Date>,
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub free_lost: Option<u32>,
149    /// Where the end of work moved to on `free_day` (Unix seconds).
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub extended_until: Option<i64>,
152    /// "Keep my usual end": the day nothing moves.
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    pub keep_end_on: Option<Date>,
155    /// The pause ("En pause"), pressed then; on while later than `paused_ended`.
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub paused_since: Option<i64>,
158    #[serde(default, skip_serializing_if = "Option::is_none")]
159    pub paused_ended: Option<i64>,
160    /// The latest press of do-not-disturb's switch the device knew of as it
161    /// pressed the pause (milliseconds, the switch's hybrid clock): the pause
162    /// ends a Reachable it knew of, even from a clock behind (review of part
163    /// 3 of "What reaches you", L2).
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub pause_saw: Option<i64>,
166    /// Days held lighter after a pause: the bad-day level.
167    #[serde(default, skip_serializing_if = "Vec::is_empty")]
168    pub lighter: Vec<Date>,
169    /// After a pause, the Porch rests until then, unless opened.
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    pub porch_rests_until: Option<i64>,
172}
173
174impl Overrides {
175    /// An act undone (closing the day, a pause ended): over these (the file
176    /// as it is now, read: its `read` kept, so that saving writes what
177    /// differs), each field the act changed (from `previous` to `after`) put
178    /// back as it was while it still holds what the act set (one another
179    /// device changed since stays), the others as they are now; the lighter
180    /// days with what the act added taken out and what it took out put back,
181    /// another device's kept (`settings::rebased`).
182    pub fn put_back(&self, previous: &Overrides, after: &Overrides) -> Overrides {
183        let table = |o: &Overrides| match toml::Value::try_from(o) {
184            Ok(toml::Value::Table(table)) => table,
185            _ => toml::Table::new(),
186        };
187        let (was, act, now) = (table(previous), table(after), table(self));
188        let mut out = now.clone();
189        let keys: std::collections::BTreeSet<&String> = was.keys().chain(act.keys()).collect();
190        let no_list = Vec::new();
191        for key in keys {
192            match (was.get(key), act.get(key)) {
193                (before, done) if before == done => {}
194                (before @ (Some(toml::Value::Array(_)) | None), done @ (Some(toml::Value::Array(_)) | None)) => {
195                    let list = |v: Option<&toml::Value>| v.and_then(toml::Value::as_array).cloned().unwrap_or_default();
196                    let back = crate::settings::rebased(&list(done), &list(before), now.get(key).and_then(toml::Value::as_array).unwrap_or(&no_list));
197                    if back.is_empty() {
198                        out.remove(key);
199                    } else {
200                        out.insert(key.clone(), toml::Value::Array(back));
201                    }
202                }
203                // Changed again since, by another device: left as it is now.
204                (_, done) if now.get(key) != done => {}
205                (Some(before), _) => {
206                    out.insert(key.clone(), before.clone());
207                }
208                (None, _) => {
209                    out.remove(key);
210                }
211            }
212        }
213        let mut back: Overrides = toml::Value::Table(out).try_into().unwrap_or_else(|_| self.clone());
214        back.read = self.read.clone();
215        back
216    }
217
218    /// The first step to show now: on the day it is for, in work time.
219    pub fn first_step_on(&self, today: Date) -> Option<(&str, &str)> {
220        let step = self.first_step.as_deref().filter(|s| !s.trim().is_empty())?;
221        self.first_step_day.is_none_or(|d| d == today).then(|| (step, self.first_step_task.as_deref().unwrap_or("")))
222    }
223
224    /// Whether the day was closed today, so today's plan can be taken back.
225    pub fn closed_today(&self, now: &Zoned) -> bool {
226        self.rest_from.and_then(|t| jiff::Timestamp::from_second(t).ok()).is_some_and(|t| t.to_zoned(now.time_zone().clone()).date() == now.date())
227    }
228}
229
230impl Overrides {
231    pub fn default_path() -> PathBuf {
232        crate::config::state_dir().join("quiet.toml")
233    }
234
235    pub fn load(path: &Path) -> Overrides {
236        let text = std::fs::read_to_string(path).unwrap_or_default();
237        let mut overrides: Overrides = toml::from_str(&text).unwrap_or_default();
238        overrides.read = crate::filelock::Read::of(&text);
239        overrides
240    }
241
242    /// Written under the file's lock, which the sharing takes too: only what
243    /// changed since it was read, over what the file holds now.
244    pub fn save(&self, path: &Path) -> Result<(), String> {
245        crate::filelock::save_merged(path, &self.read, self)
246    }
247}
248
249/// The time off that covers `date`, if any.
250pub fn time_off_on(time_off: &[TimeOff], date: Date) -> Option<&TimeOff> {
251    time_off.iter().find(|t| t.from <= date && date <= t.until)
252}
253
254/// When working hours next open after `now`, past any time off; None without hours.
255pub fn next_work(windows: &[AdminWindow], time_off: &[TimeOff], now: &Zoned) -> Option<Zoned> {
256    let mut from = now.clone();
257    for _ in 0..60 {
258        let opening = window::next_opening(windows, &from)?;
259        match time_off_on(time_off, opening.date()) {
260            None => return Some(opening),
261            // Within time off: from the day after it ends.
262            Some(off) => {
263                let after = off.until.checked_add(Span::new().days(1)).ok()?;
264                from = after.to_zoned(now.time_zone().clone()).ok()?.checked_sub(Span::new().seconds(1)).ok()?;
265            }
266        }
267    }
268    None
269}
270
271/// When work comes back once you are done for today: the next working hours
272/// after today, past any time off; without hours, tomorrow at `morning`.
273pub fn after_today(windows: &[AdminWindow], time_off: &[TimeOff], now: &Zoned, morning: i8) -> Zoned {
274    let Some(tomorrow) = now.date().tomorrow().ok().and_then(|d| d.to_zoned(now.time_zone().clone()).ok()) else {
275        return now.clone();
276    };
277    // `morning` comes from the settings: an hour that is none is midnight.
278    let at_morning = jiff::civil::Time::new(morning, 0, 0, 0).ok().and_then(|t| tomorrow.date().to_datetime(t).to_zoned(now.time_zone().clone()).ok());
279    next_work(windows, time_off, &tomorrow).unwrap_or_else(|| at_morning.unwrap_or(tomorrow))
280}
281
282/// When "Work now" ends: once the next working day is over, today's while
283/// its working hours are not; past any time off. Without working hours, at
284/// midnight. `work` are the working hours only.
285pub fn end_of_next_workday(work: &[AdminWindow], time_off: &[TimeOff], now: &Zoned) -> Zoned {
286    let zone = now.time_zone();
287    if time_off_on(time_off, now.date()).is_none()
288        && let Some((_, closing)) = window::hours_on(work, now.date(), zone).filter(|(_, closing)| closing.timestamp() > now.timestamp())
289    {
290        return closing;
291    }
292    if let Some(opening) = next_work(work, time_off, now)
293        && let Some((_, closing)) = window::hours_on(work, opening.date(), zone)
294    {
295        return closing;
296    }
297    now.date().tomorrow().ok().and_then(|d| d.to_zoned(zone.clone()).ok()).unwrap_or_else(|| now.clone())
298}
299
300/// "17:00", "tomorrow at 9:00", "Monday 5 October at 09:00".
301pub fn until_text(tr: &crate::i18n::Translator, until: &Zoned, now: &Zoned) -> String {
302    let mut args = crate::i18n::args();
303    args.set("time", until.strftime("%H:%M").to_string());
304    if until.date() == now.date() {
305        until.strftime("%H:%M").to_string()
306    } else if now.date().tomorrow().is_ok_and(|d| d == until.date()) {
307        tr.text("until-tomorrow", Some(&args))
308    } else {
309        tr.date(until, false)
310    }
311}
312
313/// What Health keeps free around now (docs/health.md, "Meals, rest and
314/// sleep"): the meals, naps and nights of today and tomorrow, each day's
315/// changes applied, and which of them are set at all.
316#[derive(Debug, Clone, Default, PartialEq)]
317pub struct Blocks {
318    /// In order: a meal from getting it ready to its end, a nap with its
319    /// minutes after, a night from winding down to waking.
320    pub kept: Vec<crate::needs::Kept>,
321    /// Some times are meals.
322    pub meals: bool,
323    /// Some times are sleep: the night or naps.
324    pub sleep: bool,
325    /// The night is set: without it, nights are leisure, and nothing keeps
326    /// notifications away while you sleep.
327    pub night: bool,
328}
329
330impl Blocks {
331    /// From Health's needs and each day's changes (`days`), the meals pushed
332    /// past `held` (events with their margins, `plan::event_spans`).
333    pub fn of(needs: &crate::needs::Needs, days: &crate::needs::Days, held: &[(i64, i64)], now: &Zoned) -> Blocks {
334        let kept = needs.kept_around(now, days, held);
335        let meals = (needs.meals_on && needs.meals.iter().any(|m| m.on)) || kept.iter().any(|k| k.kind == "meal");
336        let naps = (needs.naps_on && needs.naps.iter().any(|n| n.on)) || kept.iter().any(|k| k.kind == "nap");
337        Blocks { kept, meals, sleep: needs.sleep_on || naps, night: needs.sleep_on }
338    }
339
340    /// Read from Health's files, the meals pushed past `events`.
341    pub fn read(now: &Zoned, events: &[crate::agenda::Occurrence]) -> Blocks {
342        let needs = crate::health::Health::load(&crate::health::Health::default_path()).needs;
343        let days = crate::needs::Days::load(&crate::needs::Days::default_path());
344        Blocks::of(&needs, &days, &crate::plan::event_spans(events, 0), now)
345    }
346
347    /// The same, today's events read first.
348    pub fn read_now(now: &Zoned) -> Blocks {
349        let midnight = now.date().to_zoned(now.time_zone().clone()).map_or_else(|_| now.timestamp().as_second(), |z| z.timestamp().as_second());
350        Blocks::read(now, &crate::agenda::occurrences(midnight - 86_400, midnight + 2 * 86_400))
351    }
352
353    /// The block of one of `kinds` going on at `stamp` (Unix seconds).
354    pub fn at(&self, stamp: i64, kinds: &[&str]) -> Option<&crate::needs::Kept> {
355        self.kept.iter().find(|k| kinds.contains(&k.kind) && k.start <= stamp && stamp < k.end)
356    }
357
358    /// When the next block begins after `stamp`.
359    fn next_start(&self, stamp: i64) -> Option<i64> {
360        self.kept.iter().map(|k| k.start).filter(|start| *start > stamp).min()
361    }
362}
363
364/// What the hours alone say, meals and sleep aside.
365struct ByHours {
366    time: Time,
367    reason: Reason,
368    until: Option<Zoned>,
369    label: String,
370}
371
372/// The time by the hours, the overrides and time off: the hours of work and
373/// admin, leisure outside them. Meals and sleep come after (`mode`).
374fn by_hours(windows: &[AdminWindow], time_off: &[TimeOff], overrides: &Overrides, now: &Zoned) -> ByHours {
375    let of = |kind: &str| -> Vec<AdminWindow> { windows.iter().filter(|w| w.kind() == kind).cloned().collect() };
376    let (work, admin) = (of("work"), of("admin"));
377    let at = |seconds: i64| jiff::Timestamp::from_second(seconds).ok().map(|t| t.to_zoned(now.time_zone().clone()));
378    let made = |time: Time, reason: Reason, until: Option<Zoned>| ByHours { time, reason, until, label: String::new() };
379    let stamp = now.timestamp().as_second();
380    if let Some(rest) = overrides.rest_until.filter(|r| *r > stamp) {
381        return made(Time::Leisure, Reason::DoneForTheDay, at(rest));
382    }
383    if let Some(late) = overrides.work_until.filter(|w| *w > stamp) {
384        return made(Time::Work, Reason::WorkingLate, at(late));
385    }
386    if let Some(until) = overrides.work_now.filter(|w| *w > stamp) {
387        return made(Time::Work, Reason::WorkNow, at(until));
388    }
389    if windows.is_empty() && time_off.is_empty() {
390        return made(Time::Any, Reason::NoHours, None);
391    }
392    if let Some(off) = time_off_on(time_off, now.date()) {
393        return ByHours { time: Time::Leisure, reason: Reason::TimeOff, until: next_work(windows, time_off, now), label: off.label.clone() };
394    }
395    if windows.is_empty() {
396        return made(Time::Any, Reason::NoHours, None);
397    }
398    // Working and admin hours open now; both at once bring all they bring, until the first of them closes.
399    let (at_work, at_admin) = (window::current(&work, now), window::current(&admin, now));
400    let closing = [&at_work, &at_admin].into_iter().flatten().map(|(_, c)| c.clone()).min_by_key(Zoned::timestamp);
401    match Time::of(Area { work: at_work.is_some(), admin: at_admin.is_some(), leisure: false }) {
402        Time::Admin => made(Time::Admin, Reason::AdminTime, closing),
403        Time::Leisure => {
404            // Today's end of work moved by free time (docs/pauses.md): work from
405            // the usual end until then; Sioul ends it.
406            if let Some(end) = overrides.moved_end(now.date()).filter(|end| *end > stamp)
407                && window::hours_on(&work, now.date(), now.time_zone()).is_some_and(|(_, closing)| closing.timestamp().as_second() <= stamp)
408            {
409                return made(Time::Work, Reason::Extended, at(end));
410            }
411            let reason = if window::open_day(&work, now.date()) { Reason::Evening } else { Reason::DayOff };
412            // Until the next hours of either kind.
413            made(Time::Leisure, reason, next_work(windows, time_off, now))
414        }
415        // Work, or work and admin at once.
416        open => made(open, Reason::Working, closing),
417    }
418}
419
420/// The earlier of two times.
421fn earliest(a: Option<Zoned>, b: Option<Zoned>) -> Option<Zoned> {
422    match (a, b) {
423        (Some(a), Some(b)) => Some(if b.timestamp() < a.timestamp() { b } else { a }),
424        (a, b) => a.or(b),
425    }
426}
427
428/// What now is for, and until when (docs/areas.md): sleep first (the night
429/// from winding down to waking, a nap), then meals, then the hours:
430/// "Done for today" is leisure, "A little longer" and "Work now" are work,
431/// time off is leisure, then working and admin hours; every other time is
432/// leisure. No working or admin hours at all: everything comes (`Time::Any`),
433/// meals and sleep aside. `windows` are the week's hours of work and admin
434/// (`Config::week_hours`): an older Sioul's free time is left aside.
435pub fn mode(windows: &[AdminWindow], time_off: &[TimeOff], overrides: &Overrides, blocks: &Blocks, now: &Zoned) -> Mode {
436    let windows: Vec<AdminWindow> = windows.iter().filter(|w| w.kind() != "leisure").cloned().collect();
437    let week = Week { work_hours: windows.iter().any(|w| w.kind() == "work"), admin_hours: windows.iter().any(|w| w.kind() == "admin"), meals: blocks.meals, sleep: blocks.sleep };
438    let at = |seconds: i64| jiff::Timestamp::from_second(seconds).ok().map(|t| t.to_zoned(now.time_zone().clone()));
439    let stamp = now.timestamp().as_second();
440    let hours = by_hours(&windows, time_off, overrides, now);
441    // The pause holds everything Sioul shows, whatever the time (docs/pauses.md).
442    if overrides.paused_from().is_some() {
443        return Mode { quiet: true, time: Time::Sleep, week, reason: Reason::Paused, until: None, back: hours.until.clone(), label: String::new() };
444    }
445    // Work comes back when the block ends if these are work's hours; else as the hours say.
446    let back_after = |end: i64| if hours.time.works() { at(end) } else { hours.until.clone() };
447    // Health's times hold whatever the hours and the overrides say: "Work now" may span a night.
448    if let Some(block) = blocks.at(stamp, &["sleep", "nap"]) {
449        let reason = if block.kind == "nap" {
450            Reason::Nap
451        } else if stamp < block.at {
452            Reason::WindingDown
453        } else {
454            Reason::Sleep
455        };
456        return Mode { quiet: true, time: Time::Sleep, week, reason, until: at(block.end), back: back_after(block.end), label: String::new() };
457    }
458    // Free time: leisure whatever the hour, sleep first; the night's start ends it (docs/pauses.md).
459    if let Some(since) = overrides.free_from().filter(|since| *since <= stamp) {
460        let end = crate::pause::free_until(since, blocks, now.time_zone());
461        if stamp < end {
462            return Mode { quiet: true, time: Time::Leisure, week, reason: Reason::FreeTime, until: at(end), back: None, label: String::new() };
463        }
464    }
465    if let Some(block) = blocks.at(stamp, &["meal"]) {
466        return Mode { quiet: true, time: Time::Meals, week, reason: Reason::Meal, until: at(block.end), back: back_after(block.end), label: String::new() };
467    }
468    // The hours' own end, or the next meal or night when it comes first; an
469    // override and time off say their own time.
470    let cut = matches!(hours.reason, Reason::Working | Reason::AdminTime | Reason::Evening | Reason::DayOff | Reason::NoHours | Reason::Extended);
471    let until = if cut { earliest(hours.until.clone(), blocks.next_start(stamp).and_then(at)) } else { hours.until.clone() };
472    Mode { quiet: !(hours.time.works() || hours.time == Time::Any), time: hours.time, week, reason: hours.reason, until, back: hours.until, label: hours.label }
473}
474
475/// What the task pages need of the moment: whether work rests, whether
476/// offices are open, the days without room for tasks.
477pub struct Situation {
478    pub mode: Mode,
479    pub offices: crate::taskview::Offices,
480    /// Time off, and today when you are done for the day.
481    pub closed: std::collections::BTreeSet<Date>,
482    /// The weekdays offices open, Monday first.
483    pub office_days: [bool; 7],
484    /// What quiet time keeps of the tasks.
485    pub quiet_tasks: QuietTasks,
486}
487
488/// What the task pages keep now (docs/areas.md): what the time is for, by
489/// each task's area (its own, else its categories and projects). A call to an
490/// office fits work and admin hours, never leisure: offices keep business
491/// hours. In leisure (holidays, a day closed, evenings) and during a meal:
492/// only what is yours to enjoy, and what is yours either way (health).
493/// During sleep: no task at all.
494#[derive(Debug, Clone, Default, PartialEq, Eq)]
495pub struct QuietTasks {
496    pub time: Time,
497    pub week: Week,
498    pub areas: TaskAreas,
499    /// In free time with movement off (energy-limiting illness): movement and
500    /// exercise are not offered (docs/pauses.md, GP8).
501    pub no_movement: bool,
502}
503
504impl QuietTasks {
505    /// Whether a task stays in view now.
506    pub fn keeps(&self, task: &crate::tasks::Task) -> bool {
507        if self.time == Time::Sleep || (self.no_movement && crate::pause::is_movement(task)) {
508            return false;
509        }
510        let area = self.areas.of(task);
511        if task.office_hours && area.admin && !area.work {
512            return self.time.offices();
513        }
514        in_view(area, self.time, self.week)
515    }
516}
517
518impl Situation {
519    pub fn now(config: &crate::config::Config, overrides: &Overrides, blocks: &Blocks, now: &Zoned, tr: &crate::i18n::Translator, projects: &[crate::projects::Project]) -> Situation {
520        let mode = mode(&config.week_hours(), &config.time_off, overrides, blocks, now);
521        let office = config.office_hours();
522        let open = window::current(&office, now).is_some();
523        let next = if open { String::new() } else { window::next_opening(&office, now).map(|z| tr.when(&z)).unwrap_or_default() };
524        let mut office_days = [false; 7];
525        for w in &office {
526            if let Some(day) = w.weekday() {
527                office_days[day.to_monday_zero_offset() as usize] = true;
528            }
529        }
530        // Time off within the coming year, and today once done for it.
531        let today = now.date();
532        let horizon = today.checked_add(Span::new().days(400)).unwrap_or(today);
533        let mut closed = std::collections::BTreeSet::new();
534        for off in &config.time_off {
535            let mut day = off.from.max(today);
536            while day <= off.until && day <= horizon {
537                closed.insert(day);
538                let Ok(next) = day.tomorrow() else { break };
539                day = next;
540            }
541        }
542        if mode.reason == Reason::DoneForTheDay {
543            closed.insert(today);
544        }
545        let no_movement = mode.free() && !config.free_time.movement(&config.planning);
546        let quiet_tasks = QuietTasks { time: mode.time, week: mode.week, areas: TaskAreas::of_config(config, projects), no_movement };
547        Situation { mode, offices: crate::taskview::Offices { open, next, now: Some(now.clone()) }, closed, office_days, quiet_tasks }
548    }
549
550    /// The task filter, by what the time is for; none when no hours are set.
551    pub fn quiet_tasks(&self) -> Option<QuietTasks> {
552        (self.mode.time != Time::Any).then(|| self.quiet_tasks.clone())
553    }
554}
555
556/// Whether a task is yours, outside work: one of its categories is among
557/// `personal` (case and accents aside), or one of its cases is marked as yours.
558pub fn personal_task(task: &crate::tasks::Task, personal: &[String], personal_projects: &[String]) -> bool {
559    let fold = |s: &str| crate::text::fold(s.trim()).into_iter().collect::<String>();
560    let wanted: Vec<String> = personal.iter().map(|p| fold(p)).collect();
561    task.categories.iter().any(|c| wanted.contains(&fold(c))) || task.projects.iter().any(|c| personal_projects.contains(c))
562}
563
564#[cfg(test)]
565mod tests {
566
567    /// An undo puts back only what the act changed: a field another device
568    /// changed meanwhile stays; a lighter day the act added goes, another
569    /// device's stays (fourth review).
570    #[test]
571    fn an_undo_puts_back_only_what_the_act_changed() {
572        let day = |d: &str| d.parse::<Date>().unwrap();
573        let previous = Overrides { work_until: Some(5), lighter: vec![day("2026-10-12")], ..Overrides::default() };
574        let after = Overrides { rest_until: Some(100), lighter: vec![day("2026-10-12"), day("2026-10-13")], ..Overrides::default() };
575        let now = Overrides { rest_until: Some(100), paused_since: Some(7), lighter: vec![day("2026-10-12"), day("2026-10-13"), day("2026-10-14")], ..Overrides::default() };
576        let back = now.put_back(&previous, &after);
577        assert_eq!((back.work_until, back.rest_until, back.paused_since), (Some(5), None, Some(7)));
578        assert_eq!(back.lighter, [day("2026-10-12"), day("2026-10-14")]);
579        // A field the act set, changed again since by another device: left as it is now.
580        let now = Overrides { rest_until: Some(200), ..now };
581        assert_eq!(now.put_back(&previous, &after).rest_until, Some(200));
582    }
583    use super::*;
584    use crate::needs::{Days, Needs};
585
586    fn at(text: &str) -> Zoned {
587        text.parse().unwrap()
588    }
589
590    fn week() -> Vec<AdminWindow> {
591        ["monday", "tuesday", "wednesday", "thursday", "friday"].iter().map(|d| AdminWindow { day: d.to_string(), start: "09:00".into(), end: Some("17:00".into()), minutes: 0, kind: None }).collect()
592    }
593
594    fn none() -> Blocks {
595        Blocks::default()
596    }
597
598    /// Three meals, a nap and a night, as Health first sets them: breakfast
599    /// 07:50–08:20, lunch 12:10–13:00, a nap 14:00–14:35, dinner 19:00–20:00,
600    /// the night from 22:00 (bed at 23:00) to 07:00.
601    fn health() -> Needs {
602        Needs { meals_on: true, naps_on: true, sleep_on: true, ..Needs::default() }
603    }
604
605    #[test]
606    fn work_rests_outside_its_hours() {
607        let none = Overrides::default();
608        // 2 October 2026 is a Friday.
609        let friday_noon = at("2026-10-02T12:00[Europe/Paris]");
610        let m = mode(&week(), &[], &none, &self::none(), &friday_noon);
611        assert_eq!((m.quiet, m.reason.clone(), m.time), (false, Reason::Working, Time::Work));
612        assert_eq!(m.until.unwrap().datetime().to_string(), "2026-10-02T17:00:00");
613        let friday_evening = at("2026-10-02T19:00[Europe/Paris]");
614        let m = mode(&week(), &[], &none, &self::none(), &friday_evening);
615        assert_eq!((m.quiet, m.reason.clone(), m.time), (true, Reason::Evening, Time::Leisure), "the evening is leisure");
616        assert_eq!(m.until.unwrap().datetime().to_string(), "2026-10-05T09:00:00", "until Monday morning");
617        let saturday = at("2026-10-03T11:00[Europe/Paris]");
618        assert_eq!(mode(&week(), &[], &none, &self::none(), &saturday).reason, Reason::DayOff);
619        // Holidays the next week: leisure until the Monday after.
620        let off = vec![TimeOff { id: String::new(), from: "2026-10-05".parse().unwrap(), until: "2026-10-09".parse().unwrap(), label: "Holidays".into() }];
621        let m = mode(&week(), &off, &none, &self::none(), &saturday);
622        assert_eq!(m.until.unwrap().datetime().to_string(), "2026-10-12T09:00:00");
623        let m = mode(&week(), &off, &none, &self::none(), &at("2026-10-06T10:00[Europe/Paris]"));
624        assert_eq!((m.quiet, m.reason, m.label.as_str(), m.time), (true, Reason::TimeOff, "Holidays", Time::Leisure));
625        // Done for the day at noon: leisure; working late in the evening: work.
626        let rest = Overrides { rest_until: Some(at("2026-10-05T09:00[Europe/Paris]").timestamp().as_second()), ..Overrides::default() };
627        let m = mode(&week(), &[], &rest, &self::none(), &friday_noon);
628        assert_eq!((m.reason, m.time), (Reason::DoneForTheDay, Time::Leisure));
629        let late = Overrides { work_until: Some(at("2026-10-02T20:00[Europe/Paris]").timestamp().as_second()), ..Overrides::default() };
630        let m = mode(&week(), &[], &late, &self::none(), &friday_evening);
631        assert_eq!((m.quiet, m.reason, m.time), (false, Reason::WorkingLate, Time::Work));
632        assert_eq!(mode(&[], &[], &none, &self::none(), &saturday).reason, Reason::NoHours);
633        // Stopping on Friday at noon: work comes back on Monday; without hours, tomorrow morning.
634        assert_eq!(after_today(&week(), &[], &friday_noon, 7).datetime().to_string(), "2026-10-05T09:00:00");
635        assert_eq!(after_today(&[], &[], &friday_noon, 7).datetime().to_string(), "2026-10-03T07:00:00");
636        assert_eq!(after_today(&[], &[], &friday_noon, 30).datetime().to_string(), "2026-10-03T00:00:00", "a morning hour that is none");
637        let tr = crate::i18n::Translator::new("en");
638        assert_eq!(until_text(&tr, &at("2026-10-02T17:00[Europe/Paris]"), &friday_noon), "17:00");
639        assert_eq!(until_text(&tr, &at("2026-10-03T09:00[Europe/Paris]"), &friday_noon), "tomorrow at 09:00");
640        assert_eq!(until_text(&tr, &at("2026-10-05T09:00[Europe/Paris]"), &friday_noon), "Monday 5 October at 09:00");
641    }
642
643    #[test]
644    fn work_now_until_the_next_working_day_is_over() {
645        // Friday 2 October 2026 at 19:00, Saturday at 11:00, Friday at noon.
646        let (friday_evening, saturday, friday_noon) = (at("2026-10-02T19:00[Europe/Paris]"), at("2026-10-03T11:00[Europe/Paris]"), at("2026-10-02T12:00[Europe/Paris]"));
647        assert_eq!(end_of_next_workday(&week(), &[], &saturday).datetime().to_string(), "2026-10-05T17:00:00", "Monday's end");
648        assert_eq!(end_of_next_workday(&week(), &[], &friday_evening).datetime().to_string(), "2026-10-05T17:00:00");
649        assert_eq!(end_of_next_workday(&week(), &[], &friday_noon).datetime().to_string(), "2026-10-02T17:00:00", "today's, not over yet");
650        // Two spans a day: the day is over at the last one's end.
651        let mut split = week();
652        split.push(AdminWindow { day: "monday".into(), start: "19:00".into(), end: Some("20:00".into()), minutes: 0, kind: None });
653        assert_eq!(end_of_next_workday(&split, &[], &saturday).datetime().to_string(), "2026-10-05T20:00:00");
654        // Holidays the next week: the Monday after; no working hours: midnight.
655        let off = vec![TimeOff { id: String::new(), from: "2026-10-05".parse().unwrap(), until: "2026-10-09".parse().unwrap(), label: "Holidays".into() }];
656        assert_eq!(end_of_next_workday(&week(), &off, &saturday).datetime().to_string(), "2026-10-12T17:00:00");
657        assert_eq!(end_of_next_workday(&[], &[], &saturday).datetime().to_string(), "2026-10-04T00:00:00");
658        // Ticked on Saturday: work shown, as work, until then.
659        let now = Overrides { work_now: Some(end_of_next_workday(&week(), &[], &saturday).timestamp().as_second()), ..Overrides::default() };
660        let m = mode(&week(), &[], &now, &none(), &saturday);
661        assert_eq!((m.time, m.reason, m.quiet), (Time::Work, Reason::WorkNow, false));
662        assert_eq!(mode(&week(), &[], &now, &none(), &at("2026-10-05T17:30[Europe/Paris]")).reason, Reason::Evening, "over with Monday");
663    }
664
665    #[test]
666    fn work_and_admin_at_once() {
667        let none = Overrides::default();
668        let mut hours = week();
669        hours.push(AdminWindow { day: "friday".into(), start: "16:00".into(), end: Some("18:00".into()), minutes: 0, kind: Some("admin".into()) });
670        // Friday 2 October at 16:30: both are open; until 17:00, when work's close.
671        let m = mode(&hours, &[], &none, &self::none(), &at("2026-10-02T16:30[Europe/Paris]"));
672        assert_eq!((m.time, m.quiet), (Time::Several(Area::MIXED), false));
673        assert_eq!(m.until.unwrap().datetime().to_string(), "2026-10-02T17:00:00");
674        assert!(in_view(Area::ADMIN, m.time, m.week) && in_view(Area::WORK, m.time, m.week) && !in_view(Area::LEISURE, m.time, m.week));
675        // At 17:30, admin alone.
676        let m = mode(&hours, &[], &none, &self::none(), &at("2026-10-02T17:30[Europe/Paris]"));
677        assert_eq!((m.time, m.reason, m.quiet), (Time::Admin, Reason::AdminTime, true));
678    }
679
680    #[test]
681    fn old_leisure_windows_read_and_left_aside() {
682        // An older Sioul's free time on Saturdays: read without a word, kept in the file, never a time.
683        let config: crate::config::Config = toml::from_str(
684            "[[window]]\nday = \"friday\"\nstart = \"09:00\"\nend = \"17:00\"\n\n[[window]]\nday = \"saturday\"\nstart = \"10:00\"\nend = \"18:00\"\nkind = \"leisure\"\n\n[[window]]\nday = \"friday\"\nstart = \"18:00\"\nend = \"19:00\"\nkind = \"admin\"\n",
685        )
686        .unwrap();
687        assert_eq!(config.windows.len(), 3, "kept");
688        assert_eq!(config.week_hours().iter().map(|w| w.kind()).collect::<Vec<_>>(), ["work", "admin"]);
689        let none = Overrides::default();
690        let saturday = at("2026-10-03T11:00[Europe/Paris]");
691        let m = mode(&config.week_hours(), &[], &none, &self::none(), &saturday);
692        assert_eq!((m.time, m.reason), (Time::Leisure, Reason::DayOff));
693        // Given all the same, they change nothing.
694        let m = mode(&config.windows, &[], &none, &self::none(), &saturday);
695        assert_eq!((m.time, m.reason, m.week.work_hours, m.week.admin_hours), (Time::Leisure, Reason::DayOff, true, true));
696        // Friday at 20:00, after admin's hour: leisure until Monday... none on Monday here: the next Friday.
697        let m = mode(&config.windows, &[], &none, &self::none(), &at("2026-10-02T20:00[Europe/Paris]"));
698        assert_eq!((m.time, m.reason.clone()), (Time::Leisure, Reason::Evening));
699        assert_eq!(m.until.unwrap().datetime().to_string(), "2026-10-09T09:00:00", "never Saturday's old free time");
700    }
701
702    #[test]
703    fn sleep_then_meals_then_the_hours() {
704        let none = Overrides::default();
705        let friday = |time: &str| at(&format!("2026-10-02T{time}[Europe/Paris]"));
706        let blocks = |now: &Zoned| Blocks::of(&health(), &Days::default(), &[], now);
707        let mode_at = |time: &str, overrides: &Overrides| {
708            let now = if time.starts_with("+") { at(&format!("2026-10-03T{}[Europe/Paris]", &time[1..])) } else { friday(time) };
709            mode(&week(), &[], overrides, &blocks(&now), &now)
710        };
711        // Work, until lunch is got ready.
712        let m = mode_at("10:00", &none);
713        assert_eq!((m.time, m.reason.clone(), m.quiet), (Time::Work, Reason::Working, false));
714        assert_eq!(m.until.unwrap().datetime().to_string(), "2026-10-02T12:10:00");
715        // A meal within working hours is a meal while it lasts; work comes back after it.
716        let m = mode_at("12:30", &none);
717        assert_eq!((m.time, m.reason.clone(), m.quiet), (Time::Meals, Reason::Meal, true));
718        assert_eq!((m.until.unwrap().datetime().to_string(), m.back.unwrap().datetime().to_string()), ("2026-10-02T13:00:00".into(), "2026-10-02T13:00:00".into()));
719        assert!(m.week.meals && m.week.sleep);
720        // A nap, and its minutes to come back.
721        let m = mode_at("14:10", &none);
722        assert_eq!((m.time, m.reason.clone()), (Time::Sleep, Reason::Nap));
723        assert_eq!(m.until.unwrap().datetime().to_string(), "2026-10-02T14:35:00");
724        // The evening: leisure until the night begins, winding down, then the night until waking.
725        let m = mode_at("20:30", &none);
726        assert_eq!((m.time, m.reason.clone(), m.until.unwrap().datetime().to_string()), (Time::Leisure, Reason::Evening, "2026-10-02T22:00:00".into()));
727        assert_eq!(m.back.unwrap().datetime().to_string(), "2026-10-05T09:00:00", "work comes back on Monday");
728        let m = mode_at("22:30", &none);
729        assert!(m.sleeps() && m.quiet);
730        assert_eq!((m.time, m.reason.clone(), m.until.unwrap().datetime().to_string()), (Time::Sleep, Reason::WindingDown, "2026-10-03T07:00:00".into()));
731        assert_eq!(mode_at("23:30", &none).reason, Reason::Sleep);
732        assert_eq!((mode_at("+06:00", &none).time, mode_at("+06:00", &none).reason), (Time::Sleep, Reason::Sleep), "the night ending this morning");
733        // Sleep and meals come first, whatever the overrides: "Work now" may span a night.
734        let work_now = Overrides { work_now: Some(friday("23:59").timestamp().as_second() + 86_400 * 3), ..Overrides::default() };
735        assert_eq!(mode_at("23:30", &work_now).time, Time::Sleep);
736        assert_eq!(mode_at("12:30", &work_now).time, Time::Meals);
737        assert_eq!(mode_at("10:00", &work_now).reason, Reason::WorkNow);
738        let done = Overrides { rest_until: Some(friday("23:59").timestamp().as_second()), ..Overrides::default() };
739        assert_eq!(mode_at("12:30", &done).time, Time::Meals);
740        assert_eq!(mode_at("11:00", &done).reason, Reason::DoneForTheDay);
741        // No working or admin hours at all: everything, but meals and sleep keep their time.
742        let now = friday("12:30");
743        assert_eq!(mode(&[], &[], &none, &blocks(&now), &now).time, Time::Meals);
744        let now = friday("10:00");
745        let m = mode(&[], &[], &none, &blocks(&now), &now);
746        assert_eq!((m.time, m.quiet), (Time::Any, false));
747        assert_eq!(m.until.unwrap().datetime().to_string(), "2026-10-02T12:10:00");
748        // Lunch off today: work goes on; lunch quiet today (no notice): still a meal.
749        let mut days = Days::default();
750        days.change(now.date(), "meal:1", |b| b.off = true);
751        let noon = friday("12:30");
752        assert_eq!(mode(&week(), &[], &none, &Blocks::of(&health(), &days, &[], &noon), &noon).time, Time::Work);
753        let mut days = Days::default();
754        days.change(now.date(), "meal:1", |b| b.quiet = true);
755        assert_eq!(mode(&week(), &[], &none, &Blocks::of(&health(), &days, &[], &noon), &noon).time, Time::Meals);
756        // Without a night, nights are leisure: nothing keeps notifications away.
757        let no_night = Needs { sleep_on: false, ..health() };
758        let late = friday("23:30");
759        let blocks = Blocks::of(&no_night, &Days::default(), &[], &late);
760        assert!(!blocks.night && blocks.sleep, "naps still");
761        assert_eq!(mode(&week(), &[], &none, &blocks, &late).time, Time::Leisure);
762    }
763
764    #[test]
765    fn the_first_step_on_its_day() {
766        let monday: Date = "2026-10-05".parse().unwrap();
767        let overrides = Overrides { first_step: Some("Open the renewal form (15 min)".into()), first_step_task: Some("renewal".into()), first_step_day: Some(monday), ..Overrides::default() };
768        assert_eq!(overrides.first_step_on(monday), Some(("Open the renewal form (15 min)", "renewal")));
769        assert_eq!(overrides.first_step_on("2026-10-06".parse().unwrap()), None, "never the day after");
770        let closed = Overrides { rest_from: Some(at("2026-10-02T12:00[Europe/Paris]").timestamp().as_second()), ..Overrides::default() };
771        assert!(closed.closed_today(&at("2026-10-02T19:00[Europe/Paris]")));
772        assert!(!closed.closed_today(&at("2026-10-03T09:00[Europe/Paris]")), "today's plan is taken back the same day only");
773    }
774
775    #[test]
776    fn what_is_yours() {
777        let task = |cats: &[&str], projects: &[&str]| crate::tasks::Task { categories: cats.iter().map(|s| s.to_string()).collect(), projects: projects.iter().map(|s| s.to_string()).collect(), ..crate::tasks::Task::default() };
778        let personal = vec!["Famille".to_string(), "joy".into()];
779        assert!(personal_task(&task(&["famille"], &[]), &personal, &[]));
780        assert!(personal_task(&task(&[], &["garden"]), &personal, &["garden".into()]));
781        assert!(!personal_task(&task(&["you"], &["taxes"]), &personal, &["garden".into()]));
782        let areas = TaskAreas { work_categories: vec!["travail".into()], work_projects: vec!["client-x".into()], ..TaskAreas::usual() };
783        let week = Week { work_hours: true, ..Week::default() };
784        // Asleep: no task at all, yours neither.
785        let night = QuietTasks { time: Time::Sleep, week, areas: areas.clone(), no_movement: false };
786        assert!(!night.keeps(&task(&[], &["taxes"])) && !night.keeps(&task(&["joy"], &[])) && !night.keeps(&task(&["Travail"], &[])));
787        // In leisure (holidays, a day closed, the evening) and during a meal: only what is yours.
788        for time in [Time::Leisure, Time::Meals] {
789            let free = QuietTasks { time, ..night.clone() };
790            assert!(!free.keeps(&task(&[], &["taxes"])) && free.keeps(&task(&["joy"], &[])) && free.keeps(&task(&["santé"], &[])));
791            assert!(!free.keeps(&crate::tasks::Task { office_hours: true, ..task(&[], &["taxes"]) }), "offices keep business hours");
792        }
793        // Working hours without admin hours of its own: admin comes then, as it always did.
794        let working = QuietTasks { time: Time::Work, ..night.clone() };
795        assert!(working.keeps(&task(&[], &["taxes"])) && working.keeps(&task(&["Travail"], &[])) && !working.keeps(&task(&["joy"], &[])));
796        // Admin with hours of its own: it waits for them; a call to an office still fits the working day.
797        let set = QuietTasks { time: Time::Work, week: Week { admin_hours: true, ..week }, areas, no_movement: false };
798        // In free time with movement off: no exercise offered.
799        let free = QuietTasks { time: Time::Leisure, no_movement: true, ..set.clone() };
800        assert!(!free.keeps(&task(&["joy", "Yoga"], &[])) && free.keeps(&task(&["joy"], &[])));
801        assert!(!set.keeps(&task(&[], &["taxes"])));
802        assert!(set.keeps(&crate::tasks::Task { office_hours: true, ..task(&[], &["taxes"]) }));
803    }
804
805    #[test]
806    fn hours_for_admin() {
807        let none = Overrides::default();
808        let mut hours = week();
809        hours.push(AdminWindow { day: "friday".into(), start: "18:00".into(), end: Some("19:00".into()), minutes: 0, kind: Some("admin".into()) });
810        let m = mode(&hours, &[], &none, &self::none(), &at("2026-10-02T18:30[Europe/Paris]"));
811        assert_eq!((m.time, m.reason.clone(), m.quiet), (Time::Admin, Reason::AdminTime, true));
812        assert_eq!(m.until.unwrap().datetime().to_string(), "2026-10-02T19:00:00");
813        // After every hours set: leisure, until the next hours of either kind.
814        let m = mode(&hours, &[], &none, &self::none(), &at("2026-10-02T20:00[Europe/Paris]"));
815        assert_eq!((m.time, m.reason.clone()), (Time::Leisure, Reason::Evening));
816        assert_eq!(m.until.unwrap().datetime().to_string(), "2026-10-05T09:00:00");
817        assert_eq!(mode(&hours, &[], &none, &self::none(), &at("2026-10-02T10:00[Europe/Paris]")).time, Time::Work);
818        assert!(m.week.work_hours && m.week.admin_hours && !m.week.meals && !m.week.sleep);
819        assert_eq!(mode(&[], &[], &none, &self::none(), &at("2026-10-02T10:00[Europe/Paris]")).time, Time::Any);
820    }
821
822    #[test]
823    fn nothing_disturbs_but_doses() {
824        use crate::attention::{Attention, Column, Kind, Level, Now, Row};
825        let sleeping = Mode { quiet: true, time: Time::Sleep, week: Week::default(), reason: Reason::Sleep, until: None, back: None, label: String::new() };
826        let awake = Mode { time: Time::Leisure, reason: Reason::Evening, ..sleeping.clone() };
827        // As the matrix of what reaches you has it as usual (`attention`).
828        let usual = Attention::usual();
829        let comes = |matrix: &Attention, kind: Kind, mode: &Mode| matrix.level(Row::Own(kind), &Now::of(mode)) == Level::Now;
830        assert!(!comes(&usual, Kind::Move, &sleeping) && !comes(&usual, Kind::Dates, &sleeping), "no notification during sleep");
831        assert!(comes(&usual, Kind::Doses, &sleeping), "a dose comes: you set its time");
832        let mut silent = Attention::usual();
833        silent.set(Row::Own(Kind::Doses), Column::Sleep, Level::Later).unwrap();
834        assert!(!comes(&silent, Kind::Doses, &sleeping), "unless doses stay silent then");
835        assert!(comes(&silent, Kind::Move, &awake) && comes(&silent, Kind::Doses, &awake));
836        let winding = Mode { reason: Reason::WindingDown, ..sleeping };
837        assert!(winding.sleeps() && !comes(&usual, Kind::Move, &winding));
838    }
839}