feat: improve CLI output with relative dates, rich feedback, and recurring task info
Add relative date formatting (today, tomorrow, in 3d, etc.) for list and detail views. Add structured feedback helpers for add/complete/delete operations showing display IDs and parsed modifiers. Change Complete() to return spawned recurring instance so callers can display recurrence info. Add AppendTask to working set for immediate display ID assignment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// FormatTaskSummary returns a one-line summary for action feedback.
|
||||
// Example: `3 "Buy groceries" due:tomorrow +errand`
|
||||
func FormatTaskSummary(task *Task, ws *WorkingSet) string {
|
||||
displayID := resolveDisplayID(task, ws)
|
||||
|
||||
parts := []string{fmt.Sprintf("%d — %q", displayID, task.Description)}
|
||||
|
||||
if task.Due != nil {
|
||||
parts = append(parts, fmt.Sprintf("due:%s", FormatRelativeDate(*task.Due)))
|
||||
}
|
||||
if task.Project != nil {
|
||||
parts = append(parts, fmt.Sprintf("project:%s", *task.Project))
|
||||
}
|
||||
if len(task.Tags) > 0 {
|
||||
for _, tag := range task.Tags {
|
||||
parts = append(parts, color.CyanString("+"+tag))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// FormatTaskConfirmList returns the multi-task confirmation block.
|
||||
// Shows up to 10 tasks, then "...and N more".
|
||||
func FormatTaskConfirmList(action string, tasks []*Task, ws *WorkingSet) string {
|
||||
var b strings.Builder
|
||||
|
||||
limit := 10
|
||||
if len(tasks) < limit {
|
||||
limit = len(tasks)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&b, "About to %s %d task(s):\n", action, len(tasks))
|
||||
for i := 0; i < limit; i++ {
|
||||
task := tasks[i]
|
||||
displayID := resolveDisplayID(task, ws)
|
||||
line := fmt.Sprintf(" %3d %-40s", displayID, truncate(task.Description, 40))
|
||||
|
||||
if task.Due != nil {
|
||||
line += fmt.Sprintf(" due:%-10s", FormatRelativeDate(*task.Due))
|
||||
}
|
||||
if len(task.Tags) > 0 {
|
||||
tags := make([]string, len(task.Tags))
|
||||
for j, tag := range task.Tags {
|
||||
tags[j] = "+" + tag
|
||||
}
|
||||
line += " " + strings.Join(tags, " ")
|
||||
}
|
||||
fmt.Fprintln(&b, line)
|
||||
}
|
||||
|
||||
if len(tasks) > 10 {
|
||||
fmt.Fprintf(&b, " ...and %d more\n", len(tasks)-10)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// FormatAddFeedback returns the detailed post-add feedback block.
|
||||
func FormatAddFeedback(task *Task, displayID int) string {
|
||||
var b strings.Builder
|
||||
|
||||
fmt.Fprintf(&b, "Created task %d — %q\n", displayID, task.Description)
|
||||
|
||||
if task.Due != nil {
|
||||
fmt.Fprintf(&b, " Due: %s\n", FormatDateWithRelative(*task.Due))
|
||||
}
|
||||
if task.Project != nil {
|
||||
fmt.Fprintf(&b, " Project: %s\n", *task.Project)
|
||||
}
|
||||
if task.Priority != PriorityDefault {
|
||||
fmt.Fprintf(&b, " Priority: %s\n", priorityIntToString(task.Priority))
|
||||
}
|
||||
if task.Scheduled != nil {
|
||||
fmt.Fprintf(&b, " Scheduled: %s\n", FormatDateWithRelative(*task.Scheduled))
|
||||
}
|
||||
if task.Wait != nil {
|
||||
fmt.Fprintf(&b, " Wait: %s\n", FormatDateWithRelative(*task.Wait))
|
||||
}
|
||||
if len(task.Tags) > 0 {
|
||||
tags := make([]string, len(task.Tags))
|
||||
for i, tag := range task.Tags {
|
||||
tags[i] = "+" + tag
|
||||
}
|
||||
fmt.Fprintf(&b, " Tags: %s\n", strings.Join(tags, " "))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// FormatRecurringAddFeedback returns feedback for a newly created recurring task.
|
||||
func FormatRecurringAddFeedback(instance *Task, displayID int) string {
|
||||
var b strings.Builder
|
||||
|
||||
fmt.Fprintf(&b, "Created recurring task %d — %q\n", displayID, instance.Description)
|
||||
|
||||
if instance.RecurrenceDuration != nil {
|
||||
fmt.Fprintf(&b, " Recurrence: %s\n", FormatRecurrenceDuration(*instance.RecurrenceDuration))
|
||||
} else if instance.ParentUUID != nil {
|
||||
// Instance: get recurrence from parent
|
||||
parent, err := GetTask(*instance.ParentUUID)
|
||||
if err == nil && parent.RecurrenceDuration != nil {
|
||||
fmt.Fprintf(&b, " Recurrence: %s\n", FormatRecurrenceDuration(*parent.RecurrenceDuration))
|
||||
}
|
||||
}
|
||||
if instance.Due != nil {
|
||||
fmt.Fprintf(&b, " Due: %s\n", FormatDateWithRelative(*instance.Due))
|
||||
}
|
||||
if len(instance.Tags) > 0 {
|
||||
tags := make([]string, len(instance.Tags))
|
||||
for i, tag := range instance.Tags {
|
||||
tags[i] = "+" + tag
|
||||
}
|
||||
fmt.Fprintf(&b, " Tags: %s\n", strings.Join(tags, " "))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// FormatCompletionFeedback returns completion feedback with recurrence info.
|
||||
func FormatCompletionFeedback(task *Task, displayID int, nextInstance *Task) string {
|
||||
var b strings.Builder
|
||||
|
||||
fmt.Fprintf(&b, "Completed task %d — %q\n", displayID, task.Description)
|
||||
|
||||
if nextInstance != nil {
|
||||
if nextInstance.Due != nil {
|
||||
fmt.Fprintf(&b, "Next instance created — due: %s\n", FormatDateWithRelative(*nextInstance.Due))
|
||||
} else {
|
||||
fmt.Fprintf(&b, "Next instance created\n")
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func resolveDisplayID(task *Task, ws *WorkingSet) int {
|
||||
if ws == nil {
|
||||
return 0
|
||||
}
|
||||
for id, uuid := range ws.byID {
|
||||
if uuid == task.UUID {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func truncate(s string, max int) string {
|
||||
if len(s) <= max {
|
||||
return s
|
||||
}
|
||||
return s[:max-1] + "…"
|
||||
}
|
||||
Reference in New Issue
Block a user