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:
2026-02-19 13:44:56 +01:00
parent 779da6ddfd
commit b02c40f716
14 changed files with 323 additions and 48 deletions
+13 -5
View File
@@ -63,11 +63,14 @@ func addTask(args []string) error {
return fmt.Errorf("failed to create task: %w", err)
}
fmt.Printf("Created task %s\n", task.UUID)
if len(task.Tags) > 0 {
fmt.Printf("Tags: %s\n", strings.Join(task.Tags, ", "))
displayID, err := engine.AppendTask(task)
if err != nil {
// Non-fatal: task was created, just can't assign display ID
fmt.Printf("Created task %s\n", task.UUID)
return nil
}
fmt.Print(engine.FormatAddFeedback(task, displayID))
return nil
}
@@ -110,8 +113,13 @@ func addRecurringTask(description string, mod *engine.Modifier) error {
return err
}
fmt.Printf("Created recurring task %s\n", *instance.ParentUUID)
fmt.Printf("First instance: %s\n", instance.UUID)
displayID, err := engine.AppendTask(instance)
if err != nil {
fmt.Printf("Created recurring task %s\n", *instance.ParentUUID)
fmt.Printf("First instance: %s\n", instance.UUID)
return nil
}
fmt.Print(engine.FormatRecurringAddFeedback(instance, displayID))
return nil
}
+14 -6
View File
@@ -53,17 +53,25 @@ func deleteTasks(args []string) error {
return fmt.Errorf("no tasks matched filter")
}
fmt.Printf("Delete %d task(s)? (y/N): ", len(tasks))
var confirm string
fmt.Scanln(&confirm)
if confirm != "y" && confirm != "Y" {
return nil
if len(tasks) > 1 {
fmt.Print(engine.FormatTaskConfirmList("delete", tasks, ws))
fmt.Printf("Proceed? (y/N): ")
var confirm string
fmt.Scanln(&confirm)
if confirm != "y" && confirm != "Y" {
fmt.Println("Cancelled.")
return nil
}
}
for _, task := range tasks {
task.Delete(false) // Soft delete
}
fmt.Printf("Deleted %d task(s).\n", len(tasks))
if len(tasks) == 1 {
fmt.Printf("Deleted task %s\n", engine.FormatTaskSummary(tasks[0], ws))
} else {
fmt.Printf("Deleted %d task(s).\n", len(tasks))
}
return nil
}
+8 -4
View File
@@ -66,9 +66,9 @@ func completeTasks(args []string) error {
return fmt.Errorf("no tasks matched filter")
}
// Confirm if multiple tasks
if len(tasks) > 1 {
fmt.Printf("About to complete %d tasks. Proceed? (y/N): ", len(tasks))
fmt.Print(engine.FormatTaskConfirmList("complete", tasks, ws))
fmt.Printf("Proceed? (y/N): ")
var confirm string
fmt.Scanln(&confirm)
if confirm != "y" && confirm != "Y" {
@@ -80,14 +80,18 @@ func completeTasks(args []string) error {
// Complete tasks
completed := 0
for _, task := range tasks {
if err := task.Complete(); err != nil {
if _, err := task.Complete(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to complete task %s: %v\n", task.UUID, err)
} else {
completed++
}
}
fmt.Printf("Completed %d task(s).\n", completed)
if len(tasks) == 1 {
fmt.Printf("Completed task %s\n", engine.FormatTaskSummary(tasks[0], ws))
} else {
fmt.Printf("Completed %d task(s).\n", completed)
}
return nil
}
+2 -1
View File
@@ -240,7 +240,8 @@ func applyEditedFields(task *engine.Task, fields map[string]string) error {
return err
}
// Then complete (which saves automatically)
return task.Complete()
_, err := task.Complete()
return err
}
// If changing to deleted, use Delete() method
+2 -1
View File
@@ -87,7 +87,8 @@ func modifyTasks(filterArgs, modifierArgs []string) error {
// Confirm if multiple tasks or no filters specified
if len(tasks) > 1 || len(filterArgs) == 0 {
fmt.Printf("About to modify %d task(s). Proceed? (y/N): ", len(tasks))
fmt.Print(engine.FormatTaskConfirmList("modify", tasks, ws))
fmt.Printf("Proceed? (y/N): ")
var confirm string
fmt.Scanln(&confirm)
if confirm != "y" && confirm != "Y" {