Files
gems/opal-task/cmd/modify.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

118 lines
2.8 KiB
Go

package cmd
import (
"fmt"
"os"
"git.jnss.me/joakim/opal/internal/engine"
"github.com/spf13/cobra"
)
var modifyCmd = &cobra.Command{
Use: "modify [modifier...]",
Short: "Modify task attributes",
Long: `Modify task attributes. Filters must come before 'modify', modifiers after.
Examples:
opal 2 modify priority:H
opal +urgent modify due:tomorrow
opal project:backend modify priority:L
opal modify priority:H # Modify ALL pending tasks (with confirmation)`,
Run: func(cmd *cobra.Command, args []string) {
parsed := getParsedArgs(cmd)
if err := modifyTasks(parsed.Filters, parsed.Modifiers); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
},
}
func modifyTasks(filterArgs, modifierArgs []string) error {
// Validate we have modifiers
if len(modifierArgs) == 0 {
return fmt.Errorf("no modifiers specified (modifiers must come after 'modify')")
}
// Parse filter (or use default if no filters specified)
var filter *engine.Filter
var err error
if len(filterArgs) == 0 {
// No filters = modify all pending tasks (with confirmation)
filter = engine.DefaultFilter()
} else {
filter, err = engine.ParseFilter(filterArgs)
if err != nil {
return fmt.Errorf("failed to parse filter: %w", err)
}
}
// Load working set for ID resolution
ws, err := engine.LoadWorkingSet()
if err != nil {
// If no working set exists yet, build one from filtered tasks
tasks, err := engine.GetTasks(filter)
if err != nil {
return fmt.Errorf("failed to get tasks: %w", err)
}
ws, err = engine.BuildWorkingSet(tasks)
if err != nil {
return fmt.Errorf("failed to build working set: %w", err)
}
}
// Resolve tasks
var tasks []*engine.Task
if len(filter.IDs) > 0 {
// Resolve display IDs
for _, id := range filter.IDs {
task, err := ws.GetTaskByDisplayID(id)
if err != nil {
return err
}
tasks = append(tasks, task)
}
} else {
// Use filter to get tasks
tasks, err = engine.GetTasks(filter)
if err != nil {
return err
}
}
if len(tasks) == 0 {
return fmt.Errorf("no tasks matched filter")
}
// Confirm if multiple tasks or no filters specified
if len(tasks) > 1 || len(filterArgs) == 0 {
fmt.Print(engine.FormatTaskConfirmList("modify", tasks, ws))
fmt.Printf("Proceed? (y/N): ")
var confirm string
fmt.Scanln(&confirm)
if confirm != "y" && confirm != "Y" {
fmt.Println("Cancelled.")
return nil
}
}
// Parse and apply modifiers
mod, err := engine.ParseModifier(modifierArgs)
if err != nil {
return fmt.Errorf("failed to parse modifiers: %w", err)
}
modified := 0
for _, task := range tasks {
if err := mod.Apply(task); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to modify task %s: %v\n", task.UUID, err)
} else {
modified++
}
}
fmt.Printf("Modified %d task(s).\n", modified)
return nil
}