b02c40f716
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>
126 lines
3.3 KiB
Go
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
|
|
}
|