a68d701d14
Issue 1: Fix recurrence calculation for overdue tasks - Use completion date (End) as base for next instance, not original due date - If task due Monday completed Wednesday, next is Wednesday+7d not Monday+7d - Fallback to Due date if End is not set - Update test to verify new behavior Issue 2: Fix description parsing to work without quotes - Add parseAddArgs() to extract description from non-modifier words - Description = all words that don't start with +, -, or contain : - Enables: opal add buy groceries +shop carrots → 'buy groceries carrots' - Validate description is required (error if only modifiers) - Validate recurring tasks require due date Issue 3: Implement flexible command syntax - Add preprocessArgs() to parse arguments before Cobra routing - Detect command position and split filters (left) from modifiers (right) - Rewrite os.Args so Cobra routes correctly - Enable both 'opal 2 done' and 'opal done 2' syntax - Commands without modifiers accept filters on either side - Commands with modifiers enforce [filters] command [modifiers] - Add confirmation for modify without filters (modifies all tasks) All commands updated to use preprocessed ParsedArgs from context. All tests passing (33 tests).
173 lines
4.5 KiB
Go
173 lines
4.5 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)
|
|
}
|
|
|
|
fmt.Printf("Created task %s\n", task.UUID)
|
|
if len(task.Tags) > 0 {
|
|
fmt.Printf("Tags: %s\n", strings.Join(task.Tags, ", "))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseAddArgs extracts description and modifiers from args
|
|
// Description = all non-filter/modifier words joined with spaces
|
|
// Filters/Modifiers = args with +, -, or containing :
|
|
func parseAddArgs(args []string) (string, []string, error) {
|
|
var descParts []string
|
|
var modifiers []string
|
|
|
|
for _, arg := range args {
|
|
isFilterOrModifier := strings.HasPrefix(arg, "+") ||
|
|
strings.HasPrefix(arg, "-") ||
|
|
strings.Contains(arg, ":")
|
|
|
|
if isFilterOrModifier {
|
|
modifiers = append(modifiers, arg)
|
|
} else {
|
|
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 {
|
|
// Extract recurrence pattern
|
|
recurPattern := mod.SetAttributes["recur"]
|
|
if recurPattern == nil {
|
|
return fmt.Errorf("no recurrence pattern specified")
|
|
}
|
|
|
|
// Validate: recurring tasks must have due date
|
|
if mod.SetAttributes["due"] == nil {
|
|
return fmt.Errorf("recurring tasks require a due date (use due:YYYY-MM-DD or due:monday)")
|
|
}
|
|
|
|
duration, err := engine.ParseRecurrencePattern(*recurPattern)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid recurrence pattern: %w", err)
|
|
}
|
|
|
|
// Create template task
|
|
template, err := engine.CreateTask(description)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create template: %w", err)
|
|
}
|
|
|
|
template.Status = engine.StatusRecurring
|
|
template.RecurrenceDuration = &duration
|
|
|
|
// Apply other modifiers to template (except recur)
|
|
if mod != nil {
|
|
tempMod := &engine.Modifier{
|
|
SetAttributes: make(map[string]*string),
|
|
AddTags: mod.AddTags,
|
|
RemoveTags: mod.RemoveTags,
|
|
}
|
|
|
|
// Copy all attributes except recur
|
|
for key, val := range mod.SetAttributes {
|
|
if key != "recur" {
|
|
tempMod.SetAttributes[key] = val
|
|
}
|
|
}
|
|
|
|
if err := tempMod.Apply(template); err != nil {
|
|
return fmt.Errorf("failed to apply modifiers to template: %w", err)
|
|
}
|
|
}
|
|
|
|
// Create first instance
|
|
instance, err := engine.CreateTask(description)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create first instance: %w", err)
|
|
}
|
|
|
|
instance.ParentUUID = &template.UUID
|
|
instance.Due = template.Due
|
|
instance.Project = template.Project
|
|
instance.Priority = template.Priority
|
|
|
|
if err := instance.Save(); err != nil {
|
|
return fmt.Errorf("failed to save first instance: %w", err)
|
|
}
|
|
|
|
// Copy tags to instance
|
|
for _, tag := range template.Tags {
|
|
instance.AddTag(tag)
|
|
}
|
|
|
|
fmt.Printf("Created recurring task %s\n", template.UUID)
|
|
fmt.Printf("First instance: %s\n", instance.UUID)
|
|
fmt.Printf("Recurrence: %s\n", engine.FormatRecurrenceDuration(duration))
|
|
|
|
return nil
|
|
}
|