1use crate::areas::{Time, Week};
22use crate::config::{Config, TimeOff};
23use crate::needs::{Days, Kept, Needs};
24use crate::porch::Standing;
25use crate::quiet::{Blocks, Mode, Overrides};
26use crate::window::AdminWindow;
27use jiff::{Timestamp, Zoned};
28use serde::{Deserialize, Serialize};
29
30pub const HORIZON_DAYS: i64 = 8;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum Who {
37 Stranger,
39 Blocked,
41 Safe,
43 Neutral,
45 Restricted,
47}
48
49impl Who {
50 pub const ALL: [Who; 5] = [Who::Safe, Who::Neutral, Who::Restricted, Who::Stranger, Who::Blocked];
52
53 pub fn id(self) -> &'static str {
54 match self {
55 Who::Stranger => "stranger",
56 Who::Blocked => "blocked",
57 Who::Safe => "safe",
58 Who::Neutral => "neutral",
59 Who::Restricted => "restricted",
60 }
61 }
62
63 pub fn read(text: &str) -> Option<Who> {
64 Who::ALL.into_iter().find(|w| w.id() == text.trim().to_ascii_lowercase())
65 }
66
67 pub fn standing(self) -> Option<Standing> {
69 match self {
70 Who::Safe => Some(Standing::Safe),
71 Who::Neutral => Some(Standing::Neutral),
72 Who::Restricted => Some(Standing::Restricted),
73 Who::Blocked => Some(Standing::Blocked),
74 Who::Stranger => None,
75 }
76 }
77
78 pub fn of(standing: Standing) -> Who {
80 match standing {
81 Standing::Safe => Who::Safe,
82 Standing::Neutral => Who::Neutral,
83 Standing::Restricted => Who::Restricted,
84 Standing::Blocked => Who::Blocked,
85 }
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
91#[serde(rename_all = "lowercase")]
92pub enum Channel {
93 Mail,
94 Calls,
96 Messages,
98}
99
100impl Channel {
101 pub const ALL: [Channel; 3] = [Channel::Mail, Channel::Calls, Channel::Messages];
102
103 pub fn id(self) -> &'static str {
104 match self {
105 Channel::Mail => "mail",
106 Channel::Calls => "calls",
107 Channel::Messages => "messages",
108 }
109 }
110
111 pub fn read(text: &str) -> Option<Channel> {
112 Channel::ALL.into_iter().find(|c| c.id() == text.trim().to_ascii_lowercase())
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub struct Moment {
120 pub time: Time,
122 pub week: Week,
124 pub paused: bool,
126 pub free: bool,
128 pub nothing: bool,
130}
131
132impl Moment {
133 pub fn of(mode: &Mode, nothing: bool) -> Moment {
135 Moment { time: mode.time, week: mode.week, paused: mode.paused(), free: mode.free(), nothing }
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub struct Frame {
142 pub start: i64,
143 pub end: i64,
144 pub moment: Moment,
145}
146
147#[derive(Debug, Clone)]
153pub struct Clock {
154 windows: Vec<AdminWindow>,
155 time_off: Vec<TimeOff>,
156 overrides: Overrides,
157 needs: Needs,
158 days: Days,
159 held: Vec<(i64, i64)>,
160 nothing: bool,
162}
163
164impl Clock {
165 pub fn new(config: &Config, overrides: Overrides, needs: Needs, days: Days, held: Vec<(i64, i64)>) -> Clock {
168 let nothing = crate::pause::nothing_now(&overrides, &config.free_time);
169 Clock { windows: config.week_hours(), time_off: config.time_off.clone(), overrides, needs, days, held, nothing }
170 }
171
172 pub fn load(config: &Config, now: &Zoned) -> Clock {
175 let overrides = Overrides::load(&Overrides::default_path());
176 let needs = crate::health::Health::load(&crate::health::Health::default_path()).needs;
177 let days = Days::load(&Days::default_path());
178 let stamp = now.timestamp().as_second();
179 let events = crate::agenda::occurrences(stamp - 86_400, stamp + (HORIZON_DAYS + 1) * 86_400);
180 Clock::new(config, overrides, needs, days, crate::plan::event_spans(&events, 0))
181 }
182
183 pub fn blocks(&self, at: &Zoned) -> Blocks {
185 Blocks::of(&self.needs, &self.days, &self.held, at)
186 }
187
188 pub fn mode(&self, at: &Zoned) -> Mode {
190 crate::quiet::mode(&self.windows, &self.time_off, &self.overrides, &self.blocks(at), at)
191 }
192
193 pub fn moment(&self, at: &Zoned) -> Moment {
195 Moment::of(&self.mode(at), self.nothing)
196 }
197
198 pub fn step(&self, at: &Zoned) -> (Moment, Option<Zoned>) {
203 let blocks = self.blocks(at);
204 let mode = crate::quiet::mode(&self.windows, &self.time_off, &self.overrides, &blocks, at);
205 let moment = Moment::of(&mode, self.nothing);
206 if mode.paused() {
207 return (moment, None);
208 }
209 let stamp = at.timestamp().as_second();
210 let free = self.overrides.free_from().filter(|since| *since > stamp);
211 let edge = blocks.kept.iter().flat_map(|k| [k.start, k.end]).chain(free).filter(|t| *t > stamp).min().and_then(|t| Timestamp::from_second(t).ok()).map(|t| t.to_zoned(at.time_zone().clone()));
212 let next = match (mode.until, edge) {
213 (Some(a), Some(b)) => Some(if b.timestamp() < a.timestamp() { b } else { a }),
214 (a, b) => a.or(b),
215 };
216 (moment, next.filter(|n| n.timestamp() > at.timestamp()))
217 }
218
219 pub fn frames(&self, from: &Zoned, until: &Zoned) -> Vec<Frame> {
222 let end = until.timestamp().as_second();
223 let mut out: Vec<Frame> = Vec::new();
224 let mut at = from.clone();
225 for _ in 0..4096 {
227 let start = at.timestamp().as_second();
228 if start >= end {
229 break;
230 }
231 let (moment, next) = self.step(&at);
232 let stop = next.as_ref().map_or(end, |n| n.timestamp().as_second().min(end));
233 match out.last_mut() {
234 Some(last) if last.moment == moment && last.end == start => last.end = stop,
235 _ => out.push(Frame { start, end: stop, moment }),
236 }
237 match next {
238 Some(next) => at = next,
239 None => break,
240 }
241 }
242 out
243 }
244
245 pub fn kept(&self, from: &Zoned, to: &Zoned) -> Vec<Kept> {
248 let (start, end) = (from.timestamp().as_second(), to.timestamp().as_second());
249 let mut out: Vec<Kept> = Vec::new();
250 let mut day = from.date();
251 while let Ok(noon) = day.at(12, 0, 0, 0).to_zoned(from.time_zone().clone()) {
252 if noon.timestamp().as_second() - 86_400 > end {
253 break;
254 }
255 for kept in self.blocks(&noon).kept {
256 if kept.end > start && kept.start < end && !out.iter().any(|k| k.key == kept.key && k.start == kept.start) {
257 out.push(kept);
258 }
259 }
260 let Ok(next) = day.tomorrow() else { break };
261 day = next;
262 }
263 out.sort_by_key(|k| k.start);
264 out
265 }
266}
267
268pub fn region(config: &Config) -> Option<&'static crate::phones::Region> {
271 let language = config.language.clone().unwrap_or_else(crate::i18n::system_language);
272 let locale = if language.to_ascii_lowercase().starts_with("fr") { "fr_FR" } else { "en_GB" };
274 crate::phones::chosen(config.contacts.region.as_deref(), locale)
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use jiff::tz::TimeZone;
281
282 fn at(text: &str) -> Zoned {
283 text.parse().unwrap()
284 }
285
286 #[test]
287 fn five_states_and_three_channels() {
288 assert_eq!(Who::ALL.map(Who::id), ["safe", "neutral", "restricted", "stranger", "blocked"]);
289 assert_eq!((Who::read(" Stranger "), Who::read("grey")), (Some(Who::Stranger), None));
290 assert_eq!(Who::Stranger.standing(), None);
291 assert_eq!(Who::of(Standing::Restricted), Who::Restricted);
292 assert_eq!(serde_json::to_string(&Who::Stranger).unwrap(), "\"stranger\"");
293 assert_eq!((Channel::read("Calls"), Channel::read("letters")), (Some(Channel::Calls), None));
294 }
295
296 fn weekdays() -> Config {
298 let text = ["monday", "tuesday", "wednesday", "thursday", "friday"].map(|d| format!("[[window]]\nday = \"{d}\"\nstart = \"09:00\"\nend = \"17:00\"\n")).join("\n");
299 toml::from_str(&text).unwrap()
300 }
301
302 fn health() -> Needs {
305 Needs { meals_on: true, sleep_on: true, ..Needs::default() }
306 }
307
308 #[test]
309 fn the_clock_cuts_at_meals_and_nights() {
310 let config = weekdays();
311 let clock = Clock::new(&config, Overrides::default(), health(), Days::default(), Vec::new());
312 let friday = at("2026-10-02T10:00[Europe/Paris]");
314 let (now, next) = clock.step(&friday);
315 assert_eq!((now.time, next.map(|n| n.datetime().to_string())), (Time::Work, Some("2026-10-02T12:10:00".into())));
316 let done = Overrides { rest_until: Some(at("2026-10-05T09:00[Europe/Paris]").timestamp().as_second()), ..Overrides::default() };
318 let clock = Clock::new(&config, done, health(), Days::default(), Vec::new());
319 let paris = TimeZone::get("Europe/Paris").unwrap();
320 let hour = |s: i64| Timestamp::from_second(s).unwrap().to_zoned(paris.clone()).strftime("%a %H:%M").to_string();
321 let frames = clock.frames(&at("2026-10-02T18:00[Europe/Paris]"), &at("2026-10-03T08:00[Europe/Paris]"));
322 let said: Vec<(String, Time)> = frames.iter().map(|f| (hour(f.start), f.moment.time)).collect();
323 let want = [("Fri 18:00", Time::Leisure), ("Fri 19:00", Time::Meals), ("Fri 20:00", Time::Leisure), ("Fri 22:00", Time::Sleep), ("Sat 07:00", Time::Leisure), ("Sat 07:50", Time::Meals)];
324 assert_eq!(said, want.map(|(h, t)| (h.to_string(), t)).to_vec());
325 assert_eq!(frames.last().map(|f| hour(f.end)), Some("Sat 08:00".into()));
326 assert_eq!(clock.kept(&at("2026-10-02T18:00[Europe/Paris]"), &at("2026-10-03T08:00[Europe/Paris]")).iter().map(|k| k.kind).collect::<Vec<_>>(), ["meal", "sleep", "meal"]);
327 let free = Overrides { free_since: Some(at("2026-10-02T10:30[Europe/Paris]").timestamp().as_second()), ..Overrides::default() };
329 let clock = Clock::new(&config, free, health(), Days::default(), Vec::new());
330 let frames = clock.frames(&at("2026-10-02T00:00[Europe/Paris]"), &at("2026-10-02T12:00[Europe/Paris]"));
331 assert_eq!(frames.iter().map(|f| (hour(f.start), f.moment.free)).collect::<Vec<_>>(), [("Fri 00:00".to_string(), false), ("Fri 07:00".into(), false), ("Fri 07:50".into(), false), ("Fri 08:20".into(), false), ("Fri 09:00".into(), false), ("Fri 10:30".into(), true)]);
332 let paused = Overrides { paused_since: Some(friday.timestamp().as_second() - 60), ..Overrides::default() };
334 let clock = Clock::new(&config, paused, health(), Days::default(), Vec::new());
335 let frames = clock.frames(&friday, &at("2026-10-03T10:00[Europe/Paris]"));
336 assert_eq!((frames.len(), frames[0].moment.paused), (1, true));
337 assert_eq!(clock.step(&friday).1, None);
338 }
339}