Skip to main content

sioul_app/
hours.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! What now is for, as the window asks it (docs/areas.md): work, admin,
5//! leisure, a meal, sleep, the pauses; and what reaches you then, as the one
6//! matrix says (`attention_now`, `comes`; the model is
7//! `sioul_core::attention`, docs/attention.md). One place for the pages and
8//! the notifications; the rule itself is `sioul_core::quiet::mode`. Health's
9//! meals, naps and nights are read again when its files change, today's
10//! events at most every five minutes, today's slots of time for you when
11//! their file changes: this is asked often (every list of sites, every
12//! notification).
13
14use crate::backend::load_config;
15use jiff::Zoned;
16use sioul_core::attention::{Attention, Column, Kind, Level, Now, Row, Slots};
17use sioul_core::quiet::{self, Blocks, Mode, Overrides};
18use sioul_core::reminders::Holds;
19use std::path::Path;
20use std::sync::Mutex;
21use std::time::SystemTime;
22
23/// When a file last changed; none when it is not there.
24fn changed(path: &Path) -> Option<SystemTime> {
25    std::fs::metadata(path).and_then(|m| m.modified()).ok()
26}
27
28/// Today's events as the time they hold (their margins counted), read again
29/// every five minutes at most, and on a new day.
30fn held(now: &Zoned) -> Vec<(i64, i64)> {
31    static KEPT: Mutex<Option<(i64, jiff::civil::Date, Vec<(i64, i64)>)>> = Mutex::new(None);
32    let stamp = now.timestamp().as_second();
33    let mut kept = KEPT.lock().unwrap_or_else(|e| e.into_inner());
34    if let Some((at, day, held)) = kept.as_ref()
35        && *day == now.date()
36        && (0..300).contains(&(stamp - at))
37    {
38        return held.clone();
39    }
40    let midnight = now.date().to_zoned(now.time_zone().clone()).map_or(stamp, |z| z.timestamp().as_second());
41    // From yesterday (a meeting past midnight) to tomorrow night.
42    let held = sioul_core::plan::event_spans(&sioul_core::agenda::occurrences(midnight - 86_400, midnight + 2 * 86_400), 0);
43    *kept = Some((stamp, now.date(), held.clone()));
44    held
45}
46
47/// Health's meals, naps and nights around now, each day's changes applied
48/// and the meals pushed past events, as the Health page has them.
49pub(crate) fn blocks(now: &Zoned) -> Blocks {
50    type Key = (i64, Option<SystemTime>, Option<SystemTime>);
51    static KEPT: Mutex<Option<(Key, Blocks)>> = Mutex::new(None);
52    let (health, days) = (sioul_core::health::Health::default_path(), sioul_core::needs::Days::default_path());
53    let key = (now.timestamp().as_second() / 60, changed(&health), changed(&days));
54    if let Some((known, blocks)) = KEPT.lock().unwrap_or_else(|e| e.into_inner()).as_ref()
55        && *known == key
56    {
57        return blocks.clone();
58    }
59    let needs = sioul_core::health::Health::load(&health).needs;
60    let blocks = Blocks::of(&needs, &sioul_core::needs::Days::load(&days), &held(now), now);
61    *KEPT.lock().unwrap_or_else(|e| e.into_inner()) = Some((key, blocks.clone()));
62    blocks
63}
64
65/// What now is for, and until when.
66pub(crate) fn mode_at(now: &Zoned) -> Mode {
67    let config = load_config();
68    let overrides = Overrides::load(&Overrides::default_path());
69    quiet::mode(&config.week_hours(), &config.time_off, &overrides, &blocks(now), now)
70}
71
72pub(crate) fn mode_now() -> Mode {
73    mode_at(&Zoned::now())
74}
75
76/// Today's slots of time for you (docs/capacity.md, G18b), as the window
77/// that laid out the day wrote them (`state/slots.toml`, `capacity::dress`):
78/// every process of this device reads the same, so that Time for you holds
79/// everywhere. Read again when the file changes.
80pub(crate) fn slots() -> Slots {
81    static KEPT: Mutex<Option<(Option<SystemTime>, Slots)>> = Mutex::new(None);
82    let path = Slots::default_path();
83    let stamp = changed(&path);
84    let mut kept = KEPT.lock().unwrap_or_else(|e| e.into_inner());
85    if let Some((known, slots)) = kept.as_ref()
86        && *known == stamp
87        && stamp.is_some()
88    {
89        return slots.clone();
90    }
91    let slots = Slots::load(&path);
92    *kept = Some((stamp, slots.clone()));
93    slots
94}
95
96/// Whether `at` is in one of today's slots of time for you: a layer of the
97/// matrix, which says what waits there (as usual, new mail's telling, the
98/// sites' notifications, other apps' automatons, the pause to move).
99pub(crate) fn in_slot(at: &Zoned) -> bool {
100    slots().at(at)
101}
102
103/// The same, now.
104pub(crate) fn quiet_slot() -> bool {
105    in_slot(&Zoned::now())
106}
107
108/// The matrix as set (Settings ▸ What reaches you).
109pub(crate) fn attention() -> Attention {
110    Attention::of(&load_config())
111}
112
113/// The moment as the matrix reads it at `now`, in `mode`: the time, a slot
114/// of time for you, do-not-disturb from its switch or a focus session
115/// (docs/do-not-disturb.md), Free time's "Nothing at all", the Porch
116/// resting after a pause.
117pub(crate) fn attention_at(mode: &Mode, now: &Zoned) -> Now {
118    let config = load_config();
119    let overrides = Overrides::load(&Overrides::default_path());
120    let mut moment = Now::of(mode).layers(in_slot(now), crate::everywhere::holds_others());
121    moment.nothing = mode.free() && sioul_core::pause::nothing_now(&overrides, &config.free_time);
122    moment.holds.porch_rests = overrides.porch_rests(now.timestamp().as_second());
123    moment
124}
125
126/// The moment now.
127pub(crate) fn attention_now() -> Now {
128    let now = Zoned::now();
129    attention_at(&mode_at(&now), &now)
130}
131
132/// What the matrix says now of one kind of Sioul's own.
133pub(crate) fn level(kind: Kind) -> Level {
134    attention().level(Row::Own(kind), &attention_now())
135}
136
137/// Whether one kind comes now, at once.
138pub(crate) fn comes(kind: Kind) -> bool {
139    level(kind) == Level::Now
140}
141
142/// Reminders' holds (`reminders::gather`) with this process's layers, the
143/// do-not-disturb it keeps a few seconds rather than reading its files again.
144pub(crate) fn with_layers(mut holds: Holds) -> Holds {
145    holds.now.slot = quiet_slot();
146    holds.now.dnd = crate::everywhere::holds_others();
147    holds
148}
149
150/// Whether a dose's reminder waits now: the matrix's doses row, whose only
151/// cells that may hold it are sleep's and the pause's, your choice. Never
152/// without it: a dose at 05:00 is meant to wake you.
153pub(crate) fn doses_silent() -> bool {
154    !comes(Kind::Doses)
155}
156
157/// When this sleep ends (Unix seconds), while asleep: waking, or a nap's end;
158/// while paused, in five minutes: the pause ends when you come back.
159pub(crate) fn waking() -> Option<i64> {
160    let mode = mode_now();
161    if mode.paused() {
162        return Some(Zoned::now().timestamp().as_second() + 5 * 60);
163    }
164    let sleeps = mode.sleeps();
165    mode.until.filter(|_| sleeps).map(|until| until.timestamp().as_second())
166}
167
168/// When the sleep that ended within the last half hour began (Unix seconds),
169/// when doses stay silent during sleep: the doses due since then waited for
170/// waking, and are reminded now; the same for a pause holding doses, at
171/// coming back (docs/pauses.md). None otherwise.
172pub(crate) fn woke_from() -> Option<i64> {
173    let attention = attention();
174    let held_in = |column: Column| attention.cell(Row::Own(Kind::Doses), column) != Level::Now;
175    let now = Zoned::now();
176    let stamp = now.timestamp().as_second();
177    let overrides = Overrides::load(&Overrides::default_path());
178    let paused = overrides.paused_since.zip(overrides.paused_ended).filter(|(since, ended)| held_in(Column::Pause) && ended > since && *ended <= stamp && stamp - ended < 30 * 60).map(|(since, _)| since);
179    if !held_in(Column::Sleep) {
180        return paused;
181    }
182    let blocks = blocks(&now);
183    // Not while a next one goes on (a nap right after the night).
184    if blocks.at(stamp, &["sleep", "nap"]).is_some() {
185        return None;
186    }
187    blocks.kept.iter().filter(|k| (k.kind == "sleep" || k.kind == "nap") && k.end <= stamp && stamp - k.end < 30 * 60).map(|k| k.start).chain(paused).min()
188}
189
190/// Whether a night is set (Health): without one, nothing keeps notifications away at night.
191pub(crate) fn night_set() -> bool {
192    blocks(&Zoned::now()).night
193}