Files
gems/opal-task/cmd/add.go
T
joakim b02c40f716 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>
2026-02-19 13:44:56 +01:00

126 lines
3.3 KiB
Go

package cmd
import (
"fmt"
"os"
"strings"
"git.jnss.me/joakim/opal/internal/engine"
"github.com/spf13/cobra"
)
var addCmd = &cobra.Command{
Use: "add <description> [modifiers...]",
Short: "Add a new task",
Long: `Add a new task with optional modifiers.
Examples:
opal add buy groceries # No quotes needed!
opal add review PR priority:H project:backend
opal add buy groceries +shop carrots # Tag can be anywhere
opal add team meeting due:mon recur:1w +meetings`,
Run: func(cmd *cobra.Command, args []string) {
parsed := getParsedArgs(cmd)
// For add, combine filters and modifiers (all are args to parse)
allArgs := append(parsed.Filters, parsed.Modifiers...)
if err := addTask(allArgs); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
},
}
func addTask(args []string) error {
// Parse description and modifiers from args
// Description = all words that are NOT filters/modifiers
// Filters/Modifiers = words with +, -, or containing :
description, modifierArgs, err := parseAddArgs(args)
if err != nil {
return err
}
// Parse modifiers
var mod *engine.Modifier
if len(modifierArgs) > 0 {
mod, err = engine.ParseModifier(modifierArgs)
if err != nil {
return fmt.Errorf("failed to parse modifiers: %w", err)
}
}
// Check if this is a recurring task
isRecurring := mod != nil && mod.SetAttributes["recur"] != nil
if isRecurring {
// Create recurring task (template + first instance)
return addRecurringTask(description, mod)
}
// Create regular task
task, err := engine.CreateTaskWithModifier(description, mod)
if err != nil {
return fmt.Errorf("failed to create task: %w", err)
}
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
}
// parseAddArgs extracts description and modifiers from args.
// Tags (+tag, -tag) are always modifiers. For key:value tokens, only
// recognized attribute keys (engine.ValidAttributeKeys) are treated as
// modifiers — everything else becomes part of the description.
func parseAddArgs(args []string) (string, []string, error) {
var descParts []string
var modifiers []string
for _, arg := range args {
if strings.HasPrefix(arg, "+") || strings.HasPrefix(arg, "-") {
modifiers = append(modifiers, arg)
continue
}
if idx := strings.Index(arg, ":"); idx > 0 {
key := arg[:idx]
if engine.ValidAttributeKeys[key] {
modifiers = append(modifiers, arg)
continue
}
}
descParts = append(descParts, arg)
}
if len(descParts) == 0 {
return "", nil, fmt.Errorf("description is required")
}
description := strings.Join(descParts, " ")
return description, modifiers, nil
}
func addRecurringTask(description string, mod *engine.Modifier) error {
instance, err := engine.CreateRecurringTask(description, mod)
if err != nil {
return err
}
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
}