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