Implement opal-task Phases 6-8: Complete CLI Implementation
Phase 6: Display and Basic Commands - Add display.go with colored formatting for tasks, projects, tags - Implement cmd/root.go with Cobra command structure - Implement cmd/list.go for listing and filtering tasks - Implement cmd/add.go with support for regular and recurring tasks - Implement cmd/done.go with bulk completion and confirmation Phase 7: Advanced Commands - Implement cmd/modify.go for updating task attributes - Implement cmd/delete.go with soft delete confirmation - Implement cmd/start.go and cmd/stop.go for task timing - Implement cmd/count.go for counting filtered tasks - Implement cmd/projects.go and cmd/tags.go for aggregation Phase 8: Integration and Polish - Update main.go to use CLI commands - Add colored output with fatih/color - Format task lists with proper alignment - Highlight overdue tasks in red, upcoming in yellow - Test end-to-end workflow: add, list, done, recurring tasks - Verify recurrence spawning works correctly All CLI commands functional and tested!
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// FormatTaskList formats a list of tasks for display
|
||||
func FormatTaskList(tasks []*Task, ws *WorkingSet) string {
|
||||
if len(tasks) == 0 {
|
||||
return "No tasks found."
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
// Header
|
||||
sb.WriteString(fmt.Sprintf("%-3s %-8s %-3s %-12s %-40s %-12s %s\n",
|
||||
"ID", "Status", "Pri", "Project", "Description", "Due", "Tags"))
|
||||
sb.WriteString(strings.Repeat("-", 100) + "\n")
|
||||
|
||||
// Tasks
|
||||
for i, task := range tasks {
|
||||
displayID := i + 1
|
||||
if ws != nil {
|
||||
// Use working set display ID if available
|
||||
for id, uuid := range ws.byID {
|
||||
if uuid == task.UUID {
|
||||
displayID = id
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
status := formatStatus(task.Status)
|
||||
priority := formatPriority(task.Priority)
|
||||
project := formatProject(task.Project)
|
||||
description := truncate(task.Description, 40)
|
||||
due := formatDue(task.Due)
|
||||
tags := formatTags(task.Tags)
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%-3d %-8s %-3s %-12s %-40s %-12s %s\n",
|
||||
displayID, status, priority, project, description, due, tags))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// FormatTaskDetail formats detailed task information
|
||||
func FormatTaskDetail(task *Task) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(color.New(color.Bold).Sprint("Task Details") + "\n")
|
||||
sb.WriteString(strings.Repeat("=", 50) + "\n")
|
||||
|
||||
sb.WriteString(fmt.Sprintf("UUID: %s\n", task.UUID))
|
||||
sb.WriteString(fmt.Sprintf("Status: %s\n", formatStatus(task.Status)))
|
||||
sb.WriteString(fmt.Sprintf("Description: %s\n", task.Description))
|
||||
sb.WriteString(fmt.Sprintf("Priority: %s\n", formatPriority(task.Priority)))
|
||||
sb.WriteString(fmt.Sprintf("Project: %s\n", formatProject(task.Project)))
|
||||
|
||||
sb.WriteString(fmt.Sprintf("\nCreated: %s\n", formatTime(task.Created)))
|
||||
sb.WriteString(fmt.Sprintf("Modified: %s\n", formatTime(task.Modified)))
|
||||
|
||||
if task.Start != nil {
|
||||
sb.WriteString(fmt.Sprintf("Started: %s\n", formatTime(*task.Start)))
|
||||
}
|
||||
|
||||
if task.End != nil {
|
||||
sb.WriteString(fmt.Sprintf("Ended: %s\n", formatTime(*task.End)))
|
||||
}
|
||||
|
||||
if task.Due != nil {
|
||||
sb.WriteString(fmt.Sprintf("Due: %s\n", formatTimeWithColor(*task.Due)))
|
||||
}
|
||||
|
||||
if task.Scheduled != nil {
|
||||
sb.WriteString(fmt.Sprintf("Scheduled: %s\n", formatTime(*task.Scheduled)))
|
||||
}
|
||||
|
||||
if task.Wait != nil {
|
||||
sb.WriteString(fmt.Sprintf("Wait: %s\n", formatTime(*task.Wait)))
|
||||
}
|
||||
|
||||
if task.Until != nil {
|
||||
sb.WriteString(fmt.Sprintf("Until: %s\n", formatTime(*task.Until)))
|
||||
}
|
||||
|
||||
if task.RecurrenceDuration != nil {
|
||||
sb.WriteString(fmt.Sprintf("Recurrence: %s\n", FormatRecurrenceDuration(*task.RecurrenceDuration)))
|
||||
}
|
||||
|
||||
if task.ParentUUID != nil {
|
||||
sb.WriteString(fmt.Sprintf("Parent: %s (recurring instance)\n", *task.ParentUUID))
|
||||
}
|
||||
|
||||
if len(task.Tags) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("\nTags: %s\n", formatTags(task.Tags)))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// FormatProjects formats project list with counts
|
||||
func FormatProjects(projectCounts map[string]int) string {
|
||||
if len(projectCounts) == 0 {
|
||||
return "No projects found."
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("%-20s %s\n", "Project", "Count"))
|
||||
sb.WriteString(strings.Repeat("-", 30) + "\n")
|
||||
|
||||
for project, count := range projectCounts {
|
||||
sb.WriteString(fmt.Sprintf("%-20s %d\n", project, count))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// FormatTags formats tag list with counts
|
||||
func FormatTagCounts(tagCounts map[string]int) string {
|
||||
if len(tagCounts) == 0 {
|
||||
return "No tags found."
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("%-20s %s\n", "Tag", "Count"))
|
||||
sb.WriteString(strings.Repeat("-", 30) + "\n")
|
||||
|
||||
for tag, count := range tagCounts {
|
||||
sb.WriteString(fmt.Sprintf("%-20s %d\n", tag, count))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func formatStatus(status Status) string {
|
||||
switch status {
|
||||
case StatusPending:
|
||||
return color.YellowString("pending")
|
||||
case StatusCompleted:
|
||||
return color.GreenString("done")
|
||||
case StatusDeleted:
|
||||
return color.RedString("deleted")
|
||||
case StatusRecurring:
|
||||
return color.BlueString("template")
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func formatPriority(priority Priority) string {
|
||||
switch priority {
|
||||
case PriorityHigh:
|
||||
return color.RedString("H")
|
||||
case PriorityMedium:
|
||||
return color.YellowString("M")
|
||||
case PriorityLow:
|
||||
return color.CyanString("L")
|
||||
case PriorityDefault:
|
||||
return "D"
|
||||
default:
|
||||
return "D"
|
||||
}
|
||||
}
|
||||
|
||||
func formatProject(project *string) string {
|
||||
if project == nil {
|
||||
return "-"
|
||||
}
|
||||
return *project
|
||||
}
|
||||
|
||||
func formatDue(due *time.Time) string {
|
||||
if due == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if due.Before(now) {
|
||||
return color.RedString(due.Format("2006-01-02"))
|
||||
}
|
||||
|
||||
if due.Before(now.Add(24 * time.Hour)) {
|
||||
return color.YellowString(due.Format("2006-01-02"))
|
||||
}
|
||||
|
||||
return due.Format("2006-01-02")
|
||||
}
|
||||
|
||||
func formatTimeWithColor(t time.Time) string {
|
||||
now := time.Now()
|
||||
if t.Before(now) {
|
||||
return color.RedString(t.Format("2006-01-02 15:04"))
|
||||
}
|
||||
return t.Format("2006-01-02 15:04")
|
||||
}
|
||||
|
||||
func formatTime(t time.Time) string {
|
||||
return t.Format("2006-01-02 15:04")
|
||||
}
|
||||
|
||||
func formatTags(tags []string) string {
|
||||
if len(tags) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
formatted := make([]string, len(tags))
|
||||
for i, tag := range tags {
|
||||
formatted[i] = color.CyanString("+" + tag)
|
||||
}
|
||||
return strings.Join(formatted, " ")
|
||||
}
|
||||
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen-3] + "..."
|
||||
}
|
||||
|
||||
// GetProjectCounts returns a map of project names to task counts
|
||||
func GetProjectCounts() (map[string]int, error) {
|
||||
tasks, err := GetTasks(DefaultFilter())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
counts := make(map[string]int)
|
||||
for _, task := range tasks {
|
||||
if task.Project != nil {
|
||||
counts[*task.Project]++
|
||||
}
|
||||
}
|
||||
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
// GetTagCounts returns a map of tags to task counts
|
||||
func GetTagCounts() (map[string]int, error) {
|
||||
tasks, err := GetTasks(DefaultFilter())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
counts := make(map[string]int)
|
||||
for _, task := range tasks {
|
||||
for _, tag := range task.Tags {
|
||||
counts[tag]++
|
||||
}
|
||||
}
|
||||
|
||||
return counts, nil
|
||||
}
|
||||
Reference in New Issue
Block a user