1use crate::card::ImapOrigin;
13use crate::config::state_dir;
14use crate::porch::Triaged;
15use serde::{Deserialize, Serialize};
16use std::collections::BTreeMap;
17use std::path::{Path, PathBuf};
18
19#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
20pub struct PorchState {
21 #[serde(default)]
23 pub done: BTreeMap<String, ImapOrigin>,
24 #[serde(skip)]
26 pub read: crate::filelock::Read,
27}
28
29impl PorchState {
30 pub fn default_path() -> PathBuf {
32 state_dir().join("porch.toml")
33 }
34
35 pub fn load(path: &Path) -> PorchState {
37 let text = std::fs::read_to_string(path).unwrap_or_default();
38 let mut state: PorchState = toml::from_str(&text).unwrap_or_default();
39 state.read = crate::filelock::Read::of(&text);
40 state
41 }
42
43 pub fn save(&self, path: &Path) -> Result<(), String> {
48 crate::filelock::save_merged_by(path, &self.read, self, |merged, before| {
50 let Some(before) = before.and_then(|b| b.get("done")).and_then(toml::Value::as_table) else { return };
51 let Some(done) = merged.get_mut("done").and_then(toml::Value::as_table_mut) else { return };
52 for (account, mark) in done.iter_mut() {
53 let field = |m: &toml::Value, name: &str| m.get(name).and_then(toml::Value::as_integer);
54 if let Some(there) = before.get(account.as_str())
55 && field(there, "validity") == field(mark, "validity")
56 && field(there, "uid") > field(mark, "uid")
57 {
58 *mark = there.clone();
59 }
60 }
61 })
62 }
63
64 pub fn is_done(&self, account: Option<&str>, origin: Option<ImapOrigin>) -> bool {
66 match (account.and_then(|a| self.done.get(a)), origin) {
67 (Some(mark), Some(o)) => o.validity == mark.validity && o.uid <= mark.uid,
68 _ => false,
69 }
70 }
71
72 pub fn newest_shown(shown: &[Triaged]) -> BTreeMap<String, ImapOrigin> {
76 let mut newest: BTreeMap<String, ImapOrigin> = BTreeMap::new();
77 for t in shown.iter().filter(|t| t.lane != crate::porch::Lane::Review) {
78 let (Some(account), Some(origin)) = (&t.card.account, t.card.origin) else { continue };
79 let mark = newest.entry(account.clone()).or_insert(origin);
80 *mark = (*mark).max(origin);
81 }
82 newest
83 }
84
85 pub fn close(&mut self, newest: &BTreeMap<String, ImapOrigin>) {
88 for (account, origin) in newest {
89 let mark = self.done.entry(account.clone()).or_insert(*origin);
90 if mark.validity != origin.validity || mark.uid < origin.uid {
91 *mark = *origin;
92 }
93 }
94 }
95}
96
97#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
100pub struct MoneyState {
101 #[serde(default)]
102 pub ignored: std::collections::BTreeSet<String>,
103 #[serde(skip)]
105 pub read: crate::filelock::Read,
106}
107
108impl MoneyState {
109 pub fn default_path() -> PathBuf {
110 state_dir().join("money.toml")
111 }
112
113 pub fn load(path: &Path) -> MoneyState {
114 let text = std::fs::read_to_string(path).unwrap_or_default();
115 let mut state: MoneyState = toml::from_str(&text).unwrap_or_default();
116 state.read = crate::filelock::Read::of(&text);
117 state
118 }
119
120 pub fn save(&self, path: &Path) -> Result<(), String> {
123 crate::filelock::save_merged(path, &self.read, self)
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn closing_marks_what_was_shown() {
133 let mut state = PorchState::default();
134 let mark = |validity, uid| ImapOrigin { validity, uid };
135 state.close(&BTreeMap::from([("personal".to_string(), mark(5, 40))]));
136 assert!(state.is_done(Some("personal"), Some(mark(5, 40))));
137 assert!(!state.is_done(Some("personal"), Some(mark(5, 41))));
138 assert!(!state.is_done(Some("other"), Some(mark(5, 1))));
139 assert!(!state.is_done(Some("personal"), None));
140 state.close(&BTreeMap::from([("personal".to_string(), mark(5, 30))]));
142 assert!(state.is_done(Some("personal"), Some(mark(5, 40))));
143 state.close(&BTreeMap::from([("personal".to_string(), mark(6, 3))]));
145 assert!(!state.is_done(Some("personal"), Some(mark(5, 1))));
146 let text = toml::to_string(&state).unwrap();
147 assert_eq!(toml::from_str::<PorchState>(&text).unwrap(), state);
148 }
149}