mod actions;
mod calendar;
mod report;
#[cfg(test)]
#[path = "../../tests/brain.rs"]
mod tests;
pub use actions::NewEntry;
pub use calendar::{Calendar, CalendarView, Day, Event, Show, What};
pub use report::{Group, Report, ReportRow, ReportView, Slot};
use crate::api::ApiError;
use crate::model::{Author, Kind, RepoLink, Status, Task, now};
use crate::projects::{self, ProjectInfo};
use crate::store::Store;
use crate::tags::TagColors;
use anyhow::Result;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct Brain {
pub store: Store,
pub tasks: Vec<Task>,
pub tag_colors: TagColors,
pub warnings: Vec<String>,
pub project: Option<ProjectInfo>,
pub central: Option<PathBuf>,
pub author: Author,
}
impl Brain {
pub fn open(store: Store) -> Result<Self> {
Self::open_with(store, None)
}
fn open_with(store: Store, central: Option<PathBuf>) -> Result<Self> {
let mut brain = Self {
store,
tasks: Vec::new(),
tag_colors: TagColors::default(),
warnings: Vec::new(),
project: None,
central,
author: Author::Human,
};
brain.reload()?;
Ok(brain)
}
pub fn read_only(store: Store) -> Result<Self> {
let (tasks, warnings) = store.load()?;
let tag_colors = store.load_tag_colors()?;
Ok(Self { store, tasks, tag_colors, warnings, project: None, central: None, author: Author::Human })
}
pub fn open_in(store: Store, central: &Path) -> Result<Self> {
Self::open_with(store, Some(central.to_path_buf()))
}
pub fn reload(&mut self) -> Result<()> {
let (tasks, warnings) = self.store.load()?;
self.tasks = tasks;
self.warnings = warnings;
self.keep_one_person_doing()?;
self.drop_dangling_links()?;
self.remember_highest_id()?;
self.tag_colors = self.store.load_tag_colors()?;
self.project = projects::info(self.store.dir(), self.central.as_deref());
self.drop_dangling_repo_links()?;
self.sync_tag_colors()
}
pub fn task(&self, id: u32) -> Option<&Task> {
self.tasks.iter().find(|t| t.id == id)
}
pub(crate) fn find(&self, id: u32) -> Option<usize> {
self.tasks.iter().position(|t| t.id == id)
}
pub(crate) fn index_of(&self, id: u32) -> Result<usize, ApiError> {
self.find(id).ok_or(ApiError::NotFound(id))
}
pub fn running(&self) -> impl Iterator<Item = &Task> {
self.tasks.iter().filter(|t| t.is_running())
}
pub fn person_doing(&self) -> Option<u32> {
self.tasks.iter().find(|t| started_by_person(t)).map(|t| t.id)
}
pub fn column(&self, status: Status) -> Vec<&Task> {
let mut tasks: Vec<&Task> = self.tasks.iter().filter(|t| t.is_task() && t.status == status).collect();
tasks.sort_by(|a, b| b.status_since().cmp(&a.status_since()).then(b.id.cmp(&a.id)));
tasks
}
pub fn backlinks(&self, id: u32) -> Vec<&Task> {
let mut from: Vec<&Task> = self.tasks.iter().filter(|t| t.links.contains(&id)).collect();
from.sort_by_key(|t| t.id);
from
}
pub fn related(&self, id: u32) -> Vec<&Task> {
let links = self.task(id).map_or(&[][..], |t| t.links.as_slice());
let mut related: Vec<&Task> =
self.tasks.iter().filter(|t| t.id != id && (links.contains(&t.id) || t.links.contains(&id))).collect();
related.sort_by_key(|t| (related_rank(t.kind), std::cmp::Reverse(t.id)));
related
}
pub fn are_related(&self, a: u32, b: u32) -> bool {
let links = |x: u32, y: u32| self.task(x).is_some_and(|t| t.links.contains(&y));
links(a, b) || links(b, a)
}
pub fn sibling(&self, repo: &str) -> Option<Brain> {
let (_, dir) = self.project.as_ref()?.siblings.iter().find(|(name, _)| name == repo)?;
if !dir.is_dir() {
return None;
}
Brain::read_only(Store::at(dir).ok()?).ok()
}
pub fn repo_related(&self, id: u32) -> Vec<(String, Task)> {
let Some(project) = &self.project else { return Vec::new() };
let links = self.task(id).map_or(&[][..], |t| t.repo_links.as_slice());
let here = RepoLink { repo: project.repo.clone(), id };
let mut related = Vec::new();
for (name, _) in &project.siblings {
let Some(other) = self.sibling(name) else { continue };
let mut found: Vec<Task> = other
.tasks
.into_iter()
.filter(|t| links.contains(&RepoLink { repo: name.clone(), id: t.id }) || t.repo_links.contains(&here))
.collect();
found.sort_by_key(|t| (related_rank(t.kind), std::cmp::Reverse(t.id)));
related.extend(found.into_iter().map(|t| (name.clone(), t)));
}
related
}
pub fn all_tags(&self) -> Vec<String> {
let mut tags: Vec<String> = self.tasks.iter().flat_map(|t| t.tags.iter().cloned()).collect();
tags.sort();
tags.dedup();
tags
}
fn take_id(&self) -> Result<u32> {
let highest = self.tasks.iter().map(|t| t.id).max().unwrap_or(0);
let id = highest.max(self.store.last_id()) + 1;
self.store.save_last_id(id)?;
Ok(id)
}
fn drop_dangling_repo_links(&mut self) -> Result<()> {
let mut repos: Vec<&str> = self.tasks.iter().flat_map(|t| &t.repo_links).map(|l| l.repo.as_str()).collect();
repos.sort_unstable();
repos.dedup();
let mut gone: Vec<RepoLink> = Vec::new();
for repo in repos {
let Some(other) = self.sibling(repo) else { continue };
for link in self.tasks.iter().flat_map(|t| &t.repo_links).filter(|l| l.repo == repo) {
if other.task(link.id).is_none() && !gone.contains(link) {
gone.push(link.clone());
}
}
}
for link in &gone {
self.drop_repo_links_to(link)?;
}
Ok(())
}
fn remember_highest_id(&self) -> Result<()> {
let highest = self.tasks.iter().map(|t| t.id).max().unwrap_or(0);
if highest > self.store.last_id() {
self.store.save_last_id(highest)?;
}
Ok(())
}
fn keep_one_person_doing(&mut self) -> Result<()> {
let doing = |t: &&mut Task| started_by_person(t);
let Some(latest) = self.tasks.iter_mut().filter(doing).map(|t| (t.status_since(), t.id)).max() else {
return Ok(());
};
let now = now();
for task in self.tasks.iter_mut().filter(doing) {
if (task.status_since(), task.id) != latest {
task.set_status(Status::Parked, now, self.author);
self.store.save(task)?;
}
}
Ok(())
}
fn drop_dangling_links(&mut self) -> Result<()> {
let ids: std::collections::HashSet<u32> = self.tasks.iter().map(|t| t.id).collect();
for task in &mut self.tasks {
let before = task.links.len();
task.links.retain(|link| ids.contains(link));
if task.links.len() != before {
self.store.save(task)?;
}
}
Ok(())
}
fn sync_tag_colors(&mut self) -> Result<()> {
let mut by_age: Vec<&Task> = self.tasks.iter().collect();
by_age.sort_by_key(|t| t.id);
if self.tag_colors.assign(by_age.iter().flat_map(|t| &t.tags)) {
self.store.save_tag_colors(&self.tag_colors)?;
}
Ok(())
}
}
fn started_by_person(task: &Task) -> bool {
task.is_task() && task.status == Status::Doing && task.started_by() == Author::Human
}
fn related_rank(kind: Kind) -> u8 {
match kind {
Kind::Note => 0,
Kind::Decision => 1,
Kind::Task => 2,
}
}