Skip to main content

sioul_core/
today.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! Today's own choices: how the day is, and the steps put off for today.
5//!
6//! The day's weather ("clear", "haze", "fog") is chosen by you, never guessed
7//! from what you do: it sets how much the plan puts in a day and, in fog,
8//! shows only small steps (docs/tasks.md, after the brain-weather probes of
9//! Chen, Meng & Nie, CSCW 2026, and energy accounting). "Not now" puts a step
10//! off until tomorrow. A new day starts clean: nothing carries over.
11
12use crate::config::state_dir;
13use jiff::civil::Date;
14use serde::{Deserialize, Serialize};
15use std::collections::BTreeSet;
16use std::path::{Path, PathBuf};
17
18/// How the day is.
19#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21pub enum Weather {
22    #[default]
23    Clear,
24    Haze,
25    Fog,
26}
27
28impl Weather {
29    /// How many heavy tasks the day takes: two when clear, one in haze, none in fog.
30    pub fn heavy(self) -> u32 {
31        match self {
32            Weather::Clear => 2,
33            Weather::Haze => 1,
34            Weather::Fog => 0,
35        }
36    }
37
38    /// The share of the usual room for tasks the day has, in percent.
39    pub fn room(self) -> u32 {
40        match self {
41            Weather::Clear => 100,
42            Weather::Haze => 60,
43            Weather::Fog => 30,
44        }
45    }
46
47    pub fn parse(text: &str) -> Weather {
48        match text {
49            "haze" => Weather::Haze,
50            "fog" => Weather::Fog,
51            _ => Weather::Clear,
52        }
53    }
54}
55
56/// The steps a fog day shows: this long at most.
57pub const FOG_MINUTES: u32 = 15;
58
59#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
60pub struct Today {
61    /// The day these choices are for.
62    #[serde(default)]
63    pub date: String,
64    #[serde(default)]
65    pub weather: Weather,
66    /// UIDs put off for today ("Not now").
67    #[serde(default)]
68    pub aside: BTreeSet<String>,
69    /// The file as it was read: saved, only what changed since is written (`filelock::save_merged`).
70    #[serde(skip)]
71    pub read: crate::filelock::Read,
72}
73
74impl Today {
75    pub fn default_path() -> PathBuf {
76        state_dir().join("today.toml")
77    }
78
79    /// Today's choices; yesterday's are forgotten.
80    pub fn load(path: &Path, today: Date) -> Today {
81        let text = std::fs::read_to_string(path).unwrap_or_default();
82        let saved: Today = toml::from_str(&text).unwrap_or_default();
83        let read = crate::filelock::Read::of(&text);
84        if saved.date == today.to_string() { Today { read, ..saved } } else { Today { date: today.to_string(), read, ..Today::default() } }
85    }
86
87    /// Written under the file's lock, which the sharing takes too: only what
88    /// changed since it was read, over what the file holds now.
89    pub fn save(&self, path: &Path) -> Result<(), String> {
90        crate::filelock::save_merged(path, &self.read, self)
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn a_new_day_starts_clean() {
100        let path = std::env::temp_dir().join(format!("sioul-today-{}.toml", std::process::id()));
101        let day: Date = "2026-10-03".parse().unwrap();
102        let mut today = Today::load(&path, day);
103        today.weather = Weather::Fog;
104        today.aside.insert("letter".into());
105        today.save(&path).unwrap();
106        assert_eq!(Today::load(&path, day), today);
107        let tomorrow = Today::load(&path, "2026-10-04".parse().unwrap());
108        assert_eq!((tomorrow.weather, tomorrow.aside.len()), (Weather::Clear, 0));
109        std::fs::remove_file(&path).unwrap();
110    }
111}