mod project_builder;
mod project_guard;
mod task_builder;
mod task_guard;
mod vault_index;
use std::{collections::HashMap, fs, path::PathBuf};
use bincode::config;
use chrono::{DateTime, Utc};
use eyre::{Result, eyre};
use serde::{Deserialize, Serialize};
pub use vault_index::VaultIndex;
use crate::{
path,
project::Project,
task::{Task, TaskList},
vault::{
project_builder::ProjectBuilder, project_guard::ProjectGuard, task_builder::TaskBuilder,
task_guard::TaskGuard,
},
};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Vault {
pub created_at: DateTime<Utc>,
pub index: VaultIndex,
#[serde(skip)]
pub path: PathBuf,
pub projects: HashMap<u32, Project>,
pub task_lists: HashMap<String, TaskList>,
pub tasks: HashMap<u32, Task>,
pub updated_at: DateTime<Utc>,
}
impl Vault {
pub fn load() -> Result<Self> {
let vault_path = path::vault_file();
if !vault_path.exists() {
return Ok(Self::default());
}
let bytes = fs::read(&vault_path)?;
let (vault, _) = bincode::serde::decode_from_slice(&bytes, config::standard())?;
let mut vault: Vault = vault;
vault.path = vault_path;
Ok(vault)
}
pub fn add_project(&'_ mut self, name: impl Into<String>) -> ProjectBuilder<'_> {
ProjectBuilder::new(self, name)
}
pub fn add_task(&'_ mut self, subject: impl Into<String>) -> TaskBuilder<'_> {
TaskBuilder::new(self, subject)
}
pub fn delete_project(&mut self, project_id: u32) -> Result<()> {
if !self.projects.contains_key(&project_id) {
return Err(eyre!("Project {} not found", project_id));
}
let project = &self.projects[&project_id];
let task_ids = project.task_ids.clone();
for task_id in task_ids {
if let Some(task) = self.tasks.get_mut(&task_id) {
task.friendly_id = self.index.allocate_canonical_id().to_string();
}
self.index.remove_task_from_project(task_id);
}
let project = self.projects[&project_id].clone();
self.projects.remove(&project_id);
self.index.remove_project(&project);
self.updated_at = Utc::now();
self.save()?;
Ok(())
}
pub fn delete_task(&mut self, task_id: u32) -> Result<()> {
if !self.tasks.contains_key(&task_id) {
return Err(eyre!("Task {} not found", task_id));
}
if let Some(list_name) = self.index.get_list_name_for_task(task_id)
&& let Some(list) = self.task_lists.get_mut(list_name) {
list.task_ids.retain(|&id| id != task_id);
}
if let Some(project_id) = self.index.get_project_id_for_task(task_id)
&& let Some(project) = self.projects.get_mut(&project_id) {
project.task_ids.retain(|&id| id != task_id);
}
let task = self.tasks[&task_id].clone();
self.tasks.remove(&task_id);
self.index.remove_task(&task);
self.updated_at = Utc::now();
self.save()?;
Ok(())
}
pub fn get_project(&self, id: u32) -> Result<Project> {
self
.projects
.get(&id)
.cloned()
.ok_or_else(|| eyre!("Project not found"))
}
pub fn get_project_by_key(&self, key: impl Into<String>) -> Result<Project> {
let key = key.into();
let id = self
.index
.get_canonical_project_id_from_key(&key)
.ok_or_else(|| eyre!("Project not found"))?;
self.get_project(id)
}
pub fn get_project_by_key_mut(&mut self, key: impl Into<String>) -> Result<ProjectGuard<'_>> {
let project = self.get_project_by_key(key)?;
self.get_project_mut(project.id)
}
pub fn get_project_mut(&mut self, id: u32) -> Result<ProjectGuard<'_>> {
ProjectGuard::new(id, self)
}
pub fn get_task(&self, id: u32) -> Result<Task> {
self
.tasks
.get(&id)
.cloned()
.ok_or_else(|| eyre!("Task not found"))
}
pub fn get_task_by_friendly_id(&self, id: impl Into<String>) -> Result<Task> {
let friendly_id = id.into();
let canonical_id = self
.index
.get_canonical_id_from_friendly_id(&friendly_id)
.ok_or_else(|| eyre!("Task not found"))?;
self.get_task(canonical_id)
}
pub fn get_task_by_friendly_id_mut(&mut self, id: impl Into<String>) -> Result<TaskGuard<'_>> {
let task = self.get_task_by_friendly_id(id)?;
self.get_task_mut(task.id)
}
pub fn get_task_mut(&mut self, id: u32) -> Result<TaskGuard<'_>> {
TaskGuard::new(id, self)
}
pub fn list_all_projects(&self) -> Vec<Project> {
self.projects.values().cloned().collect()
}
pub fn list_tasks_in_list(&self, list_name: &str) -> Result<Vec<Task>> {
let task_list = self
.task_lists
.get(list_name)
.ok_or_else(|| eyre!("List '{}' not found", list_name))?;
let tasks = task_list
.task_ids
.iter()
.filter_map(|&id| self.tasks.get(&id).cloned())
.collect();
Ok(tasks)
}
pub fn list_tasks_in_project(&self, project_id: u32) -> Result<Vec<Task>> {
let project = self.get_project(project_id)?;
let tasks = project
.task_ids
.iter()
.filter_map(|&id| self.tasks.get(&id).cloned())
.collect();
Ok(tasks)
}
pub fn move_multiple_tasks_to_list(
&mut self,
task_ids: &[u32],
list_name: impl Into<String>,
) -> Result<()> {
let list_name = list_name.into();
for &task_id in task_ids {
if !self.tasks.contains_key(&task_id) {
return Err(eyre!("Task {} not found", task_id));
}
}
if !self.task_lists.contains_key(&list_name) {
return Err(eyre!("Invalid list '{}'", &list_name));
}
for &task_id in task_ids {
let old_list_name = self.index.get_list_name_for_task(task_id);
if let Some(old_list_name) = old_list_name
&& let Some(list) = self.task_lists.get_mut(old_list_name) {
list.task_ids.retain(|&id| id != task_id);
}
self.index.move_task_to_list(task_id, &list_name);
}
self
.task_lists
.get_mut(&list_name)
.unwrap()
.task_ids
.extend_from_slice(task_ids);
self.updated_at = Utc::now();
self.save()?;
Ok(())
}
pub fn move_task_to_list(&mut self, task_id: u32, list_name: impl Into<String>) -> Result<()> {
if !self.tasks.contains_key(&task_id) {
return Err(eyre!("Task {} not found", task_id));
}
let old_list_name = self.index.get_list_name_for_task(task_id);
let new_list_name = list_name.into();
if !self.task_lists.contains_key(&new_list_name) {
return Err(eyre!("Invalid list '{}'", &new_list_name));
}
self
.task_lists
.get_mut(&new_list_name)
.unwrap()
.task_ids
.push(task_id);
if let Some(old_list_name) = old_list_name
&& let Some(list) = self.task_lists.get_mut(old_list_name) {
list.task_ids.retain(|&id| id != task_id);
}
self.index.move_task_to_list(task_id, &new_list_name);
self.updated_at = Utc::now();
self.save()?;
Ok(())
}
pub fn move_task_to_project(&mut self, task_id: u32, project_id: u32) -> Result<()> {
if !self.tasks.contains_key(&task_id) {
return Err(eyre!("Task {} not found", task_id));
}
if !self.projects.contains_key(&project_id) {
return Err(eyre!("Project {} not found", project_id));
}
let old_project_id = self.index.get_project_id_for_task(task_id);
let project_key = self.projects[&project_id].key.clone();
let new_friendly_id = format!(
"{}-{}",
project_key,
self.index.allocate_friendly_id_for_project(project_id)
);
self.tasks.get_mut(&task_id).unwrap().friendly_id = new_friendly_id;
self
.projects
.get_mut(&project_id)
.unwrap()
.task_ids
.push(task_id);
if let Some(old_project_id) = old_project_id
&& let Some(old_project) = self.projects.get_mut(&old_project_id)
{
old_project.task_ids.retain(|&id| id != task_id);
}
self.index.move_task_to_project(task_id, project_id);
self.updated_at = Utc::now();
self.save()?;
Ok(())
}
pub fn save(&self) -> Result<()> {
if let Some(parent) = self.path.parent() {
fs::create_dir_all(parent)?;
}
let bytes = bincode::serde::encode_to_vec(self, config::standard())?;
fs::write(&self.path, bytes)?;
Ok(())
}
}
impl Default for Vault {
fn default() -> Self {
let mut task_lists = HashMap::new();
for name in &["today", "next", "someday"] {
let list = TaskList::new(*name);
task_lists.insert((*name).to_string(), list);
}
let now = Utc::now();
Self {
created_at: now,
index: VaultIndex::default(),
path: path::vault_file(),
projects: HashMap::new(),
task_lists,
tasks: HashMap::new(),
updated_at: now,
}
}
}