Skip to main content

sioul/
remind.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! `sioul remind`: reminders before dates (docs/reminders.md), listed, or told
5//! with Sioul's window closed (`--watch`, started with the session when asked
6//! on the Parameters page). The window tells the same ones; each is marked
7//! when told, so it is told once, whichever runs.
8
9use crate::Session;
10use jiff::Zoned;
11use sioul_core::config::{self, Config};
12use sioul_core::reminders;
13
14/// What comes in the next two weeks, with what was told already.
15pub(crate) fn list(s: &Session) -> Result<(), String> {
16    let now = Zoned::now();
17    let (all, _) = reminders::gather(&s.config, &s.tr, &now);
18    let stamp = now.timestamp().as_second();
19    let dir = reminders::told_dir();
20    let coming: Vec<&reminders::Reminder> = all.iter().filter(|r| r.until > stamp && r.at < stamp + 14 * 86_400).collect();
21    if coming.is_empty() {
22        println!("{}", s.tr.text("remind-nothing", None));
23    }
24    for reminder in coming {
25        let at = jiff::Timestamp::from_second(reminder.at).map(|t| t.to_zoned(now.time_zone().clone())).map_err(|e| e.to_string())?;
26        let told = if reminders::told(&dir, &reminder.key) { format!(" ({})", s.tr.text("remind-told", None)) } else { String::new() };
27        // Titles of tasks, events and mail: one line each, never a terminal's escape sequences.
28        println!("{}{told}\n    {}{}", s.tr.date(&at, true), crate::one_line(&reminder.title), if reminder.body.is_empty() { String::new() } else { format!(" — {}", crate::one_line(&reminder.body)) });
29    }
30    Ok(())
31}
32
33/// Stays open: each minute, what is due becomes one quiet notification. One
34/// at a time: a second `sioul remind --watch` leaves at once.
35pub(crate) fn watch(s: &Session) -> Result<(), String> {
36    let lock_path = config::state_dir().join("remind.lock");
37    std::fs::create_dir_all(config::state_dir()).map_err(|e| e.to_string())?;
38    let lock = std::fs::OpenOptions::new().create(true).truncate(false).write(true).open(&lock_path).map_err(|e| format!("{}: {e}", lock_path.display()))?;
39    // Held as long as the process lives.
40    if lock.try_lock().is_err() {
41        println!("{}", s.tr.text("remind-running", None));
42        return Ok(());
43    }
44    // Its number, for the window to stop it when asked.
45    let _ = std::fs::write(config::state_dir().join("remind.pid"), std::process::id().to_string());
46    println!("{}", s.tr.text("remind-watching", None));
47    let dir = reminders::told_dir();
48    let mut forgotten_on = None;
49    loop {
50        // Read again each time: settings, tasks and events change while it runs.
51        let config = Config::load(&s.config_path).unwrap_or_default();
52        let now = Zoned::now();
53        if forgotten_on != Some(now.date()) {
54            reminders::forget_old(&dir);
55            forgotten_on = Some(now.date());
56        }
57        // What holds now, as the matrix of what reaches you says, its layers read
58        // from their files: today's slots of time for you, do-not-disturb.
59        let (all, holds) = reminders::gather(&config, &s.tr, &now);
60        for reminder in reminders::to_tell(all, &dir, now.timestamp().as_second(), &holds) {
61            if let Err(e) = sioul_sync::notify::remind(&reminder.title, &reminder.body, None) {
62                eprintln!("{e}");
63            }
64        }
65        std::thread::sleep(std::time::Duration::from_secs(60));
66    }
67}