1use crate::config::{config_dir, state_dir};
42use serde::{Deserialize, Serialize};
43use std::collections::BTreeMap;
44use std::path::{Path, PathBuf};
45
46pub const SWITCH_FILE: &str = "do-not-disturb.toml";
48pub const PEOPLE_FILE: &str = "dnd-people.toml";
50pub const MARGIN_MS: i64 = 60_000;
54pub const FOCUS_GRACE: i64 = 30 * 60;
57pub const FOCUS_OPEN: i64 = 180 * 60;
59pub const LIVE_DAYS: i64 = 7;
61pub const REACHABLE_MOST: i64 = 24 * 3600;
65pub const CLOCK_AHEAD_MS: i64 = 24 * 3600 * 1000;
71pub const RELEASE_FOLLOWED_MS: i64 = 3600 * 1000;
74pub const REACHABLE_RELEASE: &str = "#reachable";
77pub const GONE_FILE: &str = "gone-devices.json";
80pub const COMPUTER_QUIET: i64 = 30 * 60;
87
88fn yes() -> bool {
89 true
90}
91
92fn is_false(value: &bool) -> bool {
93 !*value
94}
95
96fn is_zero(value: &i64) -> bool {
97 *value == 0
98}
99
100#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
105#[serde(rename_all = "kebab-case")]
106pub enum State {
107 Reachable,
108 #[default]
109 Usual,
110 Dnd,
111}
112
113impl State {
114 pub const ALL: [State; 3] = [State::Reachable, State::Usual, State::Dnd];
115
116 pub fn id(self) -> &'static str {
117 match self {
118 State::Reachable => "reachable",
119 State::Usual => "usual",
120 State::Dnd => "dnd",
121 }
122 }
123
124 pub fn read(word: &str) -> Option<State> {
125 State::ALL.into_iter().find(|s| s.id() == word.trim())
126 }
127
128 pub fn of(on: bool) -> State {
130 if on { State::Dnd } else { State::Usual }
131 }
132}
133
134pub const VIA_SYSTEM: &str = "system";
136
137#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
139pub struct DndSettings {
140 #[serde(default = "yes")]
142 pub button: bool,
143 #[serde(default)]
145 pub focus: bool,
146 #[serde(default = "yes")]
149 pub pauses: bool,
150 #[serde(default)]
152 pub sleep: bool,
153 #[serde(default = "yes")]
157 pub people: bool,
158 #[serde(default = "yes")]
160 pub background: bool,
161}
162
163impl Default for DndSettings {
164 fn default() -> Self {
165 DndSettings { button: true, focus: false, pauses: true, sleep: false, people: true, background: true }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
171#[serde(rename_all = "kebab-case")]
172pub enum Why {
173 Paused,
174 FreeTime,
175 Sleep,
176 Focus,
177 Manual,
178}
179
180impl Why {
181 pub fn id(self) -> &'static str {
182 match self {
183 Why::Paused => "paused",
184 Why::FreeTime => "free-time",
185 Why::Sleep => "sleep",
186 Why::Focus => "focus",
187 Why::Manual => "manual",
188 }
189 }
190
191 pub fn parse(id: &str) -> Option<Why> {
192 [Why::Paused, Why::FreeTime, Why::Sleep, Why::Focus, Why::Manual].into_iter().find(|w| w.id() == id)
193 }
194
195 pub fn global(self) -> bool {
197 matches!(self, Why::Sleep | Why::Focus | Why::Manual)
198 }
199}
200
201#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
205pub struct Device {
206 #[serde(default)]
208 pub kind: String,
209 #[serde(default)]
211 pub name: String,
212 #[serde(default)]
214 pub pressed: i64,
215 #[serde(default)]
217 pub on: bool,
218 #[serde(default)]
220 pub until: i64,
221 #[serde(default)]
223 pub seen: i64,
224 #[serde(default)]
226 pub why: String,
227 #[serde(default)]
229 pub silenced: bool,
230 #[serde(default)]
232 pub line: String,
233 #[serde(default)]
235 pub at: i64,
236 #[serde(default, skip_serializing_if = "String::is_empty")]
239 pub via: String,
240 #[serde(default, skip_serializing_if = "is_false")]
244 pub turned_off: bool,
245 #[serde(default, skip_serializing_if = "is_false")]
248 pub system_on: bool,
249 #[serde(default, skip_serializing_if = "is_false")]
252 pub closed: bool,
253 #[serde(default, skip_serializing_if = "String::is_empty")]
257 pub mode: String,
258 #[serde(default, skip_serializing_if = "is_false")]
261 pub open: bool,
262 #[serde(default, skip_serializing_if = "is_zero")]
266 pub release: i64,
267 #[serde(default, skip_serializing_if = "is_zero")]
272 pub held_off: i64,
273 #[serde(skip)]
277 pub gone: bool,
278 #[serde(flatten)]
280 pub other: toml::Table,
281}
282
283impl Device {
284 pub fn state(&self) -> State {
287 State::read(&self.mode).unwrap_or(State::of(self.on))
288 }
289
290 pub fn closing(&mut self, still: bool) {
295 self.closed = true;
296 self.silenced = still;
297 self.turned_off = false;
298 self.system_on = false;
299 self.line = String::new();
300 }
301}
302
303#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
305pub struct Switch {
306 #[serde(default)]
307 pub device: BTreeMap<String, Device>,
308 #[serde(flatten)]
310 pub other: toml::Table,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct Press {
316 pub at: i64,
318 pub on: bool,
319 pub until: i64,
321 pub from: String,
323 pub via: String,
325 pub state: State,
327 pub open: bool,
329 pub held_off: i64,
331 pub gone: bool,
334}
335
336impl Switch {
337 pub fn default_path() -> PathBuf {
338 state_dir().join(SWITCH_FILE)
339 }
340
341 pub fn read(path: &Path) -> Result<Switch, String> {
344 match std::fs::read_to_string(path) {
345 Ok(text) => toml::from_str(&text).map_err(|e| format!("{}: {e}", path.display())),
346 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Switch::default()),
347 Err(e) => Err(format!("{}: {e}", path.display())),
348 }
349 }
350
351 pub fn load(path: &Path) -> Switch {
353 Switch::read(path).unwrap_or_default()
354 }
355
356 pub fn latest(&self) -> Option<Press> {
361 let presses: Vec<(&String, &Device)> = self.device.iter().filter(|(_, d)| d.pressed > 0).collect();
362 let (from, last) = presses.iter().max_by_key(|(id, d)| (d.pressed, (*id).clone()))?;
363 let mut winner = (*from, *last);
364 let overridden = |d: &Device| presses.iter().any(|(_, later)| later.pressed > d.pressed && later.seen >= d.pressed);
367 let tied = presses
368 .iter()
369 .filter(|(id, d)| *id != *from && d.state() > last.state() && last.pressed.saturating_sub(d.pressed) <= MARGIN_MS && last.seen < d.pressed && !overridden(d))
370 .max_by_key(|(id, d)| (d.state(), d.pressed, (*id).clone()));
371 if let Some((id, d)) = tied {
372 winner = (*id, *d);
373 }
374 let d = winner.1;
375 let state = d.state();
376 let reachable = state == State::Reachable;
377 Some(Press { at: d.pressed, on: state == State::Dnd, until: d.until, from: winner.0.clone(), via: d.via.clone(), state, open: reachable && d.open, held_off: if reachable { d.held_off } else { 0 }, gone: d.gone })
378 }
379
380 pub fn counted(&self, here: &str, counts: &dyn Fn(&str) -> bool) -> Switch {
389 let device = self
390 .device
391 .iter()
392 .map(|(id, d)| {
393 if id.as_str() == here || counts(id) {
394 (id.clone(), d.clone())
395 } else {
396 let mut read = Device { pressed: d.pressed, seen: d.seen, mode: State::Usual.id().to_string(), gone: true, ..Device::default() };
397 let calls = crate::calls::part_of(d);
400 if calls.pressed > 0 {
401 crate::calls::set_part(&mut read, crate::calls::DevicePart { pressed: calls.pressed, ..crate::calls::DevicePart::default() });
402 }
403 (id.clone(), read)
404 }
405 })
406 .collect();
407 Switch { device, other: self.other.clone() }
408 }
409
410 pub fn release_stamps(&self) -> Vec<(String, i64)> {
417 let mut out: Vec<(String, i64)> = self.device.iter().filter(|(_, d)| d.release > 0 && !d.gone).map(|(id, d)| (id.clone(), d.release)).collect();
418 if let Some(p) = self.latest().filter(|p| p.state == State::Reachable && !p.gone) {
420 out.push((format!("{}{REACHABLE_RELEASE}", p.from), p.at));
421 }
422 out.sort();
423 out
424 }
425
426 pub fn next_stamp(&self, now_ms: i64) -> i64 {
434 let (presses, releases) = (now_ms.saturating_add(CLOCK_AHEAD_MS), now_ms.saturating_add(RELEASE_FOLLOWED_MS));
435 let pressed = self.device.values().map(|d| d.pressed).filter(|stamp| *stamp <= presses);
436 let released = self.device.values().map(|d| d.release).filter(|stamp| *stamp <= releases);
437 let known = pressed.chain(released).max().unwrap_or(0);
438 now_ms.max(known.saturating_add(1))
439 }
440
441 pub fn release(&mut self, here: &str, now_ms: i64) -> i64 {
444 let stamp = self.next_stamp(now_ms);
445 let own = self.device.entry(here.to_string()).or_default();
446 own.release = stamp;
447 own.at = own.at.max(stamp);
448 stamp
449 }
450
451 pub fn latest_known(&self, except: Option<&str>) -> i64 {
453 self.device.iter().filter(|(id, _)| Some(id.as_str()) != except).map(|(_, d)| d.pressed).max().unwrap_or(0)
454 }
455
456 pub fn press(&mut self, here: &str, on: bool, until: i64, now_ms: i64) -> i64 {
459 self.press_via(here, on, until, now_ms, "")
460 }
461
462 pub fn press_via(&mut self, here: &str, on: bool, until: i64, now_ms: i64, via: &str) -> i64 {
465 self.press_state(here, State::of(on), until, false, now_ms, via)
466 }
467
468 pub fn press_state(&mut self, here: &str, state: State, until: i64, open: bool, now_ms: i64, via: &str) -> i64 {
473 let stamp = self.next_stamp(now_ms);
475 let seen = self.latest_known(Some(here));
476 let held_off = held_off_of(self.latest().as_ref(), state);
479 let own = self.device.entry(here.to_string()).or_default();
480 own.held_off = held_off;
481 own.pressed = stamp;
482 own.on = state == State::Dnd;
483 own.mode = state.id().to_string();
484 own.until = if state == State::Usual { 0 } else { until.max(0) };
485 own.open = state == State::Reachable && open;
486 own.seen = seen;
487 own.at = stamp;
488 own.via = via.to_string();
489 stamp
490 }
491
492 pub fn save(&self, path: &Path) -> Result<(), String> {
494 write_whole(path, &toml::to_string(self).map_err(|e| e.to_string())?)
495 }
496}
497
498pub fn held_off_of(before: Option<&Press>, state: State) -> i64 {
503 match before {
504 Some(p) if state == State::Reachable && !p.gone => match p.state {
505 State::Usual => p.at,
506 State::Reachable => p.held_off,
507 State::Dnd => 0,
508 },
509 _ => 0,
510 }
511}
512
513pub fn change_own(path: &Path, here: &str, change: impl FnOnce(&mut Switch) -> bool) -> Result<Switch, String> {
518 crate::filelock::with_lock(path, || {
519 let mut switch = Switch::read(path)?;
520 if !here.is_empty() && change(&mut switch) {
521 switch.save(path)?;
523 }
524 Ok(switch)
525 })
526}
527
528fn write_whole(path: &Path, text: &str) -> Result<(), String> {
530 let fail = |e: std::io::Error| format!("{}: {e}", path.display());
531 if let Some(parent) = path.parent() {
532 std::fs::create_dir_all(parent).map_err(fail)?;
533 }
534 let name = path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
535 let fresh = path.with_file_name(format!(".{name}.{}.new", std::process::id()));
536 std::fs::write(&fresh, text).map_err(fail)?;
537 std::fs::rename(&fresh, path).map_err(|e| {
538 let _ = std::fs::remove_file(&fresh);
539 fail(e)
540 })
541}
542
543#[derive(Debug, Clone, Default, PartialEq, Eq)]
547pub struct Sources {
548 pub paused: Option<i64>,
550 pub free: Option<(i64, i64)>,
552 pub sleep: Option<(i64, i64)>,
554 pub focus: Option<(i64, i64)>,
556 pub pause_pressed: Option<i64>,
559 pub pause_saw: Option<i64>,
563 pub nights: Vec<(i64, i64)>,
566}
567
568pub fn sources_at(overrides: &crate::quiet::Overrides, blocks: &crate::quiet::Blocks, now: &jiff::Zoned) -> Sources {
573 let stamp = now.timestamp().as_second();
574 let free = overrides.free_from().map(|since| (since, crate::pause::free_until(since, blocks, now.time_zone())));
575 let sleep = blocks.at(stamp, &["sleep", "nap"]).map(|k| (k.start, k.end));
576 let focus = crate::timelog::running().and_then(|r| focus(&r, stamp));
577 let nights = blocks.kept.iter().filter(|k| k.kind == "sleep").map(|k| (k.start, k.end)).collect();
578 Sources { paused: overrides.paused_from(), free, sleep, focus, pause_pressed: overrides.paused_since, pause_saw: overrides.pause_saw, nights }
579}
580
581pub fn sources_from_files(now: &jiff::Zoned) -> Sources {
584 sources_at(&crate::quiet::Overrides::load(&crate::quiet::Overrides::default_path()), &crate::quiet::Blocks::read(now, &[]), now)
585}
586
587pub fn gone_path() -> PathBuf {
591 state_dir().join("share").join(GONE_FILE)
592}
593
594pub fn gone_devices() -> std::collections::BTreeSet<String> {
595 std::fs::read(gone_path()).ok().and_then(|bytes| serde_json::from_slice(&bytes).ok()).unwrap_or_default()
596}
597
598pub fn read_switch(here: &str) -> Switch {
604 let gone = gone_devices();
605 Switch::load(&Switch::default_path()).counted(here, &|id| !gone.contains(id)).as_of(now_ms())
606}
607
608fn now_ms() -> i64 {
609 std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| i64::try_from(d.as_millis()).unwrap_or(i64::MAX))
610}
611
612impl Switch {
613 pub fn as_of(mut self, now_ms: i64) -> Switch {
618 let bound = now_ms.saturating_add(CLOCK_AHEAD_MS);
619 for d in self.device.values_mut() {
620 if d.pressed > bound {
621 d.pressed = 0;
622 }
623 let mut calls = crate::calls::part_of(d);
624 if calls.pressed > bound {
625 calls.pressed = 0;
626 calls.on = false;
627 crate::calls::set_part(d, calls);
628 }
629 }
630 self
631 }
632}
633
634pub fn focus(running: &crate::timelog::Running, now: i64) -> Option<(i64, i64)> {
638 if running.paused_at.is_some() || running.start <= 0 {
639 return None;
640 }
641 let lasts = if running.planned > 0 { i64::from(running.planned) * 60 + FOCUS_GRACE } else { FOCUS_OPEN };
642 let end = running.start + running.paused.max(0) + lasts;
643 (now < end).then_some((running.start, end))
644}
645
646#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
648pub struct Held {
649 pub why: Why,
650 pub since: i64,
652 pub until: Option<i64>,
655 pub from: String,
657}
658
659#[derive(Debug, Clone, Default, PartialEq, Eq)]
662pub struct Now {
663 pub holds: Vec<Held>,
664 pub reachable: Option<Reach>,
667}
668
669#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
671pub struct Reach {
672 pub since: i64,
674 pub until: Option<i64>,
676 pub from: String,
678 pub open: bool,
680}
681
682impl Now {
683 pub fn on(&self) -> bool {
684 !self.holds.is_empty()
685 }
686
687 pub fn why(&self) -> Option<Why> {
689 self.holds.first().map(|h| h.why)
690 }
691
692 pub fn has(&self, why: Why) -> bool {
693 self.holds.iter().any(|h| h.why == why)
694 }
695
696 pub fn global(&self) -> bool {
698 self.holds.iter().any(|h| h.why.global())
699 }
700
701 pub fn until(&self) -> Option<i64> {
704 if self.holds.is_empty() || self.holds.iter().any(|h| h.until.is_none() || h.why == Why::Focus) {
705 return None;
706 }
707 self.holds.iter().filter_map(|h| h.until).max()
708 }
709
710 pub fn next_end(&self) -> Option<i64> {
712 self.holds.iter().filter_map(|h| h.until).min()
713 }
714
715 pub fn global_why(&self) -> Option<Why> {
717 self.holds.iter().map(|h| h.why).find(|w| w.global())
718 }
719
720 pub fn gates(&self) -> bool {
724 self.has(Why::Focus) || self.has(Why::Manual)
725 }
726}
727
728pub fn heard(holds: bool, system_on: bool) -> Option<bool> {
735 (holds != system_on).then_some(system_on)
736}
737
738pub fn held_by_system(now: &Now, press: Option<&Press>, here: &str) -> bool {
745 let mut global = now.holds.iter().filter(|h| h.why.global());
746 let only_manual = matches!((global.next(), global.next()), (Some(h), None) if h.why == Why::Manual);
747 only_manual && press.is_some_and(|p| p.on && p.from == here && p.via == VIA_SYSTEM)
748}
749
750pub fn now(settings: &DndSettings, sources: &Sources, press: Option<&Press>, now: i64) -> Now {
755 let reachable = press.filter(|p| p.state == State::Reachable && !p.gone).and_then(|p| {
758 let end = reachable_end(p.at / 1000, (p.until > 0).then_some(p.until / 1000), &sources.nights);
759 let paused_after = sources.pause_pressed.is_some_and(|at| at.saturating_mul(1000) >= p.at) || sources.pause_saw.is_some_and(|saw| saw >= p.at);
760 (now < end && sources.paused.is_none() && !paused_after).then(|| Reach { since: p.at / 1000, until: Some(end), from: p.from.clone(), open: p.open })
761 });
762 let pressed_off_after = |since: i64| {
767 press.is_some_and(|p| {
768 let off = match p.state {
769 _ if p.gone => 0,
770 State::Reachable => p.held_off,
771 State::Usual => p.at,
772 State::Dnd => 0,
773 };
774 off > since.saturating_mul(1000)
775 })
776 };
777 let mut holds = Vec::new();
778 let mut add = |why: Why, since: i64, until: Option<i64>| {
779 if reachable.is_some() && matches!(why, Why::FreeTime | Why::Sleep | Why::Focus) {
781 return;
782 }
783 if !pressed_off_after(since) {
784 holds.push(Held { why, since, until, from: String::new() });
785 }
786 };
787 if settings.pauses {
788 if let Some(since) = sources.paused {
789 add(Why::Paused, since, None);
790 } else if let Some((since, end)) = sources.free.filter(|(since, end)| *since <= now && now < *end) {
791 add(Why::FreeTime, since, Some(end));
792 }
793 }
794 if settings.sleep
795 && let Some((since, until)) = sources.sleep.filter(|(_, until)| now < *until)
796 {
797 add(Why::Sleep, since, Some(until));
798 }
799 if settings.focus
800 && let Some((since, end)) = sources.focus.filter(|(_, end)| now < *end)
801 {
802 add(Why::Focus, since, Some(end));
803 }
804 if let Some(p) = press.filter(|p| p.on) {
805 let until = (p.until > 0).then_some(p.until / 1000);
806 if until.is_none_or(|u| now < u) {
807 holds.push(Held { why: Why::Manual, since: p.at / 1000, until, from: p.from.clone() });
808 }
809 }
810 holds.sort_by_key(|h| h.why);
811 Now { holds, reachable }
812}
813
814#[derive(Debug, Clone, Copy, PartialEq, Eq)]
816pub enum Length {
817 Minutes(i64),
819 HoursEnd,
821 Tonight,
823 Off,
825}
826
827pub fn reachable_until(length: Length, now: i64, hours_end: Option<i64>, night: Option<i64>, waking: Option<i64>) -> Option<(i64, bool)> {
834 let latest = waking.or(night);
836 let end = match (length, waking) {
837 (Length::Minutes(minutes), _) => Some(now.saturating_add(minutes.max(1).saturating_mul(60))),
838 (Length::HoursEnd, _) => hours_end,
839 (Length::Tonight | Length::Off, Some(waking)) => Some(waking),
840 (Length::Tonight | Length::Off, None) => night,
841 }?;
842 let end = latest.map_or(end, |latest| end.min(latest));
843 (end > now).then_some((end, length == Length::Off))
844}
845
846pub fn reachable_end(at: i64, until: Option<i64>, nights: &[(i64, i64)]) -> i64 {
853 let night = nights.iter().find(|(start, end)| *start <= at && at < *end).map(|(_, end)| *end).or_else(|| nights.iter().map(|(start, _)| *start).filter(|start| *start > at).min());
854 let most = at.saturating_add(REACHABLE_MOST);
855 [until, night].into_iter().flatten().fold(most, i64::min)
856}
857
858#[derive(Debug, Clone, PartialEq, Eq)]
862pub enum Follows {
863 Silenced,
865 Cannot(String),
867 Behind,
870 Older,
873 Closed,
876}
877
878pub fn others(switch: &Switch, here: &str, heard: &[(String, i64)], now: i64, since: i64, registered: &dyn Fn(&str) -> bool) -> Vec<(String, Follows)> {
887 let live = |at: i64| now - at <= LIVE_DAYS * 86_400;
888 let mut ids: Vec<String> = heard.iter().filter(|(id, at)| id != here && live(*at)).map(|(id, _)| id.clone()).collect();
889 ids.extend(switch.device.iter().filter(|(id, d)| *id != here && d.at > 0 && live(d.at / 1000)).map(|(id, _)| id.clone()));
891 ids.sort();
892 ids.dedup();
893 ids.into_iter()
894 .map(|id| {
895 let news = heard.iter().filter(|(other, _)| *other == id).map(|(_, at)| *at).chain(switch.device.get(&id).map(|d| d.at / 1000)).max().unwrap_or(0);
897 let follows = match switch.device.get(&id) {
898 Some(d) if d.closed => Follows::Closed,
899 Some(d) if d.why.is_empty() || d.at.saturating_add(MARGIN_MS) < since => Follows::Behind,
900 Some(d) if d.silenced && d.kind == "computer" && now - news > COMPUTER_QUIET => Follows::Behind,
901 Some(d) if d.silenced => Follows::Silenced,
902 Some(d) => Follows::Cannot(d.line.clone()),
903 None if !registered(&id) => Follows::Older,
904 None => Follows::Behind,
905 };
906 (id, follows)
907 })
908 .collect()
909}
910
911#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
913pub struct Said {
914 pub line: String,
916 pub why: String,
918 pub details: Vec<String>,
920 pub everywhere: bool,
922 pub here: bool,
924}
925
926#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
928pub struct Here<'a> {
929 pub silenced: bool,
931 pub line: &'a str,
933 pub turned_off: bool,
936}
937
938pub fn said(now_: &Now, here: Here<'_>, others: &[(String, Follows)], until: &str, name_of: &dyn Fn(&str) -> String, tr: &crate::i18n::Translator) -> Said {
944 let (here_silenced, here_line) = (here.silenced, here.line);
945 let Some(why) = now_.why() else { return Said::default() };
946 let mut args = crate::i18n::args();
947 args.set("until", until.to_string());
948 let with_until = |key: &str| if until.is_empty() { tr.text(key, None) } else { tr.text(&format!("{key}-until"), Some(&args)) };
949 let following = others.iter().filter(|(_, f)| *f == Follows::Silenced).count();
950 let everywhere = here_silenced && following == others.len();
951 let not_following: Vec<&Follows> = others.iter().map(|(_, f)| f).filter(|f| **f != Follows::Silenced).collect();
954 let waiting = !not_following.is_empty() && not_following.iter().all(|f| **f == Follows::Behind);
955 let line = if here.turned_off {
956 with_until("dnd-turned-off-here")
957 } else if !here_silenced {
958 if following > 0 { with_until("dnd-elsewhere") } else { with_until("dnd-own-only") }
959 } else if others.is_empty() {
960 with_until("dnd-here-only")
961 } else if everywhere {
962 with_until("dnd-everywhere")
963 } else if waiting {
964 with_until("dnd-waiting")
965 } else if following == 0 {
966 with_until("dnd-here-only")
967 } else {
968 with_until("dnd-not-everywhere")
969 };
970 let device = |key: &str, place: String, line: Option<&str>| {
972 let mut args = crate::i18n::args();
973 args.set("device", place);
974 if let Some(line) = line {
975 args.set("line", line.to_string());
976 }
977 tr.text(key, Some(&args))
978 };
979 let on = |id: &str| {
980 let mut args = crate::i18n::args();
981 args.set("name", name_of(id));
982 tr.text("dnd-device-on", Some(&args))
983 };
984 let here_name = tr.text("dnd-device-here", None);
985 let mut details = vec![if here_silenced { device("dnd-device-silenced", here_name, None) } else { device("dnd-device-cannot", here_name, Some(here_line)) }];
986 for (id, follows) in others {
987 details.push(match follows {
988 Follows::Silenced => device("dnd-device-silenced", on(id), None),
989 Follows::Cannot(words) if !words.is_empty() => device("dnd-device-cannot", on(id), Some(words)),
990 Follows::Cannot(_) => device("dnd-device-unsilenced", on(id), None),
991 Follows::Behind => device("dnd-device-behind", on(id), None),
992 Follows::Older => device("dnd-device-older", on(id), None),
993 Follows::Closed => device("dnd-device-closed", on(id), None),
994 });
995 }
996 let why_words = match why {
997 Why::Manual => {
998 let from = now_.holds.iter().find(|h| h.why == Why::Manual).map(|h| h.from.clone()).unwrap_or_default();
999 if from.is_empty() {
1000 tr.text("dnd-why-manual", None)
1001 } else {
1002 let mut args = crate::i18n::args();
1003 args.set("device", name_of(&from));
1004 tr.text("dnd-why-manual-from", Some(&args))
1005 }
1006 }
1007 other => tr.text(&format!("dnd-why-{}", other.id()), None),
1008 };
1009 Said { line, why: why_words, details, everywhere, here: here_silenced }
1010}
1011
1012#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1016pub struct Person {
1017 pub id: String,
1019 #[serde(default)]
1020 pub name: String,
1021 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1022 pub phones: Vec<String>,
1023 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1024 pub emails: Vec<String>,
1025 #[serde(default, skip_serializing_if = "String::is_empty")]
1027 pub contact: String,
1028 #[serde(flatten)]
1031 pub other: toml::Table,
1032}
1033
1034#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1036pub struct People {
1037 #[serde(default, rename = "person", skip_serializing_if = "Vec::is_empty")]
1038 pub people: Vec<Person>,
1039 #[serde(flatten)]
1041 pub other: toml::Table,
1042}
1043
1044fn address_key(text: &str) -> String {
1046 let text = text.trim();
1047 let inner = text.rsplit_once('<').map_or(text, |(_, rest)| rest.trim_end_matches('>'));
1048 inner.trim().trim_start_matches("mailto:").to_lowercase()
1049}
1050
1051pub fn new_id() -> String {
1053 use std::hash::{BuildHasher, Hasher};
1054 use std::sync::atomic::{AtomicU64, Ordering};
1055 static COUNT: AtomicU64 = AtomicU64::new(0);
1056 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
1057 let mut hasher = std::collections::hash_map::RandomState::new().build_hasher();
1058 hasher.write_u128(now.as_nanos());
1059 hasher.write_u64(COUNT.fetch_add(1, Ordering::Relaxed));
1060 hasher.write_u32(std::process::id());
1061 format!("{:x}{:016x}", now.as_secs(), hasher.finish())
1062}
1063
1064impl People {
1065 pub fn default_path() -> PathBuf {
1066 config_dir().join(PEOPLE_FILE)
1067 }
1068
1069 pub fn read(path: &Path) -> Result<People, String> {
1073 match std::fs::read_to_string(path) {
1074 Ok(text) => toml::from_str(&text).map_err(|e| format!("{}: {e}", path.display())),
1075 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(People::default()),
1076 Err(e) => Err(format!("{}: {e}", path.display())),
1077 }
1078 }
1079
1080 pub fn load(path: &Path) -> People {
1082 People::read(path).unwrap_or_default()
1083 }
1084
1085 pub fn save(&self, path: &Path) -> Result<(), String> {
1086 write_whole(path, &toml::to_string(self).map_err(|e| e.to_string())?)
1087 }
1088
1089 pub fn admits_address(&self, address: &str) -> bool {
1091 let key = address_key(address);
1092 !key.is_empty() && key.contains('@') && self.people.iter().any(|p| p.emails.iter().any(|e| address_key(e) == key))
1093 }
1094
1095 pub fn admits_number(&self, number: &str, region: Option<&crate::phones::Region>) -> bool {
1098 let key = crate::phones::key(number, region);
1099 crate::phones::is_whole(&key) && self.people.iter().any(|p| p.phones.iter().any(|n| crate::phones::key(n, region) == key))
1100 }
1101
1102 pub fn add(&mut self, mut person: Person, region: Option<&crate::phones::Region>) -> String {
1105 person.emails.retain(|e| !address_key(e).is_empty());
1106 person.phones.retain(|n| !n.trim().is_empty());
1107 let same = |p: &Person| {
1108 (!person.contact.is_empty() && p.contact == person.contact)
1109 || person.emails.iter().any(|e| p.emails.iter().any(|x| address_key(x) == address_key(e)))
1110 || person.phones.iter().any(|n| {
1111 let key = crate::phones::key(n, region);
1112 crate::phones::is_whole(&key) && p.phones.iter().any(|x| crate::phones::key(x, region) == key)
1113 })
1114 };
1115 if let Some(known) = self.people.iter_mut().find(|p| same(p)) {
1116 for email in person.emails {
1117 if !known.emails.iter().any(|x| address_key(x) == address_key(&email)) {
1118 known.emails.push(email.trim().to_string());
1119 }
1120 }
1121 for number in person.phones {
1122 let key = crate::phones::key(&number, region);
1123 if !known.phones.iter().any(|x| crate::phones::key(x, region) == key) {
1124 known.phones.push(number.trim().to_string());
1125 }
1126 }
1127 if known.name.trim().is_empty() {
1128 known.name = person.name;
1129 }
1130 if known.contact.is_empty() {
1131 known.contact = person.contact;
1132 }
1133 return known.id.clone();
1134 }
1135 if person.id.is_empty() {
1136 person.id = new_id();
1137 }
1138 person.emails = person.emails.iter().map(|e| e.trim().to_string()).collect();
1139 person.phones = person.phones.iter().map(|n| n.trim().to_string()).collect();
1140 let id = person.id.clone();
1141 self.people.push(person);
1142 id
1143 }
1144
1145 pub fn remove(&mut self, id: &str) -> bool {
1147 let before = self.people.len();
1148 self.people.retain(|p| p.id != id);
1149 self.people.len() != before
1150 }
1151}
1152
1153pub fn take_off(people: &mut People, addresses: &[String], numbers: &[String], card: &str, region: Option<&crate::phones::Region>) -> bool {
1159 let keys: Vec<String> = numbers.iter().map(|n| crate::phones::key(n, region)).filter(|k| crate::phones::is_whole(k)).collect();
1160 let card = card.trim();
1161 let mut gone: Vec<String> = Vec::new();
1162 let mut changed = false;
1163 for person in &mut people.people {
1164 let before = (person.emails.len(), person.phones.len());
1165 person.emails.retain(|e| !addresses.iter().any(|a| address_key(a) == address_key(e)));
1166 person.phones.retain(|n| !keys.contains(&crate::phones::key(n, region)));
1167 let whole = !card.is_empty() && person.contact == card;
1168 if whole || before != (person.emails.len(), person.phones.len()) {
1169 changed = true;
1170 if whole || (person.emails.is_empty() && person.phones.is_empty()) {
1171 gone.push(person.id.clone());
1172 }
1173 }
1174 }
1175 people.people.retain(|p| !gone.contains(&p.id));
1176 changed
1177}
1178
1179pub fn change_people(path: &Path, change: impl FnOnce(&mut People)) -> Result<People, String> {
1182 crate::filelock::with_lock(path, || {
1183 let mut people = People::read(path)?;
1184 let before = people.clone();
1185 change(&mut people);
1186 if people != before {
1187 people.save(path)?;
1188 }
1189 Ok(people)
1190 })
1191}
1192
1193pub fn from_contact(contact: &crate::contacts::Contact) -> Person {
1195 Person {
1196 id: String::new(),
1197 name: contact.name.trim().to_string(),
1198 phones: contact.phones.iter().map(|p| p.value.trim().to_string()).filter(|v| !v.is_empty()).collect(),
1199 emails: contact.emails.iter().map(|e| e.value.trim().to_string()).filter(|v| !v.is_empty()).collect(),
1200 contact: contact.uid.clone(),
1201 other: toml::Table::new(),
1202 }
1203}
1204
1205pub fn from_safe(entries: &[String], contacts: &[crate::contacts::Contact]) -> (Vec<Person>, usize) {
1211 let (mut found, mut patterns) = (Vec::new(), 0);
1212 for entry in entries {
1213 let entry = entry.trim();
1214 if let Some(category) = crate::porch::category_of(entry) {
1215 found.extend(contacts.iter().filter(|c| crate::contacts::in_category(c, category)).map(from_contact));
1216 } else if let Some(uid) = crate::porch::card_of(entry) {
1217 found.extend(contacts.iter().filter(|c| c.uid.trim() == uid).map(from_contact));
1218 } else if let Some(number) = crate::porch::number_of(entry).filter(|n| !n.contains('*')) {
1219 found.push(Person { phones: vec![number.to_string()], ..Person::default() });
1220 } else if entry.starts_with('@') || !entry.contains('@') || entry.contains('*') {
1221 patterns += 1;
1222 } else {
1223 match crate::contacts::by_address(contacts, entry) {
1224 Some(card) => found.push(from_contact(card)),
1225 None => found.push(Person { name: String::new(), emails: vec![entry.to_string()], ..Person::default() }),
1226 }
1227 }
1228 }
1229 (found, patterns)
1230}
1231
1232#[cfg(test)]
1233mod tests {
1234 use super::*;
1235 use crate::i18n::Translator;
1236
1237 fn device(pressed: i64, on: bool, seen: i64) -> Device {
1238 Device { pressed, on, seen, ..Device::default() }
1239 }
1240
1241 fn at(silenced: bool, line: &str) -> Here<'_> {
1243 Here { silenced, line, turned_off: false }
1244 }
1245
1246 fn switch(devices: &[(&str, Device)]) -> Switch {
1247 Switch { device: devices.iter().map(|(id, d)| (id.to_string(), d.clone())).collect(), ..Switch::default() }
1248 }
1249
1250 #[test]
1251 fn the_latest_press_wins_whatever_device_made_it() {
1252 let s = switch(&[("desk", device(1_000_000, true, 0)), ("phone", device(1_200_000, false, 1_000_000))]);
1254 let latest = s.latest().unwrap();
1255 assert!(!latest.on && latest.from == "phone");
1256 let s = switch(&[("desk", device(1_300_000, true, 1_200_000)), ("phone", device(1_200_000, false, 1_000_000))]);
1258 assert!(s.latest().unwrap().on);
1259 assert_eq!(Switch::default().latest(), None);
1261 }
1262
1263 #[test]
1264 fn a_press_after_seeing_another_comes_after_it_whatever_the_clocks() {
1265 let mut s = switch(&[("phone", device(5_000_000, true, 0))]);
1266 let stamp = s.press("desk", false, 0, 5_000_000 - 120_000);
1269 assert_eq!(stamp, 5_000_001);
1270 assert_eq!(s.device["desk"].seen, 5_000_000);
1271 let latest = s.latest().unwrap();
1272 assert!(!latest.on && latest.from == "desk");
1273 }
1274
1275 #[test]
1276 fn two_presses_that_did_not_see_each_other_within_a_minute_turn_it_on() {
1277 let s = switch(&[("desk", device(10_000_000, true, 0)), ("phone", device(10_040_000, false, 0))]);
1279 let latest = s.latest().unwrap();
1280 assert!(latest.on && latest.from == "desk");
1281 let s = switch(&[("desk", device(10_000_000, true, 0)), ("phone", device(10_120_000, false, 0))]);
1283 assert!(!s.latest().unwrap().on);
1284 let s = switch(&[("desk", device(10_000_000, true, 0)), ("phone", device(10_040_000, false, 10_000_000))]);
1286 assert!(!s.latest().unwrap().on);
1287 let s = switch(&[("desk", device(10_000_000, false, 0)), ("phone", device(10_040_000, true, 0))]);
1289 assert!(s.latest().unwrap().on);
1290 }
1291
1292 fn pressed(pressed: i64, state: State, seen: i64) -> Device {
1293 Device { pressed, on: state == State::Dnd, mode: state.id().into(), seen, ..Device::default() }
1294 }
1295
1296 fn reachable(at_s: i64, until_s: i64) -> Press {
1297 Press { at: at_s * 1000, on: false, until: until_s * 1000, from: "phone".into(), via: String::new(), state: State::Reachable, open: false, held_off: 0, gone: false }
1298 }
1299
1300 #[test]
1305 fn the_quieter_of_three_states_wins_within_the_minute() {
1306 for a in State::ALL {
1307 for b in State::ALL {
1308 let s = switch(&[("desk", pressed(10_000_000, a, 0)), ("phone", pressed(10_030_000, b, 0))]);
1309 assert_eq!(s.latest().unwrap().state, a.max(b), "{a:?} then {b:?}, unseen");
1310 let seen = switch(&[("desk", pressed(10_000_000, a, 0)), ("phone", pressed(10_030_000, b, 10_000_000))]);
1311 assert_eq!(seen.latest().unwrap().state, b, "{a:?} then {b:?}, seen");
1312 let far = switch(&[("desk", pressed(10_000_000, a, 0)), ("phone", pressed(10_090_000, b, 0))]);
1313 assert_eq!(far.latest().unwrap().state, b, "{a:?} then {b:?}, past the minute");
1314 for c in State::ALL {
1315 let three = switch(&[("desk", pressed(10_000_000, a, 0)), ("laptop", pressed(10_020_000, b, 0)), ("phone", pressed(10_040_000, c, 0))]);
1316 let latest = three.latest().unwrap();
1317 assert_eq!(latest.state, a.max(b).max(c), "{a:?}, {b:?}, {c:?}");
1318 assert_eq!(latest.on, latest.state == State::Dnd);
1319 }
1320 }
1321 }
1322 let s = switch(&[("desk", Device { until: 12_000_000, ..pressed(10_000_000, State::Dnd, 0) }), ("phone", pressed(10_030_000, State::Reachable, 0))]);
1324 let latest = s.latest().unwrap();
1325 assert_eq!((latest.from.as_str(), latest.until, latest.state), ("desk", 12_000_000, State::Dnd));
1326 }
1327
1328 #[test]
1333 fn an_older_press_reads_by_its_on_and_reachable_writes_off() {
1334 let old: Switch = toml::from_str("[device.a]\npressed = 5\non = true\n[device.b]\npressed = 3\non = false\nmode = \"later-state\"\n").unwrap();
1335 assert_eq!((old.device["a"].state(), old.device["b"].state()), (State::Dnd, State::Usual));
1336 let mut s = Switch::default();
1337 s.press_state("phone", State::Reachable, 7_200_000, true, 3_600_000, "");
1338 let own = &s.device["phone"];
1339 assert_eq!((own.on, own.mode.as_str(), own.until, own.open), (false, "reachable", 7_200_000, true));
1340 let text = toml::to_string(&s).unwrap();
1341 assert!(text.contains("mode = \"reachable\"") && text.contains("open = true"), "{text}");
1342 let back: Switch = toml::from_str(&text).unwrap();
1343 assert_eq!((back.latest().unwrap().state, back.latest().unwrap().open), (State::Reachable, true));
1344 s.press_state("phone", State::Usual, 9_000_000, true, 3_700_000, "");
1346 assert_eq!((s.device["phone"].until, s.device["phone"].open, s.device["phone"].mode.as_str()), (0, false, "usual"));
1347 s.press("desk", true, 0, 3_800_000);
1348 assert_eq!(s.device["desk"].mode, "dnd");
1349 }
1350
1351 #[test]
1356 fn reachable_holds_off_the_times_and_a_pause_ends_it() {
1357 let settings = DndSettings { sleep: true, focus: true, pauses: true, ..DndSettings::default() };
1358 let press = reachable(1_000, 8_000);
1359 let before = Sources { sleep: Some((500, 9_000)), focus: Some((600, 9_000)), free: Some((700, 9_000)), ..Sources::default() };
1360 let held = now(&settings, &before, Some(&press), 2_000);
1361 assert!(!held.on() && held.reachable.as_ref().is_some_and(|r| r.since == 1_000 && r.until == Some(8_000)), "{held:?}");
1362 let during = Sources { sleep: Some((1_500, 9_000)), focus: Some((1_600, 9_000)), free: Some((1_700, 9_000)), ..Sources::default() };
1363 assert!(!now(&settings, &during, Some(&press), 2_000).on());
1364 let after = now(&settings, &during, Some(&press), 8_000);
1366 assert!(after.reachable.is_none() && after.has(Why::Sleep) && after.has(Why::Focus), "{after:?}");
1367 let again = now(&settings, &before, Some(&press), 8_000);
1368 assert!(again.has(Why::Sleep) && again.has(Why::Focus) && again.has(Why::FreeTime), "{again:?}");
1369 let paused = Sources { paused: Some(500), pause_pressed: Some(500), ..Sources::default() };
1371 let held = now(&settings, &paused, Some(&press), 2_000);
1372 assert!(held.reachable.is_none() && held.has(Why::Paused));
1373 let over = Sources { pause_pressed: Some(1_500), ..Sources::default() };
1375 assert!(now(&settings, &over, Some(&press), 2_000).reachable.is_none());
1376 let earlier = Sources { pause_pressed: Some(900), ..Sources::default() };
1378 assert!(now(&settings, &earlier, Some(&press), 2_000).reachable.is_some());
1379 let usual = Press { state: State::Usual, ..press.clone() };
1381 assert!(!now(&settings, &Sources { paused: Some(500), ..Sources::default() }, Some(&usual), 2_000).on());
1382 assert_eq!((heard(false, true), heard(false, false)), (Some(true), None));
1384 }
1385
1386 #[test]
1391 fn reachable_ends_as_its_length_says() {
1392 let (now_s, hours_end, night, waking) = (36_000, 61_200, 81_000, 111_600);
1393 assert_eq!(reachable_until(Length::Minutes(30), now_s, Some(hours_end), Some(night), None), Some((now_s + 1_800, false)));
1394 assert_eq!(reachable_until(Length::Minutes(120), now_s, Some(hours_end), Some(night), None), Some((now_s + 7_200, false)));
1395 assert_eq!(reachable_until(Length::HoursEnd, now_s, Some(hours_end), Some(night), None), Some((hours_end, false)));
1396 assert_eq!(reachable_until(Length::Tonight, now_s, Some(hours_end), Some(night), None), Some((night, false)));
1397 assert_eq!(reachable_until(Length::Off, now_s, Some(hours_end), Some(night), None), Some((night, true)), "by day: at the latest the night's start");
1398 let asleep = 90_000;
1399 assert_eq!(reachable_until(Length::Off, asleep, None, Some(night + 86_400), Some(waking)), Some((waking, true)), "at night: until waking");
1400 assert_eq!(reachable_until(Length::Tonight, asleep, None, Some(night + 86_400), Some(waking)), Some((waking, false)));
1401 assert_eq!(reachable_until(Length::Off, now_s, None, None, None), None, "no night known: no end, not offered");
1402 assert_eq!(reachable_until(Length::HoursEnd, now_s, Some(now_s - 1), Some(night), None), None);
1403 }
1404
1405 #[test]
1410 fn releases_are_asked_by_device_and_the_gone_release_nothing() {
1411 let mut s = switch(&[("phone", pressed(1_000_000, State::Usual, 0))]);
1412 assert!(s.release_stamps().is_empty());
1413 let stamp = s.release("desk", 900_000);
1414 assert_eq!((stamp, s.release_stamps()), (1_000_001, vec![("desk".to_string(), 1_000_001)]), "after every press known, whatever the clock");
1415 s.press_state("laptop", State::Reachable, 9_000_000, false, 2_000_000, "");
1416 assert_eq!(s.release_stamps(), vec![("desk".to_string(), 1_000_001), (format!("laptop{REACHABLE_RELEASE}"), 2_000_000)]);
1417 s.device.insert("phone".into(), pressed(1_970_000, State::Dnd, 0));
1419 assert_eq!((s.latest().unwrap().state, s.release_stamps()), (State::Dnd, vec![("desk".to_string(), 1_000_001)]));
1420 assert!(s.counted("phone", &|id| id != "desk").release_stamps().is_empty());
1422 assert!(s.counted("phone", &|_| false).device.contains_key("phone"), "this device always");
1423 }
1424
1425 #[test]
1433 fn a_gone_device_s_press_still_ends_what_came_before_it() {
1434 const DAY: i64 = 86_400_000;
1435 let t = 1_790_000_000_000;
1436 let settings = DndSettings { focus: true, ..DndSettings::default() };
1437 let s = switch(&[("phone", pressed(t - 3 * DAY, State::Dnd, 0)), ("old-laptop", pressed(t - 2 * DAY, State::Usual, t - 3 * DAY))]);
1438 let gone = s.counted("phone", &|id| id != "old-laptop");
1439 let latest = gone.latest().unwrap();
1440 assert_eq!((latest.from.as_str(), latest.state, latest.gone), ("old-laptop", State::Usual, true));
1441 assert!(!now(&settings, &Sources::default(), Some(&latest), t / 1000).on(), "no Do not disturb back");
1442 for state in [State::Reachable, State::Dnd] {
1444 let mut late = switch(&[("phone", pressed(t - 3 * DAY, State::Usual, 0)), ("old-laptop", Device { until: t + DAY, release: t, at: t, ..pressed(t - MARGIN_MS, state, t - 3 * DAY) })]);
1445 late.device.get_mut("old-laptop").unwrap().open = true;
1446 let read = late.counted("phone", &|id| id != "old-laptop");
1447 let p = read.latest().unwrap();
1448 let focus = Sources { focus: Some((t / 1000 - 3600, t / 1000 + 3600)), ..Sources::default() };
1449 let held = now(&settings, &focus, Some(&p), t / 1000);
1450 assert!(held.reachable.is_none() && held.has(Why::Focus) && !held.has(Why::Manual), "{state:?}: {held:?}");
1451 assert!(read.release_stamps().is_empty());
1452 assert!(others(&read, "phone", &[], t / 1000, t, &|_| true).is_empty(), "no device that follows");
1453 }
1454 }
1455
1456 #[test]
1461 fn reachable_never_outlives_its_latest_end() {
1462 let settings = DndSettings { sleep: true, focus: true, ..DndSettings::default() };
1463 let night = (90_000, 90_000 + 8 * 3600);
1464 let nights = vec![night, (night.0 + 86_400, night.1 + 86_400)];
1465 let at = night.0 - 45 * 60;
1467 assert_eq!(reachable_until(Length::Minutes(120), at, None, Some(night.0), None), Some((night.0, false)));
1468 assert_eq!(reachable_until(Length::Minutes(120), night.1 - 1800, None, Some(night.0 + 86_400), Some(night.1)), Some((night.1, false)));
1470 let older = reachable(at, at + 7200);
1472 let asleep = Sources { sleep: Some(night), nights: nights.clone(), ..Sources::default() };
1473 let held = now(&settings, &asleep, Some(&older), night.0 + 1800);
1474 assert!(held.reachable.is_none() && held.has(Why::Sleep), "{held:?}");
1475 assert_eq!(now(&settings, &Sources { nights: nights.clone(), ..Sources::default() }, Some(&older), at + 60).reachable.and_then(|r| r.until), Some(night.0), "its end said as the night's start");
1476 let open = reachable(at, 0);
1478 assert!(now(&settings, &asleep, Some(&open), night.0 + 60).reachable.is_none());
1479 assert!(now(&settings, &Sources::default(), Some(&open), at + REACHABLE_MOST - 1).reachable.is_some());
1480 assert!(now(&settings, &Sources::default(), Some(&open), at + REACHABLE_MOST).reachable.is_none());
1481 let far = reachable(at, at + 365 * 86_400);
1483 let tenth = (night.0 + 10 * 86_400, night.1 + 10 * 86_400);
1484 let held = now(&settings, &Sources { sleep: Some(tenth), nights: vec![tenth], ..Sources::default() }, Some(&far), tenth.0 + 60);
1485 assert!(held.reachable.is_none() && held.on(), "{held:?}");
1486 assert_eq!(reachable_end(at, Some(i64::MAX), &[]), at + REACHABLE_MOST, "never overflows");
1487 assert_eq!(reachable_end(i64::MAX - 10, None, &[]), i64::MAX);
1488 }
1489
1490 #[test]
1497 fn reachable_s_end_keeps_what_as_usual_held_off() {
1498 let t = 1_790_000_000_000;
1499 const MIN: i64 = 60_000;
1500 let settings = DndSettings { focus: true, sleep: true, ..DndSettings::default() };
1501 let focus = Sources { focus: Some((t / 1000, t / 1000 + 4 * 3600)), ..Sources::default() };
1502 for other in ["phone", "desk"] {
1503 let mut s = switch(&[("phone", pressed(t + 30 * MIN, State::Usual, 0))]);
1504 let at = |s: &Switch, m: i64| now(&settings, &focus, s.latest().as_ref(), (t + m * MIN) / 1000);
1505 assert!(!at(&s, 45).on());
1506 s.press_state(other, State::Reachable, t + 90 * MIN, false, t + 60 * MIN, "");
1507 assert_eq!(s.device[other].held_off, t + 30 * MIN);
1508 let during = at(&s, 75);
1509 assert!(!during.on() && during.reachable.is_some());
1510 assert!(!at(&s, 95).on(), "Reachable over, on {other}: the session stays held off");
1511 s.press_state(other, State::Reachable, t + 120 * MIN, false, t + 100 * MIN, "");
1513 assert_eq!(s.device[other].held_off, t + 30 * MIN);
1514 }
1515 let night = Sources { sleep: Some((t / 1000 - 1800, t / 1000 + 8 * 3600)), nights: vec![(t / 1000 - 1800, t / 1000 + 8 * 3600)], ..Sources::default() };
1517 let mut s = switch(&[("phone", pressed(t, State::Usual, 0))]);
1518 s.press_state("phone", State::Reachable, t + 60 * MIN, false, t + 30 * MIN, "");
1519 assert!(!now(&settings, &night, s.latest().as_ref(), (t + 65 * MIN) / 1000).on());
1520 let mut plain = Switch::default();
1522 plain.press_state("phone", State::Reachable, t + 90 * MIN, false, t + 60 * MIN, "");
1523 assert_eq!(plain.device["phone"].held_off, 0);
1524 assert!(now(&settings, &focus, plain.latest().as_ref(), (t + 95 * MIN) / 1000).has(Why::Focus));
1525 let mut after_dnd = switch(&[("desk", pressed(t, State::Dnd, 0))]);
1527 after_dnd.press_state("phone", State::Reachable, t + 90 * MIN, false, t + 60 * MIN, "");
1528 assert_eq!(after_dnd.device["phone"].held_off, 0);
1529 }
1530
1531 #[test]
1538 fn release_stamps_never_block_one_another() {
1539 use crate::appnotes::Ledger;
1540 let t = 1_790_000_000_000;
1541 const MIN: i64 = 60_000;
1542 let now_s = t / 1000;
1543 let mut s = switch(&[("phone", pressed(t - 86_400_000, State::Usual, 0))]);
1544 let ahead = s.release("laptop", t + 10 * MIN);
1545 let mut ledger = Ledger::default();
1546 assert!(ledger.apply_releases(&s.release_stamps(), now_s).0);
1547 let pressed_at = s.press_state("phone", State::Reachable, t + 62 * MIN, false, t + 2 * MIN, "");
1548 assert!(pressed_at > ahead, "the phone's press after the release it saw");
1549 assert!(ledger.apply_releases(&s.release_stamps(), now_s + 120).0, "and it releases");
1550 let mut unseen = switch(&[("tablet", Device { pressed: t + 3 * MIN, mode: State::Reachable.id().into(), until: t + 60 * MIN, ..Device::default() })]);
1552 unseen.device.insert("laptop".into(), s.device["laptop"].clone());
1553 assert!(ledger.apply_releases(&unseen.release_stamps(), now_s + 180).0, "never shadowed by another device's stamp");
1554 let mut odd = switch(&[("phone", pressed(t - 86_400_000, State::Usual, 0)), ("odd", Device { release: i64::MAX, ..pressed(t - 86_400_000, State::Usual, 0) })]);
1556 let mut ledger = Ledger::default();
1557 assert_eq!(ledger.apply_releases(&odd.release_stamps(), now_s), (false, vec!["odd".to_string()]), "passed over, said");
1558 assert_eq!(ledger.apply_releases(&odd.release_stamps(), now_s + 1), (false, vec![]), "said once");
1559 let desk = odd.release("desk", t + MIN);
1560 assert_eq!(desk, t + MIN, "the clock never follows a stamp a day ahead");
1561 assert!(ledger.apply_releases(&odd.release_stamps(), now_s + 60).0);
1562 let press = odd.press_state("phone", State::Reachable, t + 62 * MIN, false, t + 2 * MIN, "");
1563 assert!(press < i64::MAX && press > desk);
1564 assert!(ledger.apply_releases(&odd.release_stamps(), now_s + 120).0);
1565 assert_eq!(odd.next_stamp(i64::MAX), i64::MAX, "saturates");
1566 }
1567
1568 #[test]
1573 fn a_press_overridden_knowingly_never_comes_back() {
1574 let t = 1_790_000_000_000;
1575 let s = switch(&[("A", pressed(t, State::Dnd, t - 86_400_000)), ("B", Device { until: t + 3_600_000, ..pressed(t + 10_000, State::Reachable, t) }), ("C", pressed(t + 20_000, State::Usual, t - 86_400_000))]);
1576 assert_eq!(s.latest().map(|p| (p.from, p.state)), Some(("C".to_string(), State::Usual)));
1577 let two = switch(&[("A", pressed(t, State::Dnd, t - 86_400_000)), ("C", pressed(t + 20_000, State::Usual, t - 86_400_000))]);
1579 assert_eq!(two.latest().map(|p| p.from), Some("A".to_string()));
1580 }
1581
1582 #[test]
1586 fn a_pause_that_knew_of_reachable_ends_it_whatever_its_clock() {
1587 let settings = DndSettings::default();
1588 let press = reachable(10_000, 20_000);
1589 let behind = Sources { pause_pressed: Some(10_000 - 60), pause_saw: Some(press.at), ..Sources::default() };
1590 assert!(now(&settings, &behind, Some(&press), 11_000).reachable.is_none());
1591 let before = Sources { pause_pressed: Some(10_000 - 60), pause_saw: Some(press.at - 1), ..Sources::default() };
1592 assert!(now(&settings, &before, Some(&press), 11_000).reachable.is_some());
1593 }
1594
1595 #[test]
1601 fn a_gone_device_s_expecting_a_call_opens_nothing() {
1602 const DAY: i64 = 86_400_000;
1603 let t = 1_790_000_000_000;
1604 let gone = |id: &str| id != "old-laptop";
1605 let mut raw = switch(&[("phone", pressed(t - 3 * DAY, State::Usual, 0)), ("old-laptop", pressed(t - 2 * DAY, State::Usual, 0))]);
1606 crate::calls::press(&mut raw, "phone", true, 0, t - 3 * DAY);
1607 crate::calls::press(&mut raw, "old-laptop", false, 0, t - 2 * DAY);
1608 let read = crate::calls::through_of(&raw.counted("phone", &gone)).unwrap();
1609 assert!(!read.holds(t), "its screen again still ends the phone's: {read:?}");
1610 let mut other = switch(&[("phone", pressed(t - 3 * DAY, State::Usual, 0))]);
1611 crate::calls::press(&mut other, "old-laptop", true, 0, t - 2 * DAY);
1612 assert!(crate::calls::through_of(&other.counted("phone", &gone)).is_none_or(|e| !e.holds(t)), "nothing expected");
1613 let mut screening = switch(&[("old-laptop", pressed(t - 2 * DAY, State::Usual, 0))]);
1614 crate::calls::set_screens(&mut screening, "old-laptop", true);
1615 assert!(crate::calls::screens_anywhere(&screening) && !crate::calls::screens_anywhere(&screening.counted("phone", &gone)));
1616 }
1617
1618 #[test]
1623 fn a_device_s_clock_set_right_never_shadows_its_own_releases() {
1624 use crate::appnotes::Ledger;
1625 let t = 1_790_000_000_000;
1626 const MIN: i64 = 60_000;
1627 let mut s = switch(&[("phone", pressed(t - 86_400_000, State::Usual, 0))]);
1628 s.release("laptop", t + 120 * MIN);
1629 let mut ledger = Ledger::default();
1630 assert!(ledger.apply_releases(&s.release_stamps(), t / 1000).0, "applied, at the phone's now");
1631 assert!(!ledger.apply_releases(&s.release_stamps(), t / 1000 + 60).0, "once");
1632 s.press_state("laptop", State::Reachable, t + 63 * MIN, false, t + 3 * MIN, "");
1633 assert!(ledger.apply_releases(&s.release_stamps(), t / 1000 + 180).0, "its Reachable, its clock set right");
1634 let release = s.release("laptop", t + 5 * MIN);
1635 assert!(release < t + 120 * MIN, "the clock never follows a release hours ahead");
1636 assert!(ledger.apply_releases(&s.release_stamps(), t / 1000 + 300).0, "its release, set right");
1637 assert!(!ledger.apply_releases(&s.release_stamps(), t / 1000 + 360).0, "each once");
1638 s.press_state("desk", State::Reachable, t + 66 * MIN, false, t + 6 * MIN, "");
1639 assert!(ledger.apply_releases(&s.release_stamps(), t / 1000 + 360).0, "another device's");
1640 }
1641
1642 #[test]
1647 fn a_release_far_ahead_never_drags_the_presses() {
1648 let t = 1_790_000_000_000;
1649 let mut s = switch(&[("phone", pressed(t - 86_400_000, State::Usual, 0))]);
1650 s.release("odd", t + 23 * 3_600_000);
1651 let usual = s.press_state("phone", State::Usual, 0, false, t, "");
1652 assert_eq!(usual, t);
1653 let settings = DndSettings { focus: true, sleep: true, ..DndSettings::default() };
1654 let later = Sources { focus: Some((t / 1000 + 3600, t / 1000 + 4 * 3600)), ..Sources::default() };
1655 assert!(now(&settings, &later, s.latest().as_ref(), t / 1000 + 7200).on(), "the session begun an hour later holds");
1656 let night = Sources { sleep: Some((t / 1000 + 10 * 3600, t / 1000 + 18 * 3600)), ..Sources::default() };
1657 assert!(now(&settings, &night, s.latest().as_ref(), t / 1000 + 11 * 3600).on(), "tonight holds");
1658 let mut near = switch(&[("laptop", Device { release: t + 30 * 60_000, ..pressed(t - 86_400_000, State::Usual, 0) })]);
1659 assert_eq!(near.press_state("phone", State::Usual, 0, false, t, ""), t + 30 * 60_000 + 1, "a release within the hour: after it");
1660 let mut ahead = switch(&[("laptop", pressed(t + 5 * 3_600_000, State::Dnd, 0))]);
1661 assert_eq!(ahead.press_state("phone", State::Usual, 0, false, t, ""), t + 5 * 3_600_000 + 1, "a press hours ahead (a time zone set wrong): after it");
1662 }
1663
1664 #[test]
1668 fn a_press_far_ahead_never_wins_for_ever() {
1669 let t = 1_790_000_000_000;
1670 let mut s = switch(&[("phone", pressed(t - 86_400_000, State::Dnd, 0)), ("odd", pressed(t + 365 * 86_400_000, State::Usual, 0))]);
1671 crate::calls::press(&mut s, "odd", true, 0, t + 365 * 86_400_000);
1672 assert_eq!(s.latest().map(|p| p.from), Some("odd".to_string()), "as the file says");
1673 let read = s.clone().as_of(t);
1674 assert_eq!(read.latest().map(|p| (p.from, p.state)), Some(("phone".to_string(), State::Dnd)));
1675 assert!(crate::calls::through_of(&read).is_none());
1676 let mut later = switch(&[("odd", pressed(i64::MAX, State::Dnd, 0))]);
1678 assert!(later.clone().as_of(t).latest().is_none());
1679 assert_eq!(later.press_state("phone", State::Usual, 0, false, t, ""), t);
1680 }
1681
1682 #[test]
1685 fn a_gone_device_s_as_usual_gives_reachable_nothing_to_carry() {
1686 let t = 1_790_000_000_000;
1687 let raw = switch(&[("old-laptop", pressed(t, State::Usual, 0))]);
1688 assert_eq!(held_off_of(raw.latest().as_ref(), State::Reachable), t, "as the file says");
1689 assert_eq!(held_off_of(raw.counted("phone", &|id| id != "old-laptop").latest().as_ref(), State::Reachable), 0);
1690 }
1691
1692 #[test]
1693 fn until_ends_it_by_each_devices_clock() {
1694 let settings = DndSettings::default();
1695 let press = Press { at: 1_000_000, on: true, until: 1_600_000, from: "desk".into(), via: String::new(), state: State::of(true), open: false, held_off: 0, gone: false };
1696 let held = now(&settings, &Sources::default(), Some(&press), 1_500);
1697 assert_eq!(held.why(), Some(Why::Manual));
1698 assert_eq!(held.until(), Some(1_600));
1699 assert!(!now(&settings, &Sources::default(), Some(&press), 1_600).on());
1700 }
1701
1702 #[test]
1703 fn each_trigger_is_a_setting_and_the_pause_comes_first() {
1704 let sources = Sources { paused: Some(100), free: Some((50, 900)), sleep: Some((80, 2000)), focus: Some((90, 1000)), pause_pressed: Some(100), ..Sources::default() };
1705 let none = DndSettings { pauses: false, sleep: false, focus: false, ..DndSettings::default() };
1706 assert!(!now(&none, &sources, None, 500).on());
1707 let all = DndSettings { pauses: true, sleep: true, focus: true, ..DndSettings::default() };
1708 let held = now(&all, &sources, None, 500);
1709 assert_eq!(held.holds.iter().map(|h| h.why).collect::<Vec<_>>(), vec![Why::Paused, Why::Sleep, Why::Focus]);
1711 assert_eq!(held.why(), Some(Why::Paused));
1712 assert!(held.global() && held.global_why() == Some(Why::Sleep));
1713 assert_eq!(held.until(), None);
1715 let free = Sources { free: Some((50, 900)), ..Sources::default() };
1717 let held = now(&all, &free, None, 500);
1718 assert_eq!((held.why(), held.until()), (Some(Why::FreeTime), Some(900)));
1719 assert!(!held.global());
1720 assert!(!now(&all, &free, None, 900).on(), "Free time over at its end");
1721 }
1722
1723 #[test]
1724 fn pressed_off_after_a_reason_began_holds_it_off_until_it_ends() {
1725 let settings = DndSettings { focus: true, sleep: true, ..DndSettings::default() };
1726 let focus = Sources { focus: Some((1_000, 5_000)), ..Sources::default() };
1727 let off_after = Press { at: 2_000_000, on: false, until: 0, from: "phone".into(), via: String::new(), state: State::of(false), open: false, held_off: 0, gone: false };
1728 assert!(!now(&settings, &focus, Some(&off_after), 3_000).on(), "the person turned it off during the session");
1729 let later = Sources { focus: Some((2_500, 6_000)), ..Sources::default() };
1731 assert!(now(&settings, &later, Some(&off_after), 3_000).has(Why::Focus));
1732 let on = Press { on: true, state: State::Dnd, ..off_after.clone() };
1734 let held = now(&settings, &focus, Some(&on), 3_000);
1735 assert!(held.has(Why::Focus) && held.has(Why::Manual));
1736 let off_before = Press { at: 500_000, ..off_after };
1737 assert!(now(&settings, &focus, Some(&off_before), 3_000).has(Why::Focus));
1738 }
1739
1740 #[test]
1741 fn mail_and_sioul_s_own_notifications_are_gated_by_the_switch_and_focus_only() {
1742 let settings = DndSettings { focus: true, sleep: true, ..DndSettings::default() };
1743 let press = Press { at: 1_000_000, on: true, until: 0, from: "desk".into(), via: String::new(), state: State::of(true), open: false, held_off: 0, gone: false };
1744 assert!(now(&settings, &Sources::default(), Some(&press), 2_000).gates());
1745 assert!(now(&settings, &Sources { focus: Some((1_000, 9_000)), ..Sources::default() }, None, 2_000).gates());
1746 assert!(!now(&settings, &Sources { sleep: Some((1_000, 9_000)), ..Sources::default() }, None, 2_000).gates());
1748 assert!(!now(&settings, &Sources { paused: Some(1_000), ..Sources::default() }, None, 2_000).gates());
1749 assert!(!now(&settings, &Sources::default(), None, 2_000).gates());
1750 }
1751
1752 #[test]
1753 fn a_forgotten_focus_session_lets_you_be_reached_again() {
1754 let mut running = crate::timelog::Running { task: "t".into(), start: 1_000, planned: 25, paused_at: None, paused: 0 };
1755 assert_eq!(focus(&running, 1_100), Some((1_000, 1_000 + 25 * 60 + FOCUS_GRACE)));
1756 assert_eq!(focus(&running, 1_000 + 25 * 60 + FOCUS_GRACE), None);
1757 running.paused = 600;
1759 assert_eq!(focus(&running, 1_100).map(|(_, end)| end), Some(1_000 + 600 + 25 * 60 + FOCUS_GRACE));
1760 running.paused_at = Some(1_200);
1761 assert_eq!(focus(&running, 1_300), None);
1762 let open = crate::timelog::Running { task: "t".into(), start: 1_000, ..crate::timelog::Running::default() };
1764 assert_eq!(focus(&open, 1_100).map(|(_, end)| end), Some(1_000 + FOCUS_OPEN));
1765 }
1766
1767 #[test]
1768 fn the_status_says_where_it_holds() {
1769 let tr = Translator::new("en");
1770 let settings = DndSettings::default();
1771 let press = Press { at: 1_000_000, on: true, until: 0, from: "phone".into(), via: String::new(), state: State::of(true), open: false, held_off: 0, gone: false };
1772 let held = now(&settings, &Sources::default(), Some(&press), 2_000);
1773 let name = |id: &str| if id == "phone" { "your phone".to_string() } else { id.to_string() };
1774 let all = [("phone".to_string(), Follows::Silenced)];
1775 let s = said(&held, at(true, ""), &all, "15:00", &name, &tr);
1776 assert_eq!(s.line, "Do not disturb, on every device, until 15:00.");
1777 assert!(s.everywhere && s.here);
1778 assert_eq!(s.why, "Turned on from your phone.");
1779 assert_eq!(s.details, vec!["Here: silenced.", "On your phone: silenced."]);
1780 let s = said(&held, at(true, ""), &[], "", &name, &tr);
1781 assert_eq!(s.line, "Do not disturb, here only.");
1782 let behind = [("phone".to_string(), Follows::Behind), ("desk".to_string(), Follows::Silenced)];
1784 let s = said(&held, at(true, ""), &behind, "", &name, &tr);
1785 assert_eq!(s.line, "Do not disturb here; your other devices' news is on its way.");
1786 assert_eq!(s.details[1], "On your phone: waiting for its news.");
1787 let s = said(&held, at(true, ""), &[("phone".to_string(), Follows::Behind)], "15:00", &name, &tr);
1788 assert_eq!(s.line, "Do not disturb here until 15:00; your other devices' news is on its way.");
1789 let s = said(&held, at(true, ""), &[("phone".to_string(), Follows::Older)], "", &name, &tr);
1791 assert_eq!(s.line, "Do not disturb, here only.");
1792 assert_eq!(s.details[1], "On your phone: an older Sioul, which cannot follow until it is updated.");
1793 let cannot = [("phone".to_string(), Follows::Cannot("Your phone is not silenced: Sioul does not have Android's “Do Not Disturb access”.".into()))];
1794 let s = said(&held, at(true, ""), &cannot, "", &name, &tr);
1795 assert_eq!(s.line, "Do not disturb, here only.");
1796 assert_eq!(s.details[1], "On your phone: Your phone is not silenced: Sioul does not have Android's “Do Not Disturb access”.");
1797 let s = said(&held, at(false, "This computer's notifications cannot be silenced by Sioul."), &all, "", &name, &tr);
1799 assert_eq!(s.line, "Do not disturb, on your other devices; this one keeps only Sioul's own notifications back.");
1800 assert!(!s.everywhere);
1801 assert_eq!(said(&Now::default(), at(true, ""), &all, "", &name, &tr), Said::default());
1803 let fr = Translator::new("fr");
1805 let s = said(&held, at(true, ""), &all, "15:00", &name, &fr);
1806 assert_eq!(s.line, "Ne pas déranger, sur tous vos appareils, jusqu’à 15:00.");
1807 assert_eq!(s.details[0], "Ici\u{202f}: en silence.");
1808 assert_eq!(s.details[1], "Sur your phone\u{202f}: en silence.");
1809 }
1810
1811 #[test]
1812 fn others_count_when_heard_lately_and_follow_by_their_own_table() {
1813 let now_s = 1_000_000;
1814 let mut s = Switch::default();
1815 s.device.insert("phone".into(), Device { why: "manual".into(), silenced: true, at: now_s * 1000, ..Device::default() });
1816 s.device.insert("old".into(), Device { why: "manual".into(), silenced: true, at: (now_s - 30 * 86_400) * 1000, ..Device::default() });
1817 s.device.insert("laptop".into(), Device { why: "manual".into(), silenced: false, line: "Windows".into(), at: now_s * 1000, ..Device::default() });
1818 let heard = vec![("phone".to_string(), now_s - 60), ("tablet".to_string(), now_s - 3600), ("gone".to_string(), now_s - 10 * 86_400), ("desk".to_string(), now_s)];
1819 let registered = |_: &str| true;
1820 let followed = others(&s, "desk", &heard, now_s, (now_s - 60) * 1000, ®istered);
1821 assert_eq!(followed, vec![("laptop".to_string(), Follows::Cannot("Windows".into())), ("phone".to_string(), Follows::Silenced), ("tablet".to_string(), Follows::Behind)]);
1822 let later = others(&s, "desk", &heard, now_s, (now_s + 120) * 1000, ®istered);
1824 assert!(later.iter().all(|(_, f)| *f == Follows::Behind), "{later:?}");
1825 let close = others(&s, "desk", &heard, now_s, (now_s + 30) * 1000, ®istered);
1826 assert!(close.iter().any(|(_, f)| *f == Follows::Silenced), "{close:?}");
1827 let older = others(&s, "desk", &heard, now_s, (now_s - 60) * 1000, &|id: &str| id != "tablet");
1829 assert!(older.contains(&("tablet".to_string(), Follows::Older)), "{older:?}");
1830 }
1831
1832 #[test]
1833 fn only_this_devices_table_is_written_and_a_broken_file_is_left_alone() {
1834 let dir = std::env::temp_dir().join(format!("sioul-everywhere-{}-{}", std::process::id(), line!()));
1835 let _ = std::fs::remove_dir_all(&dir);
1836 let path = dir.join(SWITCH_FILE);
1837 std::fs::create_dir_all(&dir).unwrap();
1839 std::fs::write(&path, "[device.\"phone-1\"]\nkind = \"phone\"\npressed = 7\non = true\n").unwrap();
1840 let s = change_own(&path, "desk-1", |s| {
1841 s.press("desk-1", false, 0, 1_000);
1842 true
1843 })
1844 .unwrap();
1845 assert_eq!(s.device.len(), 2);
1846 let again = Switch::read(&path).unwrap();
1847 assert_eq!(again.device["phone-1"].pressed, 7, "the other's table untouched");
1848 assert!(!again.device["desk-1"].on && again.device["desk-1"].seen == 7);
1849 std::fs::write(&path, "[device.\"phone-1\"\nbroken").unwrap();
1851 assert!(change_own(&path, "desk-1", |_| true).is_err());
1852 assert!(std::fs::read_to_string(&path).unwrap().contains("broken"));
1853 let _ = std::fs::remove_dir_all(&dir);
1854 }
1855
1856 #[test]
1857 fn the_list_admits_its_people_by_address_and_number() {
1858 let fr = crate::phones::region_named("FR");
1859 let mut people = People::default();
1860 let alice = people.add(Person { name: "Alice".into(), phones: vec!["04 65 71 23 45".into()], emails: vec!["Alice@Example.org".into()], ..Person::default() }, fr);
1861 assert!(people.admits_address("alice@example.org"));
1862 assert!(people.admits_address("Alice Martin <ALICE@example.org>"));
1863 assert!(!people.admits_address("bob@example.org"));
1864 assert!(!people.admits_address(""));
1865 assert!(people.admits_number("+33 4 65 71 23 45", fr));
1866 assert!(people.admits_number("0465712345", fr));
1867 assert!(!people.admits_number("0465712346", fr));
1868 let mut short = People::default();
1870 short.add(Person { phones: vec!["3631".into()], ..Person::default() }, fr);
1871 assert!(!short.admits_number("3631", fr));
1872 let again = people.add(Person { name: "A.".into(), phones: vec!["+33465712345".into()], emails: vec!["alice@work.example".into()], ..Person::default() }, fr);
1874 assert_eq!(again, alice);
1875 assert_eq!(people.people.len(), 1);
1876 assert_eq!(people.people[0].emails, vec!["Alice@Example.org", "alice@work.example"]);
1877 assert_eq!(people.people[0].name, "Alice", "the name kept");
1878 assert!(people.remove(&alice) && people.people.is_empty());
1879 }
1880
1881 #[test]
1882 fn someone_blocked_comes_off_the_list() {
1883 let fr = crate::phones::region_named("FR");
1884 let mut people = People::default();
1885 let alice = people.add(Person { name: "Alice".into(), phones: vec!["04 65 71 23 45".into()], emails: vec!["alice@example.org".into(), "alice@work.example".into()], ..Person::default() }, fr);
1886 people.add(Person { name: "Bob".into(), emails: vec!["bob@example.org".into()], contact: "uid-bob".into(), ..Person::default() }, fr);
1887 assert!(take_off(&mut people, &["Alice <ALICE@example.org>".into()], &[], "", fr));
1889 assert_eq!(people.people.iter().find(|p| p.id == alice).map(|p| p.emails.clone()), Some(vec!["alice@work.example".to_string()]));
1890 assert!(!take_off(&mut people, &["carol@example.org".into()], &["+33 1 99 00 00 01".into()], "", fr));
1892 assert!(take_off(&mut people, &[], &[], "uid-bob", fr));
1894 assert!(take_off(&mut people, &["alice@work.example".into()], &["+33465712345".into()], "", fr));
1895 assert!(people.people.is_empty());
1896 }
1897
1898 #[test]
1899 fn the_list_travels_one_person_at_a_time_and_is_never_read_empty() {
1900 let dir = std::env::temp_dir().join(format!("sioul-everywhere-{}-{}", std::process::id(), line!()));
1901 let _ = std::fs::remove_dir_all(&dir);
1902 let path = dir.join(PEOPLE_FILE);
1903 change_people(&path, |p| {
1904 p.add(Person { name: "Alice".into(), emails: vec!["alice@example.org".into()], ..Person::default() }, None);
1905 })
1906 .unwrap();
1907 let text = std::fs::read_to_string(&path).unwrap();
1908 assert!(text.contains("[[person]]") && text.contains("alice@example.org"), "{text}");
1909 std::fs::write(&path, format!("{text}\n[[person]]\nid = \"b\"\nname = \"Bob\"\nphones = [\"+33536490002\"]\n")).unwrap();
1911 let people = change_people(&path, |p| {
1912 p.add(Person { name: "Carol".into(), emails: vec!["carol@example.org".into()], ..Person::default() }, None);
1913 })
1914 .unwrap();
1915 assert_eq!(people.people.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["Alice", "Bob", "Carol"]);
1916 std::fs::write(&path, "[[person]\nbroken").unwrap();
1918 assert!(People::read(&path).is_err());
1919 assert!(change_people(&path, |p| p.people.clear()).is_err());
1920 assert!(std::fs::read_to_string(&path).unwrap().contains("broken"));
1921 assert!(People::load(&path).people.is_empty(), "a question gets nobody");
1922 let _ = std::fs::remove_dir_all(&dir);
1923 }
1924
1925 #[test]
1926 fn the_safe_list_brings_its_people_with_their_cards() {
1927 let card = |name: &str, email: &str, phone: &str, category: &str| crate::contacts::Contact {
1928 uid: format!("uid-{name}"),
1929 name: name.into(),
1930 emails: vec![crate::contacts::Labeled { label: String::new(), value: email.into() }],
1931 phones: vec![crate::contacts::Labeled { label: String::new(), value: phone.into() }],
1932 categories: if category.is_empty() { Vec::new() } else { vec![category.into()] },
1933 ..crate::contacts::Contact::default()
1934 };
1935 let contacts = vec![card("Alice", "alice@example.org", "+33 4 65 71 23 45", ""), card("Bob", "bob@example.org", "+33 1 99 00 00 01", "Famille"), card("Eve", "eve@example.org", "", "")];
1936 let safe = ["alice@example.org", "category:famille", "@example.com", "*@example.net", "dan@example.net", "contact:uid-Eve", "tel:+33 3 53 01 00 07", "tel:+33899*"].map(String::from).to_vec();
1937 let (people, patterns) = from_safe(&safe, &contacts);
1938 assert_eq!(patterns, 3, "a domain, a pattern, a prefix");
1939 assert_eq!(people.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["Alice", "Bob", "", "Eve", ""]);
1940 assert_eq!(people[0].phones, vec!["+33 4 65 71 23 45"]);
1941 assert_eq!(people[0].contact, "uid-Alice");
1942 assert_eq!(people[2].emails, vec!["dan@example.net"]);
1943 assert_eq!((people[3].contact.as_str(), people[4].phones.clone()), ("uid-Eve", vec!["+33 3 53 01 00 07".to_string()]));
1944 }
1945
1946 #[test]
1947 fn settings_have_their_defaults_and_old_files_read() {
1948 let config: crate::config::Config = toml::from_str("").unwrap();
1949 assert_eq!(config.dnd, DndSettings::default());
1950 assert!(config.dnd.button && config.dnd.pauses && config.dnd.people && !config.dnd.focus && !config.dnd.sleep);
1951 let config: crate::config::Config = toml::from_str("[dnd]\nfocus = true\n").unwrap();
1952 assert!(config.dnd.focus && config.dnd.pauses);
1953 let s: Switch = toml::from_str("[device.a]\npressed = 5\non = true\nfuture = \"x\"\n").unwrap();
1955 assert!(s.latest().unwrap().on);
1956 assert!(toml::to_string(&s).unwrap().contains("future = \"x\""), "a later version's field written back");
1957 let p: People = toml::from_str("[[person]]\nid = \"a\"\nname = \"A\"\nring = \"loud\"\n").unwrap();
1958 assert!(toml::to_string(&p).unwrap().contains("ring = \"loud\""));
1959 }
1960
1961 #[test]
1962 fn every_word_has_both_languages() {
1963 for language in ["en", "fr"] {
1964 let tr = Translator::new(language);
1965 for key in [
1966 "dnd-everywhere", "dnd-everywhere-until", "dnd-here-only", "dnd-here-only-until", "dnd-waiting", "dnd-waiting-until", "dnd-not-everywhere", "dnd-not-everywhere-until", "dnd-elsewhere", "dnd-elsewhere-until", "dnd-own-only", "dnd-own-only-until",
1967 "dnd-device-silenced", "dnd-device-cannot", "dnd-device-unsilenced", "dnd-device-behind", "dnd-device-older", "dnd-device-here", "dnd-device-on", "dnd-device-phone", "dnd-device-other",
1968 "dnd-why-manual", "dnd-why-manual-from", "dnd-why-focus", "dnd-why-sleep", "dnd-why-paused", "dnd-why-free-time",
1969 "dnd-turned-off-here", "dnd-turned-off-here-until", "dnd-device-closed",
1970 ] {
1971 let mut args = crate::i18n::args();
1972 args.set("until", "15:00");
1973 args.set("device", "X");
1974 args.set("line", "Y.");
1975 args.set("name", "Z");
1976 let words = tr.text(key, Some(&args));
1977 assert!(!words.is_empty() && words != key && !words.contains('{'), "{language}: {key}: {words}");
1978 }
1979 }
1980 }
1981
1982 #[test]
1983 fn a_change_of_the_system_presses_only_when_it_disagrees() {
1984 assert_eq!(heard(false, true), Some(true));
1986 assert_eq!(heard(true, false), Some(false));
1988 assert_eq!(heard(true, true), None);
1990 assert_eq!(heard(false, false), None);
1991 }
1992
1993 #[test]
1994 fn a_press_heard_from_the_system_says_so_and_holds_like_any() {
1995 let mut s = switch(&[("desk", device(1_000_000, true, 0))]);
1996 let stamp = s.press_via("phone", false, 0, 900_000, VIA_SYSTEM);
1997 assert_eq!(stamp, 1_000_001);
1999 let latest = s.latest().unwrap();
2000 assert!(!latest.on && latest.from == "phone" && latest.via == VIA_SYSTEM);
2001 s.press("phone", true, 0, 2_000_000);
2003 assert_eq!(s.latest().unwrap().via, "");
2004 assert!(!toml::to_string(&s).unwrap().contains("via"), "an empty word is not written");
2005 s.press_via("desk", true, 0, 3_000_000, VIA_SYSTEM);
2007 let text = toml::to_string(&s).unwrap();
2008 assert!(text.contains("via = \"system\""), "{text}");
2009 let again: Switch = toml::from_str(&text).unwrap();
2010 assert_eq!(again.latest().unwrap().via, VIA_SYSTEM);
2011 let old: Switch = toml::from_str("[device.a]\npressed = 5\non = true\n").unwrap();
2012 assert_eq!(old.latest().unwrap().via, "");
2013 let settings = DndSettings { focus: true, ..DndSettings::default() };
2015 let focus = Sources { focus: Some((1_000, 9_000)), ..Sources::default() };
2016 let off = Press { at: 2_000_000, on: false, until: 0, from: "phone".into(), via: VIA_SYSTEM.into(), state: State::of(false), open: false, held_off: 0, gone: false };
2017 assert!(!now(&settings, &focus, Some(&off), 3_000).on());
2018 }
2019
2020 #[test]
2021 fn the_switch_held_by_the_system_alone_adds_no_mode() {
2022 let settings = DndSettings { sleep: true, focus: true, ..DndSettings::default() };
2023 let heard_here = Press { at: 1_000_000, on: true, until: 0, from: "phone".into(), via: VIA_SYSTEM.into(), state: State::of(true), open: false, held_off: 0, gone: false };
2024 let held = now(&settings, &Sources::default(), Some(&heard_here), 2_000);
2025 assert!(held_by_system(&held, Some(&heard_here), "phone"));
2027 assert!(!held_by_system(&held, Some(&heard_here), "desk"));
2029 let pressed = Press { via: String::new(), ..heard_here.clone() };
2031 assert!(!held_by_system(&held, Some(&pressed), "phone"));
2032 let night = now(&settings, &Sources { sleep: Some((1_500, 9_000)), ..Sources::default() }, Some(&heard_here), 2_000);
2034 assert!(!held_by_system(&night, Some(&heard_here), "phone"));
2035 let paused = now(&DndSettings::default(), &Sources { paused: Some(1_500), ..Sources::default() }, Some(&heard_here), 2_000);
2037 assert!(held_by_system(&paused, Some(&heard_here), "phone"));
2038 assert!(!held_by_system(&Now::default(), Some(&heard_here), "phone"));
2040 }
2041
2042 #[test]
2043 fn a_nightly_schedule_turns_it_on_then_off_on_every_device() {
2044 let settings = DndSettings::default();
2045 let mut s = Switch::default();
2046 let night = 22 * 3_600;
2048 let before = now(&settings, &Sources::default(), s.latest().as_ref(), night);
2049 assert_eq!(heard(before.on(), true), Some(true));
2050 s.press_via("phone", true, 0, night * 1000, VIA_SYSTEM);
2051 let held = now(&settings, &Sources::default(), s.latest().as_ref(), night + 60);
2053 assert!(held.on() && held.has(Why::Manual));
2054 assert!(!held_by_system(&held, s.latest().as_ref(), "desk"));
2055 assert!(held_by_system(&held, s.latest().as_ref(), "phone"));
2056 let morning = 31 * 3_600;
2058 let still = now(&settings, &Sources::default(), s.latest().as_ref(), morning);
2059 assert_eq!(heard(still.on(), false), Some(false));
2060 s.press_via("phone", false, 0, morning * 1000, VIA_SYSTEM);
2061 assert!(!now(&settings, &Sources::default(), s.latest().as_ref(), morning + 60).on(), "off on every device");
2062 let next = night + 86_400;
2064 assert_eq!(heard(now(&settings, &Sources::default(), s.latest().as_ref(), next).on(), true), Some(true));
2065 }
2066
2067 #[test]
2068 fn a_press_made_off_line_meets_the_latest() {
2069 let s = switch(&[
2072 ("phone", Device { pressed: 36_000_000, on: true, seen: 0, via: VIA_SYSTEM.into(), ..Device::default() }),
2073 ("desk", device(36_300_000, false, 0)),
2074 ]);
2075 assert!(!s.latest().unwrap().on, "the computer's later off");
2076 let s = switch(&[
2078 ("phone", Device { pressed: 36_000_000, on: true, seen: 0, via: VIA_SYSTEM.into(), ..Device::default() }),
2079 ("desk", device(36_030_000, false, 0)),
2080 ]);
2081 let latest = s.latest().unwrap();
2082 assert!(latest.on && latest.from == "phone" && latest.via == VIA_SYSTEM);
2083 }
2084
2085 #[test]
2086 fn a_computer_closed_or_quiet_is_said_so() {
2087 let tr = Translator::new("en");
2088 let now_s = 1_000_000;
2089 let since = (now_s - 3_600) * 1000;
2090 let silenced = |at_s: i64| Device { kind: "computer".into(), name: "laptop".into(), why: "manual".into(), silenced: true, at: at_s * 1000, ..Device::default() };
2091 let mut quit = silenced(now_s - 600);
2093 quit.closing(false);
2094 assert!(quit.closed && !quit.silenced && quit.line.is_empty());
2095 let s = Switch { device: [("laptop".to_string(), quit.clone())].into(), ..Switch::default() };
2096 let text = toml::to_string(&s).unwrap();
2097 assert!(text.contains("closed = true"), "{text}");
2098 let read: Switch = toml::from_str(&text).unwrap();
2099 let heard = vec![("laptop".to_string(), now_s - 600)];
2100 let followed = others(&read, "phone", &heard, now_s, since, &|_| true);
2101 assert_eq!(followed, vec![("laptop".to_string(), Follows::Closed)]);
2102 let press = Press { at: since + 1, on: true, until: 0, from: "phone".into(), via: String::new(), state: State::of(true), open: false, held_off: 0, gone: false };
2103 let held = now(&DndSettings::default(), &Sources::default(), Some(&press), now_s);
2104 let name = |id: &str| id.to_string();
2105 let said_ = said(&held, at(true, ""), &followed, "", &name, &tr);
2106 assert_eq!(said_.details[1], "On laptop: Sioul is closed.");
2107 assert_eq!(said_.line, "Do not disturb, here only.");
2108 assert_eq!(said(&held, at(true, ""), &followed, "", &name, &Translator::new("fr")).details[1], "Sur laptop\u{202f}: Sioul est fermé.");
2109 let mut early = silenced(now_s - 10 * 3_600);
2111 early.closing(false);
2112 let s = Switch { device: [("laptop".to_string(), early)].into(), ..Switch::default() };
2113 assert_eq!(others(&s, "phone", &heard, now_s, since, &|_| true)[0].1, Follows::Closed);
2114 let crashed = Switch { device: [("laptop".to_string(), silenced(now_s - 2_000))].into(), ..Switch::default() };
2116 let quiet = vec![("laptop".to_string(), now_s - 2_000)];
2117 assert_eq!(others(&crashed, "phone", &quiet, now_s, since, &|_| true)[0].1, Follows::Behind);
2118 let lately = vec![("laptop".to_string(), now_s - 600)];
2120 assert_eq!(others(&crashed, "phone", &lately, now_s, since, &|_| true)[0].1, Follows::Silenced);
2121 let phone = Device { kind: "phone".into(), ..silenced(now_s - 2_000) };
2123 let s = Switch { device: [("phone".to_string(), phone)].into(), ..Switch::default() };
2124 let quiet_phone = vec![("phone".to_string(), now_s - 2_000)];
2125 assert_eq!(others(&s, "laptop", &quiet_phone, now_s, since, &|_| true)[0].1, Follows::Silenced);
2126 }
2127
2128 #[test]
2129 fn a_device_turned_off_by_hand_says_so() {
2130 let tr = Translator::new("en");
2131 let press = Press { at: 1_000_000, on: true, until: 0, from: "desk".into(), via: String::new(), state: State::of(true), open: false, held_off: 0, gone: false };
2132 let held = now(&DndSettings::default(), &Sources::default(), Some(&press), 2_000);
2133 let name = |id: &str| id.to_string();
2134 let here = Here { silenced: false, line: "You turned do-not-disturb off on this phone.", turned_off: true };
2135 let s = said(&held, here, &[("desk".to_string(), Follows::Silenced)], "", &name, &tr);
2136 assert_eq!(s.line, "Do not disturb on your other devices; you turned it off here.");
2137 assert_eq!(s.details[0], "Here: You turned do-not-disturb off on this phone.");
2138 assert!(!s.here && !s.everywhere);
2139 let s = said(&held, here, &[], "15:00", &name, &Translator::new("fr"));
2140 assert_eq!(s.line, "Ne pas déranger jusqu\u{2019}à 15:00 sur vos autres appareils\u{202f}; vous l\u{2019}avez désactivé ici.");
2141 }
2142}