kanbrain_core/brain/
mod.rs1mod actions;
12mod calendar;
13mod report;
14#[cfg(test)]
15#[path = "../../tests/brain.rs"]
16mod tests;
17
18pub use actions::NewEntry;
19pub use calendar::{Calendar, CalendarView, Day, Event, Show, What};
20pub use report::{Group, Report, ReportRow, ReportView, Slot};
21
22use crate::api::ApiError;
23use crate::model::{Author, Kind, RepoLink, Status, Task, now};
24use crate::projects::{self, ProjectInfo};
25use crate::store::Store;
26use crate::tags::TagColors;
27use anyhow::Result;
28use std::path::{Path, PathBuf};
29
30#[derive(Debug)]
32pub struct Brain {
33 pub store: Store,
35 pub tasks: Vec<Task>,
37 pub tag_colors: TagColors,
39 pub warnings: Vec<String>,
41 pub project: Option<ProjectInfo>,
43 pub central: Option<PathBuf>,
46 pub author: Author,
49}
50
51impl Brain {
52 pub fn open(store: Store) -> Result<Self> {
54 Self::open_with(store, None)
55 }
56
57 fn open_with(store: Store, central: Option<PathBuf>) -> Result<Self> {
59 let mut brain = Self {
60 store,
61 tasks: Vec::new(),
62 tag_colors: TagColors::default(),
63 warnings: Vec::new(),
64 project: None,
65 central,
66 author: Author::Human,
67 };
68 brain.reload()?;
69 Ok(brain)
70 }
71
72 pub fn read_only(store: Store) -> Result<Self> {
76 let (tasks, warnings) = store.load()?;
77 let tag_colors = store.load_tag_colors()?;
78 Ok(Self { store, tasks, tag_colors, warnings, project: None, central: None, author: Author::Human })
79 }
80
81 pub fn open_in(store: Store, central: &Path) -> Result<Self> {
84 Self::open_with(store, Some(central.to_path_buf()))
85 }
86
87 pub fn reload(&mut self) -> Result<()> {
89 let (tasks, warnings) = self.store.load()?;
90 self.tasks = tasks;
91 self.warnings = warnings;
92 self.keep_one_session_running()?;
94 self.keep_one_task_doing()?;
95 self.drop_dangling_links()?;
96 self.remember_highest_id()?;
97 self.tag_colors = self.store.load_tag_colors()?;
98 self.project = projects::info(self.store.dir(), self.central.as_deref());
99 self.drop_dangling_repo_links()?;
100 self.sync_tag_colors()
101 }
102
103 pub fn task(&self, id: u32) -> Option<&Task> {
105 self.tasks.iter().find(|t| t.id == id)
106 }
107
108 pub(crate) fn find(&self, id: u32) -> Option<usize> {
110 self.tasks.iter().position(|t| t.id == id)
111 }
112
113 pub(crate) fn index_of(&self, id: u32) -> Result<usize, ApiError> {
115 self.find(id).ok_or(ApiError::NotFound(id))
116 }
117
118 pub fn running(&self) -> Option<&Task> {
120 self.tasks.iter().find(|t| t.is_running())
121 }
122
123 pub fn column(&self, status: Status) -> Vec<&Task> {
125 let mut tasks: Vec<&Task> = self.tasks.iter().filter(|t| t.is_task() && t.status == status).collect();
126 tasks.sort_by(|a, b| b.status_since().cmp(&a.status_since()).then(b.id.cmp(&a.id)));
127 tasks
128 }
129
130 pub fn backlinks(&self, id: u32) -> Vec<&Task> {
132 let mut from: Vec<&Task> = self.tasks.iter().filter(|t| t.links.contains(&id)).collect();
133 from.sort_by_key(|t| t.id);
134 from
135 }
136
137 pub fn related(&self, id: u32) -> Vec<&Task> {
140 let links = self.task(id).map_or(&[][..], |t| t.links.as_slice());
141 let mut related: Vec<&Task> =
142 self.tasks.iter().filter(|t| t.id != id && (links.contains(&t.id) || t.links.contains(&id))).collect();
143 related.sort_by_key(|t| (related_rank(t.kind), std::cmp::Reverse(t.id)));
144 related
145 }
146
147 pub fn are_related(&self, a: u32, b: u32) -> bool {
149 let links = |x: u32, y: u32| self.task(x).is_some_and(|t| t.links.contains(&y));
150 links(a, b) || links(b, a)
151 }
152
153 pub fn sibling(&self, repo: &str) -> Option<Brain> {
157 let (_, dir) = self.project.as_ref()?.siblings.iter().find(|(name, _)| name == repo)?;
158 if !dir.is_dir() {
159 return None;
160 }
161 Brain::read_only(Store::at(dir).ok()?).ok()
162 }
163
164 pub fn repo_related(&self, id: u32) -> Vec<(String, Task)> {
168 let Some(project) = &self.project else { return Vec::new() };
169 let links = self.task(id).map_or(&[][..], |t| t.repo_links.as_slice());
170 let here = RepoLink { repo: project.repo.clone(), id };
171 let mut related = Vec::new();
172 for (name, _) in &project.siblings {
173 let Some(other) = self.sibling(name) else { continue };
174 let mut found: Vec<Task> = other
175 .tasks
176 .into_iter()
177 .filter(|t| links.contains(&RepoLink { repo: name.clone(), id: t.id }) || t.repo_links.contains(&here))
178 .collect();
179 found.sort_by_key(|t| (related_rank(t.kind), std::cmp::Reverse(t.id)));
180 related.extend(found.into_iter().map(|t| (name.clone(), t)));
181 }
182 related
183 }
184
185 pub fn all_tags(&self) -> Vec<String> {
187 let mut tags: Vec<String> = self.tasks.iter().flat_map(|t| t.tags.iter().cloned()).collect();
188 tags.sort();
189 tags.dedup();
190 tags
191 }
192
193 fn take_id(&self) -> Result<u32> {
197 let highest = self.tasks.iter().map(|t| t.id).max().unwrap_or(0);
198 let id = highest.max(self.store.last_id()) + 1;
199 self.store.save_last_id(id)?;
200 Ok(id)
201 }
202
203 fn drop_dangling_repo_links(&mut self) -> Result<()> {
207 let mut repos: Vec<&str> = self.tasks.iter().flat_map(|t| &t.repo_links).map(|l| l.repo.as_str()).collect();
208 repos.sort_unstable();
209 repos.dedup();
210 let mut gone: Vec<RepoLink> = Vec::new();
211 for repo in repos {
212 let Some(other) = self.sibling(repo) else { continue };
213 for link in self.tasks.iter().flat_map(|t| &t.repo_links).filter(|l| l.repo == repo) {
214 if other.task(link.id).is_none() && !gone.contains(link) {
215 gone.push(link.clone());
216 }
217 }
218 }
219 for link in &gone {
220 self.drop_repo_links_to(link)?;
221 }
222 Ok(())
223 }
224
225 fn remember_highest_id(&self) -> Result<()> {
228 let highest = self.tasks.iter().map(|t| t.id).max().unwrap_or(0);
229 if highest > self.store.last_id() {
230 self.store.save_last_id(highest)?;
231 }
232 Ok(())
233 }
234
235 fn keep_one_task_doing(&mut self) -> Result<()> {
238 let doing = |t: &&mut Task| t.is_task() && t.status == Status::Doing;
239 let Some(latest) = self.tasks.iter_mut().filter(doing).map(|t| (t.status_since(), t.id)).max() else {
240 return Ok(());
241 };
242 let now = now();
243 for task in self.tasks.iter_mut().filter(doing) {
244 if (task.status_since(), task.id) != latest {
245 task.set_status(Status::Parked, now, self.author);
246 self.store.save(task)?;
247 }
248 }
249 Ok(())
250 }
251
252 fn drop_dangling_links(&mut self) -> Result<()> {
255 let ids: std::collections::HashSet<u32> = self.tasks.iter().map(|t| t.id).collect();
256 for task in &mut self.tasks {
257 let before = task.links.len();
258 task.links.retain(|link| ids.contains(link));
259 if task.links.len() != before {
260 self.store.save(task)?;
261 }
262 }
263 Ok(())
264 }
265
266 fn keep_one_session_running(&mut self) -> Result<()> {
269 let started = |t: &Task| t.sessions.last().map(|s| s.start);
270 let Some(latest) = self.tasks.iter().filter(|t| t.is_running()).filter_map(started).max() else {
271 return Ok(());
272 };
273 for task in &mut self.tasks {
274 if task.is_running() && started(task) != Some(latest) {
275 task.stop_session(latest);
276 self.store.save(task)?;
277 }
278 }
279 Ok(())
280 }
281
282 fn sync_tag_colors(&mut self) -> Result<()> {
284 let mut by_age: Vec<&Task> = self.tasks.iter().collect();
285 by_age.sort_by_key(|t| t.id);
286 if self.tag_colors.assign(by_age.iter().flat_map(|t| &t.tags)) {
287 self.store.save_tag_colors(&self.tag_colors)?;
288 }
289 Ok(())
290 }
291}
292
293fn related_rank(kind: Kind) -> u8 {
295 match kind {
296 Kind::Note => 0,
297 Kind::Decision => 1,
298 Kind::Task => 2,
299 }
300}