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).
95 lines
2.1 KiB
Go
95 lines
2.1 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"git.jnss.me/joakim/opal/internal/engine"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var doneCmd = &cobra.Command{
|
|
Use: "done [filter...]",
|
|
Short: "Mark tasks as completed",
|
|
Long: `Mark one or more tasks as completed.
|
|
|
|
Examples:
|
|
opal done 1 # Complete task with display ID 1
|
|
opal 1 done # Flexible syntax (same as above)
|
|
opal done +urgent # Complete all urgent tasks
|
|
opal +urgent done # Flexible syntax (same as above)
|
|
opal done project:backend`,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
parsed := getParsedArgs(cmd)
|
|
if err := completeTasks(parsed.Filters); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
},
|
|
}
|
|
|
|
func completeTasks(args []string) error {
|
|
// Parse filter
|
|
filter, err := engine.ParseFilter(args)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse filter: %w", err)
|
|
}
|
|
|
|
// Load working set to resolve IDs
|
|
ws, err := engine.LoadWorkingSet()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load working set: %w", err)
|
|
}
|
|
|
|
// Get matching 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
|
|
matched, err := engine.GetTasks(filter)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get tasks: %w", err)
|
|
}
|
|
tasks = matched
|
|
}
|
|
|
|
if len(tasks) == 0 {
|
|
fmt.Println("No tasks matched.")
|
|
return nil
|
|
}
|
|
|
|
// Confirm if multiple tasks
|
|
if len(tasks) > 1 {
|
|
fmt.Printf("About to complete %d tasks. Proceed? (y/N): ", len(tasks))
|
|
var confirm string
|
|
fmt.Scanln(&confirm)
|
|
if confirm != "y" && confirm != "Y" {
|
|
fmt.Println("Cancelled.")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Complete tasks
|
|
completed := 0
|
|
for _, task := range tasks {
|
|
if err := task.Complete(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to complete task %s: %v\n", task.UUID, err)
|
|
} else {
|
|
completed++
|
|
}
|
|
}
|
|
|
|
fmt.Printf("Completed %d task(s).\n", completed)
|
|
|
|
return nil
|
|
}
|