Tasks TUI: - Add context support to TaskBackend interface (get_context, set_context, get_contexts methods) - Implement context methods in DstaskClient - Add Context section to FilterSidebar (above projects/tags) - Context changes persist via backend CLI Calendar TUI: - Remove duplicate header from InvitesPanel (use border_title instead) - Fix border_title color to use $primary - Fix WeekGrid to always scroll to work day start (7am) on mount
305 lines
7.3 KiB
Python
305 lines
7.3 KiB
Python
"""Task backend abstraction for Tasks TUI.
|
|
|
|
This module defines the abstract interface that all task backends must implement,
|
|
allowing the TUI to work with different task management systems (dstask, taskwarrior, etc.)
|
|
"""
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
from enum import Enum
|
|
from typing import Optional
|
|
|
|
|
|
class TaskStatus(Enum):
|
|
"""Task status values."""
|
|
|
|
PENDING = "pending"
|
|
ACTIVE = "active"
|
|
DONE = "done"
|
|
DELETED = "deleted"
|
|
|
|
|
|
class TaskPriority(Enum):
|
|
"""Task priority levels (P0 = highest, P3 = lowest)."""
|
|
|
|
P0 = "P0" # Critical
|
|
P1 = "P1" # High
|
|
P2 = "P2" # Normal
|
|
P3 = "P3" # Low
|
|
|
|
@classmethod
|
|
def from_string(cls, value: str) -> "TaskPriority":
|
|
"""Parse priority from string."""
|
|
value = value.upper().strip()
|
|
if value in ("P0", "0", "CRITICAL"):
|
|
return cls.P0
|
|
elif value in ("P1", "1", "HIGH", "H"):
|
|
return cls.P1
|
|
elif value in ("P2", "2", "NORMAL", "MEDIUM", "M"):
|
|
return cls.P2
|
|
elif value in ("P3", "3", "LOW", "L"):
|
|
return cls.P3
|
|
return cls.P2 # Default to normal
|
|
|
|
|
|
@dataclass
|
|
class Task:
|
|
"""Unified task representation across backends."""
|
|
|
|
uuid: str
|
|
id: int # Short numeric ID for display
|
|
summary: str
|
|
status: TaskStatus = TaskStatus.PENDING
|
|
priority: TaskPriority = TaskPriority.P2
|
|
project: str = ""
|
|
tags: list[str] = field(default_factory=list)
|
|
notes: str = ""
|
|
due: Optional[datetime] = None
|
|
created: Optional[datetime] = None
|
|
resolved: Optional[datetime] = None
|
|
|
|
@property
|
|
def is_overdue(self) -> bool:
|
|
"""Check if task is overdue."""
|
|
if self.due is None or self.status == TaskStatus.DONE:
|
|
return False
|
|
# Use timezone-aware now() if due date is timezone-aware
|
|
now = datetime.now(timezone.utc) if self.due.tzinfo else datetime.now()
|
|
return now > self.due
|
|
|
|
|
|
@dataclass
|
|
class Project:
|
|
"""Project information."""
|
|
|
|
name: str
|
|
task_count: int = 0
|
|
resolved_count: int = 0
|
|
active: bool = True
|
|
priority: TaskPriority = TaskPriority.P2
|
|
|
|
|
|
class TaskBackend(ABC):
|
|
"""Abstract base class for task management backends."""
|
|
|
|
@abstractmethod
|
|
def get_tasks(
|
|
self,
|
|
project: Optional[str] = None,
|
|
tags: Optional[list[str]] = None,
|
|
status: Optional[TaskStatus] = None,
|
|
) -> list[Task]:
|
|
"""Get tasks, optionally filtered by project, tags, or status.
|
|
|
|
Args:
|
|
project: Filter by project name
|
|
tags: Filter by tags (tasks must have all specified tags)
|
|
status: Filter by status
|
|
|
|
Returns:
|
|
List of matching tasks
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_next_tasks(self) -> list[Task]:
|
|
"""Get the 'next' tasks to work on (priority-sorted actionable tasks)."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_task(self, task_id: str) -> Optional[Task]:
|
|
"""Get a single task by ID or UUID.
|
|
|
|
Args:
|
|
task_id: Task ID (numeric) or UUID
|
|
|
|
Returns:
|
|
Task if found, None otherwise
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def add_task(
|
|
self,
|
|
summary: str,
|
|
project: Optional[str] = None,
|
|
tags: Optional[list[str]] = None,
|
|
priority: Optional[TaskPriority] = None,
|
|
due: Optional[datetime] = None,
|
|
notes: Optional[str] = None,
|
|
) -> Task:
|
|
"""Create a new task.
|
|
|
|
Args:
|
|
summary: Task description
|
|
project: Project name
|
|
tags: List of tags
|
|
priority: Task priority
|
|
due: Due date
|
|
notes: Additional notes
|
|
|
|
Returns:
|
|
The created task
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def complete_task(self, task_id: str) -> bool:
|
|
"""Mark a task as complete.
|
|
|
|
Args:
|
|
task_id: Task ID or UUID
|
|
|
|
Returns:
|
|
True if successful
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def delete_task(self, task_id: str) -> bool:
|
|
"""Delete a task.
|
|
|
|
Args:
|
|
task_id: Task ID or UUID
|
|
|
|
Returns:
|
|
True if successful
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def start_task(self, task_id: str) -> bool:
|
|
"""Start working on a task (mark as active).
|
|
|
|
Args:
|
|
task_id: Task ID or UUID
|
|
|
|
Returns:
|
|
True if successful
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def stop_task(self, task_id: str) -> bool:
|
|
"""Stop working on a task (mark as pending).
|
|
|
|
Args:
|
|
task_id: Task ID or UUID
|
|
|
|
Returns:
|
|
True if successful
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def modify_task(
|
|
self,
|
|
task_id: str,
|
|
summary: Optional[str] = None,
|
|
project: Optional[str] = None,
|
|
tags: Optional[list[str]] = None,
|
|
priority: Optional[TaskPriority] = None,
|
|
due: Optional[datetime] = None,
|
|
notes: Optional[str] = None,
|
|
) -> bool:
|
|
"""Modify a task.
|
|
|
|
Args:
|
|
task_id: Task ID or UUID
|
|
summary: New summary (if provided)
|
|
project: New project (if provided)
|
|
tags: New tags (if provided)
|
|
priority: New priority (if provided)
|
|
due: New due date (if provided)
|
|
notes: New notes (if provided)
|
|
|
|
Returns:
|
|
True if successful
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_projects(self) -> list[Project]:
|
|
"""Get all projects.
|
|
|
|
Returns:
|
|
List of projects with task counts
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_tags(self) -> list[str]:
|
|
"""Get all tags.
|
|
|
|
Returns:
|
|
List of tag names
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def sync(self) -> bool:
|
|
"""Sync tasks with remote (if supported).
|
|
|
|
Returns:
|
|
True if successful (or not applicable)
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def edit_task_interactive(self, task_id: str) -> bool:
|
|
"""Open task in editor for interactive editing.
|
|
|
|
Args:
|
|
task_id: Task ID or UUID
|
|
|
|
Returns:
|
|
True if successful
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def edit_note_interactive(self, task_id: str) -> bool:
|
|
"""Open task notes in editor for interactive editing.
|
|
|
|
Args:
|
|
task_id: Task ID or UUID
|
|
|
|
Returns:
|
|
True if successful
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_context(self) -> Optional[str]:
|
|
"""Get the current context filter.
|
|
|
|
Returns:
|
|
Current context string, or None if no context is set
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def set_context(self, context: Optional[str]) -> bool:
|
|
"""Set the context filter.
|
|
|
|
Args:
|
|
context: Context string (e.g., "+work", "project:foo") or None to clear
|
|
|
|
Returns:
|
|
True if successful
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_contexts(self) -> list[str]:
|
|
"""Get available predefined contexts.
|
|
|
|
For taskwarrior, returns named contexts from config.
|
|
For dstask, may return common tag-based contexts.
|
|
|
|
Returns:
|
|
List of context names/filters
|
|
"""
|
|
pass
|