feat: Add TaskWarrior integration with TaskChampion

- Error handling with TaskError enum and TaskResult<T> alias
- Data models: Task, TaskPriority, TaskStatus, TaskAnnotation
- Advanced filtering: TaskFilter with hierarchical projects, tags
  (AND/OR), priority, due dates, and search
- TaskService: Full CRUD, tags, projects, annotations, dependencies,
  sync
This commit is contained in:
Ignacio Perez
2025-12-25 15:47:38 -03:00
parent 9ba1191c47
commit 33769a4686
6 changed files with 1097 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
use std::fmt;
pub enum TaskError {
Storage(String),
NotFound(uuid::Uuid),
InvalidTag(String),
InvalidProject(String),
InvalidPriority(String),
InvalidDue(String),
InvalidWait(String),
InvalidAnnotation(String),
InvalidDependency(String),
}
impl fmt::Display for TaskError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
TaskError::Storage(msg) => write!(f, "Storage error: {}", msg),
TaskError::NotFound(id) => write!(f, "Task not found: {}", id),
TaskError::InvalidTag(tag) => write!(f, "Invalid tag: {}", tag),
TaskError::InvalidProject(project) => write!(f, "Invalid project: {}", project),
TaskError::InvalidPriority(priority) => write!(f, "Invalid priority: {}", priority),
TaskError::InvalidDue(due) => write!(f, "Invalid due date: {}", due),
TaskError::InvalidWait(wait) => write!(f, "Invalid wait date: {}", wait),
TaskError::InvalidAnnotation(annotation) => {
write!(f, "Invalid annotation: {}", annotation)
}
TaskError::InvalidDependency(dependency) => {
write!(f, "Invalid dependency: {}", dependency)
}
}
}
}
pub type TaskResult<T> = Result<T, TaskError>;
+266
View File
@@ -0,0 +1,266 @@
use std::collections::HashSet;
use chrono::{DateTime, Utc};
use super::model::{Task, TaskPriority, TaskStatus};
use crate::models::{DueFilter, FilterState, PriorityFilter, StatusFilter};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TagsFilterMode {
#[default]
And,
Or,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DueDateFilter {
Overdue,
Today,
ThisWeek,
NoDate,
Before(DateTime<Utc>),
After(DateTime<Utc>),
}
#[derive(Debug, Clone, Default)]
pub struct TaskFilter {
pub status: Option<TaskStatus>,
pub project: Option<String>,
pub project_include_children: bool,
pub tags: HashSet<String>,
pub tags_mode: TagsFilterMode,
pub priority: Option<TaskPriority>,
pub due_filter: Option<DueDateFilter>,
pub search_text: Option<String>,
pub is_active: Option<bool>,
pub is_blocked: Option<bool>,
}
impl TaskFilter {
pub fn new() -> Self {
Self::default()
}
pub fn with_status(mut self, status: TaskStatus) -> Self {
self.status = Some(status);
self
}
pub fn with_project(mut self, project: String, include_children: bool) -> Self {
self.project = Some(project);
self.project_include_children = include_children;
self
}
pub fn with_tags(mut self, tags: HashSet<String>, mode: TagsFilterMode) -> Self {
self.tags = tags;
self.tags_mode = mode;
self
}
pub fn with_priority(mut self, priority: TaskPriority) -> Self {
self.priority = Some(priority);
self
}
pub fn with_due(mut self, filter: DueDateFilter) -> Self {
self.due_filter = Some(filter);
self
}
pub fn with_search(mut self, text: String) -> Self {
if !text.is_empty() {
self.search_text = Some(text.to_lowercase());
}
self
}
}
impl From<&FilterState> for TaskFilter {
fn from(state: &FilterState) -> Self {
let mut filter = Self::new();
filter.status = match state.status_filter {
StatusFilter::All => None,
StatusFilter::Pending => Some(TaskStatus::Pending),
StatusFilter::Completed => Some(TaskStatus::Completed),
StatusFilter::Waiting => Some(TaskStatus::Pending),
StatusFilter::Deleted => Some(TaskStatus::Deleted),
};
if let Some(ref project) = state.selected_project {
filter.project = Some(project.clone());
filter.project_include_children = true;
}
if !state.active_tags.is_empty() {
filter.tags = state.active_tags.clone();
filter.tags_mode = TagsFilterMode::And;
}
filter.priority = match state.priority_filter {
PriorityFilter::All => None,
PriorityFilter::High => Some(TaskPriority::High),
PriorityFilter::Medium => Some(TaskPriority::Medium),
PriorityFilter::Low => Some(TaskPriority::Low),
PriorityFilter::None => Some(TaskPriority::None),
};
filter.due_filter = match state.due_filter {
DueFilter::All => None,
DueFilter::Overdue => Some(DueDateFilter::Overdue),
DueFilter::Today => Some(DueDateFilter::Today),
DueFilter::ThisWeek => Some(DueDateFilter::ThisWeek),
DueFilter::NoDate => Some(DueDateFilter::NoDate),
};
if !state.search_text.is_empty() {
filter.search_text = Some(state.search_text.to_lowercase());
}
filter
}
}
impl From<FilterState> for TaskFilter {
fn from(state: FilterState) -> Self {
Self::from(&state)
}
}
impl TaskFilter {
pub fn matches(&self, task: &Task) -> bool {
if let Some(status) = &self.status {
match status {
TaskStatus::Pending => {
if !matches!(task.status, TaskStatus::Pending) {
return false;
}
if task.wait.map(|w| w > Utc::now()).unwrap_or(false) {
return false;
}
}
_ => {
if &task.status != status {
return false;
}
}
}
}
if let Some(project) = &self.project {
match &task.project {
None => return false,
Some(task_project) => {
if self.project_include_children {
if !task_project.starts_with(project) {
return false;
}
} else if task_project != project {
return false;
}
}
}
}
if !self.tags.is_empty() {
match self.tags_mode {
TagsFilterMode::And => {
for tag in &self.tags {
if !task.tags.contains(tag) {
return false;
}
}
}
TagsFilterMode::Or => {
let has_any = self.tags.iter().any(|t| task.tags.contains(t));
if !has_any {
return false;
}
}
}
}
if let Some(priority) = &self.priority {
if &task.priority != priority {
return false;
}
}
if let Some(due_filter) = &self.due_filter {
match due_filter {
DueDateFilter::Overdue => {
if !task.is_overdue() {
return false;
}
}
DueDateFilter::Today => {
if !task.is_due_today() {
return false;
}
}
DueDateFilter::ThisWeek => {
let is_due_this_week = task
.due
.map(|d| {
let now = Utc::now();
let week_end = now + chrono::Duration::days(7);
d >= now && d <= week_end
})
.unwrap_or(false);
if !is_due_this_week {
return false;
}
}
DueDateFilter::NoDate => {
if task.due.is_some() {
return false;
}
}
DueDateFilter::Before(dt) => {
if task.due.map(|d| d >= *dt).unwrap_or(true) {
return false;
}
}
DueDateFilter::After(dt) => {
if task.due.map(|d| d <= *dt).unwrap_or(true) {
return false;
}
}
}
}
if let Some(search) = &self.search_text {
let desc_match = task.description.to_lowercase().contains(search);
let proj_match = task
.project
.as_ref()
.map(|p| p.to_lowercase().contains(search))
.unwrap_or(false);
let tag_match = task.tags.iter().any(|t| t.to_lowercase().contains(search));
if !desc_match && !proj_match && !tag_match {
return false;
}
}
if let Some(is_active) = self.is_active {
if task.is_active != is_active {
return false;
}
}
if let Some(is_blocked) = self.is_blocked {
if task.is_blocked != is_blocked {
return false;
}
}
true
}
pub fn apply(&self, tasks: &[Task]) -> Vec<Task> {
tasks.iter().filter(|t| self.matches(t)).cloned().collect()
}
}
+9
View File
@@ -0,0 +1,9 @@
pub mod error;
pub mod filter;
pub mod model;
pub mod service;
pub use error::{TaskError, TaskResult};
pub use filter::{DueDateFilter, TagsFilterMode, TaskFilter};
pub use model::{Task, TaskAnnotation, TaskPriority, TaskStatus, TaskUpdate};
pub use service::{SyncResult, TaskService};
+216
View File
@@ -0,0 +1,216 @@
use std::collections::HashSet;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TaskPriority {
High,
Medium,
Low,
None,
}
impl Default for TaskPriority {
fn default() -> Self {
TaskPriority::None
}
}
impl std::fmt::Display for TaskPriority {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TaskPriority::High => write!(f, "High"),
TaskPriority::Medium => write!(f, "Medium"),
TaskPriority::Low => write!(f, "Low"),
TaskPriority::None => write!(f, "None"),
}
}
}
impl From<&str> for TaskPriority {
fn from(s: &str) -> Self {
match s {
"high" | "High" | "H" | "h" => TaskPriority::High,
"medium" | "Medium" | "M" | "m" => TaskPriority::Medium,
"low" | "Low" | "L" | "l" => TaskPriority::Low,
_ => TaskPriority::None,
}
}
}
impl From<String> for TaskPriority {
fn from(s: String) -> Self {
Self::from(s.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TaskStatus {
Pending,
Completed,
Deleted,
Unknown(String),
Recurring,
}
impl Default for TaskStatus {
fn default() -> Self {
TaskStatus::Pending
}
}
impl From<&str> for TaskStatus {
fn from(s: &str) -> Self {
match s {
"pending" | "Pending" | "P" | "p" => TaskStatus::Pending,
"completed" | "Completed" | "C" | "c" => TaskStatus::Completed,
"deleted" | "Deleted" | "D" | "d" => TaskStatus::Deleted,
"recurring" | "Recurring" | "R" | "r" => TaskStatus::Recurring,
_ => TaskStatus::Unknown(String::new()),
}
}
}
impl From<String> for TaskStatus {
fn from(s: String) -> Self {
Self::from(s.as_str())
}
}
impl From<taskchampion::Status> for TaskStatus {
fn from(status: taskchampion::Status) -> Self {
match status {
taskchampion::Status::Pending => TaskStatus::Pending,
taskchampion::Status::Completed => TaskStatus::Completed,
taskchampion::Status::Deleted => TaskStatus::Deleted,
taskchampion::Status::Unknown(reason) => TaskStatus::Unknown(reason),
taskchampion::Status::Recurring => TaskStatus::Recurring,
}
}
}
impl std::fmt::Display for TaskStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TaskStatus::Pending => write!(f, "Pending"),
TaskStatus::Completed => write!(f, "Completed"),
TaskStatus::Deleted => write!(f, "Deleted"),
TaskStatus::Unknown(reason) => write!(f, "Unknown ({})", reason),
TaskStatus::Recurring => write!(f, "Recurring"),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TaskAnnotation {
pub entry: DateTime<Utc>,
pub content: String,
}
impl From<taskchampion::Annotation> for TaskAnnotation {
fn from(annotation: taskchampion::Annotation) -> Self {
TaskAnnotation {
entry: annotation.entry,
content: annotation.description,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Task {
pub uuid: uuid::Uuid,
pub status: TaskStatus,
pub description: String,
pub project: Option<String>,
pub priority: TaskPriority,
pub tags: HashSet<String>,
pub due: Option<DateTime<Utc>>,
pub wait: Option<DateTime<Utc>>,
pub entry: Option<DateTime<Utc>>,
pub modified: Option<DateTime<Utc>>,
pub annotations: Vec<TaskAnnotation>,
pub dependencies: HashSet<uuid::Uuid>,
pub is_active: bool,
pub is_blocked: bool,
pub working_id: Option<usize>,
}
impl Task {
pub fn new(
uuid: uuid::Uuid,
status: TaskStatus,
description: String,
project: Option<String>,
priority: TaskPriority,
tags: HashSet<String>,
due: Option<DateTime<Utc>>,
wait: Option<DateTime<Utc>>,
entry: Option<DateTime<Utc>>,
modified: Option<DateTime<Utc>>,
annotations: Vec<TaskAnnotation>,
dependencies: HashSet<uuid::Uuid>,
is_active: bool,
is_blocked: bool,
working_id: Option<usize>,
) -> Self {
Self {
uuid,
status,
description,
project,
priority,
tags,
due,
wait,
entry,
modified,
annotations,
dependencies,
is_active,
is_blocked,
working_id,
}
}
pub fn is_overdue(&self) -> bool {
self.due.map_or(false, |due| due < Utc::now())
}
pub fn is_due_today(&self) -> bool {
self.due
.map_or(false, |due| due.date_naive() == Utc::now().date_naive())
}
}
impl From<taskchampion::Task> for Task {
fn from(task: taskchampion::Task) -> Self {
Self {
uuid: task.get_uuid(),
status: task.get_status().into(),
description: task.get_description().to_string(),
project: task.get_value("project").map(|v| v.to_string()),
priority: task.get_priority().into(),
tags: task.get_tags().map(|t| t.to_string()).collect(),
due: task.get_due().map(Into::into),
wait: task.get_wait().map(Into::into),
entry: task.get_entry().map(Into::into),
modified: task.get_modified().map(Into::into),
annotations: task.get_annotations().map(Into::into).collect(),
dependencies: task.get_dependencies().map(Into::into).collect(),
is_active: task.is_active(),
is_blocked: task.is_blocked(),
working_id: None,
}
}
}
pub struct TaskUpdate {
pub description: Option<String>,
pub project: Option<String>,
pub priority: Option<String>,
pub tags: Option<HashSet<String>>,
pub due: Option<DateTime<Utc>>,
pub wait: Option<DateTime<Utc>>,
pub annotations: Option<Vec<String>>,
pub dependencies: Option<Vec<String>>,
}
+570
View File
@@ -0,0 +1,570 @@
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use chrono::{DateTime, Utc};
use taskchampion::{
Operations, Replica, ServerConfig, Status, StorageConfig, Tag, storage::AccessMode,
};
use uuid::Uuid;
use super::error::{TaskError, TaskResult};
use super::filter::TaskFilter;
use super::model::{Task, TaskPriority, TaskStatus};
pub struct TaskService {
replica: Replica,
taskdb_dir: PathBuf,
}
impl TaskService {
pub fn new() -> TaskResult<Self> {
let taskdb_dir = dirs::home_dir()
.ok_or_else(|| TaskError::Storage("Cannot find home directory".into()))?
.join(".task");
Self::with_path(taskdb_dir)
}
pub fn with_path(taskdb_dir: PathBuf) -> TaskResult<Self> {
let storage = StorageConfig::OnDisk {
taskdb_dir: taskdb_dir.clone(),
create_if_missing: true,
access_mode: AccessMode::ReadWrite,
};
let replica = Replica::new(
storage
.into_storage()
.map_err(|e| TaskError::Storage(e.to_string()))?,
);
Ok(Self {
replica,
taskdb_dir,
})
}
pub fn create_task(&mut self, description: String) -> TaskResult<Task> {
let uuid = Uuid::new_v4();
let mut ops = Operations::new();
let mut tc_task = self
.replica
.create_task(uuid, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
tc_task
.set_description(description, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.replica
.commit_operations(ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
let working_set = self
.replica
.working_set()
.map_err(|e| TaskError::Storage(e.to_string()))?;
let mut task: Task = tc_task.into();
task.working_id = working_set.by_uuid(uuid);
Ok(task)
}
pub fn get_task(&mut self, uuid: Uuid) -> TaskResult<Option<Task>> {
let tc_task = self
.replica
.get_task(uuid)
.map_err(|e| TaskError::Storage(e.to_string()))?;
match tc_task {
None => Ok(None),
Some(task) => {
let working_set = self
.replica
.working_set()
.map_err(|e| TaskError::Storage(e.to_string()))?;
let mut task: Task = task.into();
task.working_id = working_set.by_uuid(uuid);
Ok(Some(task))
}
}
}
pub fn get_all_tasks(&mut self) -> TaskResult<Vec<Task>> {
let all = self
.replica
.all_tasks()
.map_err(|e| TaskError::Storage(e.to_string()))?;
let working_set = self
.replica
.working_set()
.map_err(|e| TaskError::Storage(e.to_string()))?;
let tasks: Vec<Task> = all
.iter()
.map(|(uuid, tc_task)| {
let mut task: Task = tc_task.clone().into();
task.working_id = working_set.by_uuid(*uuid);
task
})
.collect();
Ok(tasks)
}
pub fn get_filtered_tasks(&mut self, filter: &TaskFilter) -> TaskResult<Vec<Task>> {
let all = self.get_all_tasks()?;
Ok(filter.apply(&all))
}
pub fn update_task(
&mut self,
uuid: Uuid,
description: Option<String>,
project: Option<Option<String>>,
priority: Option<String>,
tags: Option<HashSet<String>>,
due: Option<Option<DateTime<Utc>>>,
wait: Option<Option<DateTime<Utc>>>,
) -> TaskResult<Task> {
let mut ops = Operations::new();
let mut tc_task = self
.replica
.get_task(uuid)
.map_err(|e| TaskError::Storage(e.to_string()))?
.ok_or(TaskError::NotFound(uuid))?;
if let Some(desc) = description {
tc_task
.set_description(desc, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
}
if let Some(proj) = project {
tc_task
.set_value("project", proj, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
}
if let Some(pri) = priority {
tc_task
.set_priority(pri, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
}
if let Some(d) = due {
tc_task
.set_due(d, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
}
if let Some(w) = wait {
tc_task
.set_wait(w, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
}
if let Some(new_tags) = tags {
let current_tags: HashSet<String> = tc_task.get_tags().map(|t| t.to_string()).collect();
for tag_str in current_tags.difference(&new_tags) {
if let Ok(tag) = Tag::try_from(tag_str.as_str()) {
tc_task
.remove_tag(&tag, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
}
}
for tag_str in new_tags.difference(&current_tags) {
if let Ok(tag) = Tag::try_from(tag_str.as_str()) {
tc_task
.add_tag(&tag, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
}
}
}
self.replica
.commit_operations(ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.get_task(uuid)?.ok_or(TaskError::NotFound(uuid))
}
pub fn complete_task(&mut self, uuid: Uuid) -> TaskResult<Task> {
let mut ops = Operations::new();
let mut tc_task = self
.replica
.get_task(uuid)
.map_err(|e| TaskError::Storage(e.to_string()))?
.ok_or(TaskError::NotFound(uuid))?;
tc_task
.done(&mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.replica
.commit_operations(ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.get_task(uuid)?.ok_or(TaskError::NotFound(uuid))
}
pub fn reopen_task(&mut self, uuid: Uuid) -> TaskResult<Task> {
let mut ops = Operations::new();
let mut tc_task = self
.replica
.get_task(uuid)
.map_err(|e| TaskError::Storage(e.to_string()))?
.ok_or(TaskError::NotFound(uuid))?;
tc_task
.set_status(Status::Pending, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.replica
.commit_operations(ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.get_task(uuid)?.ok_or(TaskError::NotFound(uuid))
}
pub fn delete_task(&mut self, uuid: Uuid) -> TaskResult<()> {
let mut ops = Operations::new();
let mut tc_task = self
.replica
.get_task(uuid)
.map_err(|e| TaskError::Storage(e.to_string()))?
.ok_or(TaskError::NotFound(uuid))?;
tc_task
.set_status(Status::Deleted, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.replica
.commit_operations(ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
Ok(())
}
pub fn start_task(&mut self, uuid: Uuid) -> TaskResult<Task> {
let mut ops = Operations::new();
let mut tc_task = self
.replica
.get_task(uuid)
.map_err(|e| TaskError::Storage(e.to_string()))?
.ok_or(TaskError::NotFound(uuid))?;
tc_task
.start(&mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.replica
.commit_operations(ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.get_task(uuid)?.ok_or(TaskError::NotFound(uuid))
}
pub fn stop_task(&mut self, uuid: Uuid) -> TaskResult<Task> {
let mut ops = Operations::new();
let mut tc_task = self
.replica
.get_task(uuid)
.map_err(|e| TaskError::Storage(e.to_string()))?
.ok_or(TaskError::NotFound(uuid))?;
tc_task
.stop(&mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.replica
.commit_operations(ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.get_task(uuid)?.ok_or(TaskError::NotFound(uuid))
}
pub fn add_tag(&mut self, uuid: Uuid, tag_str: &str) -> TaskResult<Task> {
let mut ops = Operations::new();
let mut tc_task = self
.replica
.get_task(uuid)
.map_err(|e| TaskError::Storage(e.to_string()))?
.ok_or(TaskError::NotFound(uuid))?;
let tag = Tag::try_from(tag_str).map_err(|_| TaskError::InvalidTag(tag_str.to_string()))?;
tc_task
.add_tag(&tag, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.replica
.commit_operations(ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.get_task(uuid)?.ok_or(TaskError::NotFound(uuid))
}
pub fn remove_tag(&mut self, uuid: Uuid, tag_str: &str) -> TaskResult<Task> {
let mut ops = Operations::new();
let mut tc_task = self
.replica
.get_task(uuid)
.map_err(|e| TaskError::Storage(e.to_string()))?
.ok_or(TaskError::NotFound(uuid))?;
let tag = Tag::try_from(tag_str).map_err(|_| TaskError::InvalidTag(tag_str.to_string()))?;
tc_task
.remove_tag(&tag, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.replica
.commit_operations(ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.get_task(uuid)?.ok_or(TaskError::NotFound(uuid))
}
pub fn list_tags(&mut self) -> TaskResult<Vec<(String, usize)>> {
let all_tasks = self.get_all_tasks()?;
let mut tag_counts: HashMap<String, usize> = HashMap::new();
for task in all_tasks {
if matches!(task.status, TaskStatus::Pending) {
for tag in task.tags {
*tag_counts.entry(tag).or_insert(0) += 1;
}
}
}
let mut stats: Vec<(String, usize)> = tag_counts.into_iter().collect();
stats.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
Ok(stats)
}
pub fn list_projects(&mut self) -> TaskResult<Vec<(String, usize, usize)>> {
let all_tasks = self.get_all_tasks()?;
let mut project_counts: HashMap<String, (usize, usize)> = HashMap::new(); // (total, pending)
for task in all_tasks {
if let Some(project) = &task.project {
let entry = project_counts.entry(project.clone()).or_insert((0, 0));
entry.0 += 1;
if matches!(task.status, TaskStatus::Pending) {
entry.1 += 1;
}
}
}
let stats: Vec<(String, usize, usize)> = project_counts
.into_iter()
.map(|(name, (task_count, pending_count))| (name, task_count, pending_count))
.collect();
Ok(stats)
}
pub fn get_projects_for_tree(&mut self) -> TaskResult<Vec<(String, usize)>> {
let stats = self.list_projects()?;
Ok(stats
.into_iter()
.map(|(name, _total, pending)| (name, pending))
.collect())
}
pub fn add_annotation(&mut self, uuid: Uuid, description: String) -> TaskResult<Task> {
let mut ops = Operations::new();
let mut tc_task = self
.replica
.get_task(uuid)
.map_err(|e| TaskError::Storage(e.to_string()))?
.ok_or(TaskError::NotFound(uuid))?;
let annotation = taskchampion::Annotation {
entry: Utc::now(),
description,
};
tc_task
.add_annotation(annotation, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.replica
.commit_operations(ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.get_task(uuid)?.ok_or(TaskError::NotFound(uuid))
}
pub fn remove_annotation(&mut self, uuid: Uuid, entry: DateTime<Utc>) -> TaskResult<Task> {
let mut ops = Operations::new();
let mut tc_task = self
.replica
.get_task(uuid)
.map_err(|e| TaskError::Storage(e.to_string()))?
.ok_or(TaskError::NotFound(uuid))?;
tc_task
.remove_annotation(entry, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.replica
.commit_operations(ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.get_task(uuid)?.ok_or(TaskError::NotFound(uuid))
}
pub fn add_dependency(&mut self, uuid: Uuid, depends_on: Uuid) -> TaskResult<Task> {
let mut ops = Operations::new();
let mut tc_task = self
.replica
.get_task(uuid)
.map_err(|e| TaskError::Storage(e.to_string()))?
.ok_or(TaskError::NotFound(uuid))?;
tc_task
.add_dependency(depends_on, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.replica
.commit_operations(ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.get_task(uuid)?.ok_or(TaskError::NotFound(uuid))
}
pub fn remove_dependency(&mut self, uuid: Uuid, depends_on: Uuid) -> TaskResult<Task> {
let mut ops = Operations::new();
let mut tc_task = self
.replica
.get_task(uuid)
.map_err(|e| TaskError::Storage(e.to_string()))?
.ok_or(TaskError::NotFound(uuid))?;
tc_task
.remove_dependency(depends_on, &mut ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.replica
.commit_operations(ops)
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.get_task(uuid)?.ok_or(TaskError::NotFound(uuid))
}
pub fn sync(&mut self) -> TaskResult<SyncResult> {
let server_dir = self.taskdb_dir.join("server");
if !server_dir.exists() {
return Ok(SyncResult {
success: false,
message: "Server not configured".to_string(),
local_ops_before: 0,
local_ops_after: 0,
});
}
let local_ops_before = self
.replica
.num_local_operations()
.map_err(|e| TaskError::Storage(e.to_string()))?;
let server_config = ServerConfig::Local { server_dir };
let mut server = server_config
.into_server()
.map_err(|e| TaskError::Storage(e.to_string()))?;
self.replica
.sync(&mut server, false)
.map_err(|e| TaskError::Storage(e.to_string()))?;
let local_ops_after = self
.replica
.num_local_operations()
.map_err(|e| TaskError::Storage(e.to_string()))?;
Ok(SyncResult {
success: true,
message: "Sync completed".to_string(),
local_ops_before,
local_ops_after,
})
}
pub fn pending_sync_operations(&mut self) -> TaskResult<usize> {
Ok(self
.replica
.num_local_operations()
.map_err(|e| TaskError::Storage(e.to_string()))?)
}
pub fn rebuild_working_set(&mut self, renumber: bool) -> TaskResult<()> {
self.replica
.rebuild_working_set(renumber)
.map_err(|e| TaskError::Storage(e.to_string()))?;
Ok(())
}
pub fn expire_tasks(&mut self) -> TaskResult<()> {
self.replica
.expire_tasks()
.map_err(|e| TaskError::Storage(e.to_string()))?;
Ok(())
}
pub fn working_set(&mut self) -> TaskResult<Vec<(usize, Task)>> {
let ws = self
.replica
.working_set()
.map_err(|e| TaskError::Storage(e.to_string()))?;
let mut result = Vec::new();
for (idx, uuid) in ws.iter() {
if let Some(task) = self.get_task(uuid)? {
result.push((idx, task));
}
}
Ok(result)
}
pub fn get_task_by_working_id(&mut self, id: usize) -> TaskResult<Option<Task>> {
let ws = self
.replica
.working_set()
.map_err(|e| TaskError::Storage(e.to_string()))?;
match ws.by_index(id) {
None => Ok(None),
Some(uuid) => self.get_task(uuid),
}
}
}
#[derive(Debug, Clone)]
pub struct SyncResult {
pub success: bool,
pub message: String,
pub local_ops_before: usize,
pub local_ops_after: usize,
}