1mod actions;
13mod calendar;
14mod report;
15#[cfg(test)]
16#[path = "../../tests/brain.rs"]
17mod tests;
18
19pub use actions::NewEntry;
20pub use calendar::{Calendar, CalendarView, Day, Event, Show, What};
21pub use report::{Group, Report, ReportRow, ReportView, Slot};
22
23use crate::api::ApiError;
24use crate::model::{Author, Kind, RepoLink, Status, Task, now};
25use crate::projects::{self, ProjectInfo};
26use crate::store::{self, Skipped, Store};
27use crate::tags::TagColors;
28use anyhow::{Context, Result};
29use std::path::{Path, PathBuf};
30
31#[derive(Debug)]
33pub struct Brain {
34 pub store: Store,
36 pub tasks: Vec<Task>,
38 pub tag_colors: TagColors,
40 pub warnings: Vec<String>,
42 pub project: Option<ProjectInfo>,
44 pub central: Option<PathBuf>,
47 pub author: Author,
50}
51
52impl Brain {
53 pub fn open(store: Store) -> Result<Self> {
55 Self::open_with(store, None)
56 }
57
58 fn open_with(store: Store, central: Option<PathBuf>) -> Result<Self> {
61 Self::open_skipping(store, central).map(|(brain, _)| brain)
62 }
63
64 fn open_skipping(store: Store, central: Option<PathBuf>) -> Result<(Self, Vec<Skipped>)> {
66 crate::migrate::migrate(&store)?;
67 let mut brain = Self {
68 store,
69 tasks: Vec::new(),
70 tag_colors: TagColors::default(),
71 warnings: Vec::new(),
72 project: None,
73 central,
74 author: Author::Human,
75 };
76 let skipped = brain.reload_skipping()?;
77 Ok((brain, skipped))
78 }
79
80 pub fn read_only(store: Store) -> Result<Self> {
84 let (tasks, skipped) = store.load_entries()?;
85 let warnings = skipped.iter().map(ToString::to_string).collect();
86 let tag_colors = store.load_tag_colors()?;
87 Ok(Self { store, tasks, tag_colors, warnings, project: None, central: None, author: Author::Human })
88 }
89
90 pub fn open_in(store: Store, central: &Path) -> Result<Self> {
93 Self::open_with(store, Some(central.to_path_buf()))
94 }
95
96 pub(crate) fn open_in_skipping(store: Store, central: &Path) -> Result<(Self, Vec<Skipped>)> {
99 Self::open_skipping(store, Some(central.to_path_buf()))
100 }
101
102 pub fn reload(&mut self) -> Result<()> {
104 self.reload_skipping().map(drop)
105 }
106
107 fn reload_skipping(&mut self) -> Result<Vec<Skipped>> {
109 let (tasks, skipped) = self.store.load_entries()?;
110 self.tasks = tasks;
111 self.warnings = skipped.iter().map(ToString::to_string).collect();
112 self.renumber_shared_ids()?;
113 self.keep_one_person_doing()?;
114 self.drop_dangling_links()?;
115 self.remember_highest_id()?;
116 self.tag_colors = self.store.load_tag_colors()?;
117 self.project = projects::info(self.store.dir(), self.central.as_deref());
118 self.drop_dangling_repo_links()?;
119 self.sync_tag_colors()?;
120 Ok(skipped)
121 }
122
123 pub fn task(&self, id: u32) -> Option<&Task> {
125 self.tasks.iter().find(|t| t.id == id)
126 }
127
128 pub(crate) fn find(&self, id: u32) -> Option<usize> {
130 self.tasks.iter().position(|t| t.id == id)
131 }
132
133 pub(crate) fn index_of(&self, id: u32) -> Result<usize, ApiError> {
135 self.find(id).ok_or(ApiError::NotFound(id))
136 }
137
138 pub fn running(&self) -> impl Iterator<Item = &Task> {
140 self.tasks.iter().filter(|t| t.is_running())
141 }
142
143 pub fn person_doing(&self) -> Option<u32> {
145 self.tasks.iter().find(|t| t.doing_by(Author::Human)).map(|t| t.id)
146 }
147
148 pub fn column(&self, status: Status) -> Vec<&Task> {
150 column(&self.tasks, status)
151 }
152
153 pub fn backlinks(&self, id: u32) -> Vec<&Task> {
155 let mut from: Vec<&Task> = self.tasks.iter().filter(|t| t.links.contains(&id)).collect();
156 from.sort_by_key(|t| t.id);
157 from
158 }
159
160 pub fn related(&self, id: u32) -> Vec<&Task> {
163 let links = self.task(id).map_or(&[][..], |t| t.links.as_slice());
164 let mut related: Vec<&Task> =
165 self.tasks.iter().filter(|t| t.id != id && (links.contains(&t.id) || t.links.contains(&id))).collect();
166 related.sort_by_key(|t| (related_rank(t.kind), std::cmp::Reverse(t.id)));
167 related
168 }
169
170 pub fn are_related(&self, a: u32, b: u32) -> bool {
172 let links = |x: u32, y: u32| self.task(x).is_some_and(|t| t.links.contains(&y));
173 links(a, b) || links(b, a)
174 }
175
176 pub fn sibling(&self, repo: &str) -> Option<Brain> {
180 let (_, dir) = self.project.as_ref()?.siblings.iter().find(|(name, _)| name == repo)?;
181 if !dir.is_dir() {
182 return None;
183 }
184 Brain::read_only(Store::at(dir).ok()?).ok()
185 }
186
187 pub fn repo_related(&self, id: u32) -> Vec<(String, Task)> {
191 let Some(project) = &self.project else { return Vec::new() };
192 let links = self.task(id).map_or(&[][..], |t| t.repo_links.as_slice());
193 let here = RepoLink { repo: project.repo.clone(), id };
194 let mut related = Vec::new();
195 for (name, _) in &project.siblings {
196 let Some(other) = self.sibling(name) else { continue };
197 let mut found: Vec<Task> = other
198 .tasks
199 .into_iter()
200 .filter(|t| links.contains(&RepoLink { repo: name.clone(), id: t.id }) || t.repo_links.contains(&here))
201 .collect();
202 found.sort_by_key(|t| (related_rank(t.kind), std::cmp::Reverse(t.id)));
203 related.extend(found.into_iter().map(|t| (name.clone(), t)));
204 }
205 related
206 }
207
208 pub fn all_tags(&self) -> Vec<String> {
210 let mut tags: Vec<String> = self.tasks.iter().flat_map(|t| t.tags.iter().cloned()).collect();
211 tags.sort();
212 tags.dedup();
213 tags
214 }
215
216 fn take_id(&self) -> Result<u32> {
221 let highest = self.tasks.iter().map(|t| t.id).chain(self.store.file_ids()).max().unwrap_or(0);
222 let id = highest.max(self.store.last_id()).checked_add(1).context("no ids left: the highest id is taken")?;
225 self.store.save_last_id(id)?;
226 Ok(id)
227 }
228
229 fn drop_dangling_repo_links(&mut self) -> Result<()> {
235 let mut repos: Vec<&str> = self.tasks.iter().flat_map(|t| &t.repo_links).map(|l| l.repo.as_str()).collect();
236 repos.sort_unstable();
237 repos.dedup();
238 let mut gone: Vec<RepoLink> = Vec::new();
239 for repo in repos {
240 let Some(other) = self.sibling(repo) else { continue };
241 let files = other.store.file_ids();
243 let last = other.store.last_id().max(files.iter().copied().max().unwrap_or(0));
244 for link in self.tasks.iter().flat_map(|t| &t.repo_links).filter(|l| l.repo == repo) {
245 let deleted = other.task(link.id).is_none() && !files.contains(&link.id) && link.id <= last;
246 if deleted && !gone.contains(link) {
247 gone.push(link.clone());
248 }
249 }
250 }
251 for link in &gone {
252 self.drop_repo_links_to(link)?;
253 }
254 Ok(())
255 }
256
257 fn renumber_shared_ids(&mut self) -> Result<()> {
261 let own_name =
262 |t: &Task| t.path.as_ref().and_then(|p| p.file_name()).is_some_and(|n| *n == *store::file_name(t));
263 let mut order: Vec<usize> = (0..self.tasks.len()).collect();
264 let key = |i: usize| {
265 let t = &self.tasks[i];
266 (t.id, !own_name(t), t.created, t.path.as_ref())
267 };
268 order.sort_by(|&a, &b| key(a).cmp(&key(b)));
269 let mut seen = std::collections::HashSet::new();
270 for i in order {
271 if !seen.insert(self.tasks[i].id) {
272 let id = self.take_id()?;
273 let task = &mut self.tasks[i];
274 task.id = id;
275 self.store.save(task)?;
276 }
277 }
278 Ok(())
279 }
280
281 fn remember_highest_id(&self) -> Result<()> {
284 let highest = self.tasks.iter().map(|t| t.id).max().unwrap_or(0);
285 if highest > self.store.last_id() {
286 self.store.save_last_id(highest)?;
287 }
288 Ok(())
289 }
290
291 fn keep_one_person_doing(&mut self) -> Result<()> {
295 let doing = |t: &&mut Task| t.doing_by(Author::Human);
296 let Some(latest) = self.tasks.iter_mut().filter(doing).map(|t| (t.status_since(), t.id)).max() else {
297 return Ok(());
298 };
299 let now = now();
300 for task in self.tasks.iter_mut().filter(doing) {
301 if (task.status_since(), task.id) != latest {
302 task.set_status(Status::Parked, now, self.author);
303 self.store.save(task)?;
304 }
305 }
306 Ok(())
307 }
308
309 fn drop_dangling_links(&mut self) -> Result<()> {
312 let mut ids = self.store.file_ids();
313 ids.extend(self.tasks.iter().map(|t| t.id));
314 for task in &mut self.tasks {
315 let before = task.links.len();
316 task.links.retain(|link| ids.contains(link));
317 if task.links.len() != before {
318 self.store.save(task)?;
319 }
320 }
321 Ok(())
322 }
323
324 fn sync_tag_colors(&mut self) -> Result<()> {
326 let mut by_age: Vec<&Task> = self.tasks.iter().collect();
327 by_age.sort_by_key(|t| t.id);
328 if self.tag_colors.assign(by_age.iter().flat_map(|t| &t.tags)) {
329 self.store.save_tag_colors(&self.tag_colors)?;
330 }
331 Ok(())
332 }
333}
334
335pub(crate) fn column(tasks: &[Task], status: Status) -> Vec<&Task> {
337 let mut column: Vec<&Task> = tasks.iter().filter(|t| t.is_task() && t.status == status).collect();
338 column.sort_by(|a, b| b.status_since().cmp(&a.status_since()).then(b.id.cmp(&a.id)));
339 column
340}
341
342fn related_rank(kind: Kind) -> u8 {
344 match kind {
345 Kind::Note => 0,
346 Kind::Decision => 1,
347 Kind::Task => 2,
348 }
349}