7aaaa86a0a
Add annotations as JSON column on tasks table with Annotate/Denotate methods and CLI commands. Add undo system backed by change_log with lightweight undo_stack table (capped at 10 entries). All mutating CLI commands (add, done, delete, modify, start, stop) now record undo entries. Undo restores prior task state from change_log data. Schema changes (in v1 migration): - annotations TEXT column on tasks - undo_stack table - annotations field in change_log triggers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
130 lines
3.3 KiB
Go
130 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)
|
|
}
|
|
|
|
engine.RecordUndo("add", task.UUID)
|
|
|
|
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
|
|
}
|
|
|
|
engine.RecordUndo("add", 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
|
|
}
|