diff --git a/opal-task/cmd/root.go b/opal-task/cmd/root.go index 1818122..00bf4cd 100644 --- a/opal-task/cmd/root.go +++ b/opal-task/cmd/root.go @@ -34,7 +34,7 @@ var commandNames = []string{ "add", "done", "modify", "delete", "start", "stop", "count", "projects", "tags", "info", "edit", "server", "sync", "reports", "setup", - "version", "annotate", "denotate", "undo", "log", "completion", + "version", "annotate", "denotate", "undo", "uncomplete", "log", "completion", } // Report names (dynamically populated) @@ -241,6 +241,7 @@ func init() { annotateCmd.GroupID = "task" denotateCmd.GroupID = "task" undoCmd.GroupID = "task" + uncompleteCmd.GroupID = "task" logCmd.GroupID = "task" rootCmd.AddCommand(addCmd) @@ -254,6 +255,7 @@ func init() { rootCmd.AddCommand(annotateCmd) rootCmd.AddCommand(denotateCmd) rootCmd.AddCommand(undoCmd) + rootCmd.AddCommand(uncompleteCmd) rootCmd.AddCommand(logCmd) // Other commands diff --git a/opal-task/cmd/uncomplete.go b/opal-task/cmd/uncomplete.go new file mode 100644 index 0000000..4fc1ae6 --- /dev/null +++ b/opal-task/cmd/uncomplete.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "fmt" + "os" + + "git.jnss.me/joakim/opal/internal/engine" + "github.com/spf13/cobra" +) + +var uncompleteCmd = &cobra.Command{ + Use: "uncomplete [filter...]", + Short: "Restore a completed task to pending", + Long: `Restore a completed task back to pending status. + +Unlike undo, this is a targeted action that works on any completed task +regardless of when it was completed. + +Examples: + opal 2 uncomplete + opal uncomplete +errand`, + Run: func(cmd *cobra.Command, args []string) { + parsed := getParsedArgs(cmd) + if err := uncompleteTasks(parsed.Filters); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + }, +} + +func uncompleteTasks(args []string) error { + if len(args) == 0 { + return fmt.Errorf("no task specified") + } + + filter, err := engine.ParseFilter(args) + if err != nil { + return fmt.Errorf("failed to parse filter: %w", err) + } + + ws, err := engine.LoadWorkingSet() + if err != nil { + return fmt.Errorf("failed to load working set: %w", err) + } + + var tasks []*engine.Task + + if len(filter.IDs) > 0 { + for _, id := range filter.IDs { + task, err := ws.GetTaskByDisplayID(id) + if err != nil { + return err + } + tasks = append(tasks, task) + } + } else { + tasks, err = engine.GetTasks(filter) + if err != nil { + return fmt.Errorf("failed to get tasks: %w", err) + } + } + + if len(tasks) == 0 { + return fmt.Errorf("no tasks matched filter") + } + + uncompleted := 0 + for _, task := range tasks { + if task.Status != engine.StatusCompleted { + fmt.Fprintf(os.Stderr, "Warning: task %s is not completed, skipping\n", task.UUID) + continue + } + task.Status = engine.StatusPending + task.End = nil + if err := task.Save(); err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to uncomplete task %s: %v\n", task.UUID, err) + } else { + uncompleted++ + } + } + + if uncompleted == 1 { + fmt.Printf("Restored task %s to pending\n", engine.FormatTaskSummary(tasks[0], ws)) + } else { + fmt.Printf("Restored %d task(s) to pending.\n", uncompleted) + } + return nil +}