diff --git a/cmd/task/add.go b/cmd/task/add.go index f7cbe277a7a7bade8234a7bc71fada66e562f656..e3981d0136c998b2064df447c81aaf1439da87a9 100644 --- a/cmd/task/add.go +++ b/cmd/task/add.go @@ -5,12 +5,26 @@ package task import ( + "bufio" + "errors" "fmt" + "os" + "strings" + "git.secluded.site/go-lunatask" + "git.secluded.site/lune/internal/client" "git.secluded.site/lune/internal/completion" + "git.secluded.site/lune/internal/config" + "git.secluded.site/lune/internal/ui" "github.com/spf13/cobra" ) +// ErrUnknownGoal indicates the specified goal key was not found in config. +var ErrUnknownGoal = errors.New("unknown goal key") + +// ErrNoInput indicates no input was provided on stdin. +var ErrNoInput = errors.New("no input provided on stdin") + // AddCmd creates a new task. Exported for use by the add shortcut. var AddCmd = &cobra.Command{ Use: "add NAME", @@ -20,13 +34,7 @@ var AddCmd = &cobra.Command{ The task name is required. Use flags to set additional properties. Use "-" as NAME to read the task name from stdin.`, Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - // TODO: implement task creation - name := args[0] - fmt.Fprintf(cmd.OutOrStdout(), "Creating task: %s (not yet implemented)\n", name) - - return nil - }, + RunE: runAdd, } func init() { @@ -37,8 +45,11 @@ func init() { AddCmd.Flags().IntP("priority", "p", 0, "Priority: -2 to 2") AddCmd.Flags().IntP("estimate", "e", 0, "Estimate in minutes (0-720)") AddCmd.Flags().StringP("motivation", "m", "", "Motivation: must, should, want") - AddCmd.Flags().Int("eisenhower", 0, "Eisenhower quadrant: 1-4") - AddCmd.Flags().String("schedule", "", "Schedule date (natural language)") + AddCmd.Flags().Bool("important", false, "Mark as important (Eisenhower matrix)") + AddCmd.Flags().Bool("not-important", false, "Mark as not important") + AddCmd.Flags().Bool("urgent", false, "Mark as urgent (Eisenhower matrix)") + AddCmd.Flags().Bool("not-urgent", false, "Mark as not urgent") + AddCmd.Flags().String("schedule", "", "Schedule date (YYYY-MM-DD)") _ = AddCmd.RegisterFlagCompletionFunc("area", completion.Areas) _ = AddCmd.RegisterFlagCompletionFunc("goal", completion.Goals) @@ -46,6 +57,178 @@ func init() { completion.Static("later", "next", "started", "waiting")) _ = AddCmd.RegisterFlagCompletionFunc("motivation", completion.Static("must", "should", "want")) - _ = AddCmd.RegisterFlagCompletionFunc("eisenhower", - completion.Static("1", "2", "3", "4")) +} + +func runAdd(cmd *cobra.Command, args []string) error { + name, err := resolveName(args[0]) + if err != nil { + return err + } + + apiClient, err := client.New() + if err != nil { + return err + } + + builder := apiClient.NewTask(name) + + if err := applyAreaAndGoal(cmd, builder); err != nil { + return err + } + + if err := applyOptionalFlags(cmd, builder); err != nil { + return err + } + + task, err := builder.Create(cmd.Context()) + if err != nil { + fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Failed to create task")) + + return err + } + + fmt.Fprintln(cmd.OutOrStdout(), ui.Success.Render("Created task: "+task.ID)) + + return nil +} + +func resolveName(arg string) (string, error) { + if arg != "-" { + return arg, nil + } + + scanner := bufio.NewScanner(os.Stdin) + if scanner.Scan() { + return strings.TrimSpace(scanner.Text()), nil + } + + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("reading stdin: %w", err) + } + + return "", ErrNoInput +} + +func applyAreaAndGoal(cmd *cobra.Command, builder *lunatask.TaskBuilder) error { + areaKey, _ := cmd.Flags().GetString("area") + goalKey, _ := cmd.Flags().GetString("goal") + + if areaKey == "" && goalKey == "" { + return nil + } + + if areaKey == "" && goalKey != "" { + fmt.Fprintln(cmd.ErrOrStderr(), ui.Warning.Render("Goal specified without area; ignoring")) + + return nil + } + + cfg, err := config.Load() + if err != nil { + if errors.Is(err, config.ErrNotFound) { + fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Config not found; run 'lune init' to configure areas")) + } + + return err + } + + area := cfg.AreaByKey(areaKey) + if area == nil { + fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Unknown area: "+areaKey)) + + return fmt.Errorf("%w: %s", ErrUnknownArea, areaKey) + } + + builder.InArea(area.ID) + + if goalKey == "" { + return nil + } + + goal := area.GoalByKey(goalKey) + if goal == nil { + fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Unknown goal: "+goalKey)) + + return fmt.Errorf("%w: %s", ErrUnknownGoal, goalKey) + } + + builder.InGoal(goal.ID) + + return nil +} + +func applyOptionalFlags(cmd *cobra.Command, builder *lunatask.TaskBuilder) error { + if status, _ := cmd.Flags().GetString("status"); status != "" { + builder.WithStatus(lunatask.TaskStatus(status)) + } + + if note, _ := cmd.Flags().GetString("note"); note != "" { + resolved, err := resolveNote(note) + if err != nil { + return err + } + + builder.WithNote(resolved) + } + + if priority, _ := cmd.Flags().GetInt("priority"); priority != 0 { + builder.WithPriority(priority) + } + + if estimate, _ := cmd.Flags().GetInt("estimate"); estimate != 0 { + builder.WithEstimate(estimate) + } + + if motivation, _ := cmd.Flags().GetString("motivation"); motivation != "" { + builder.WithMotivation(lunatask.Motivation(motivation)) + } + + applyEisenhower(cmd, builder) + + return applySchedule(cmd, builder) +} + +func applyEisenhower(cmd *cobra.Command, builder *lunatask.TaskBuilder) { + if important, _ := cmd.Flags().GetBool("important"); important { + builder.Important() + } else if notImportant, _ := cmd.Flags().GetBool("not-important"); notImportant { + builder.NotImportant() + } + + if urgent, _ := cmd.Flags().GetBool("urgent"); urgent { + builder.Urgent() + } else if notUrgent, _ := cmd.Flags().GetBool("not-urgent"); notUrgent { + builder.NotUrgent() + } +} + +func applySchedule(cmd *cobra.Command, builder *lunatask.TaskBuilder) error { + schedule, _ := cmd.Flags().GetString("schedule") + if schedule == "" { + return nil + } + + date, err := lunatask.ParseDate(schedule) + if err != nil { + fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Invalid date format: "+schedule)) + + return fmt.Errorf("parsing schedule date: %w", err) + } + + builder.ScheduledOn(date) + + return nil +} + +func resolveNote(note string) (string, error) { + if note != "-" { + return note, nil + } + + data, err := os.ReadFile("/dev/stdin") + if err != nil { + return "", fmt.Errorf("reading stdin: %w", err) + } + + return strings.TrimSpace(string(data)), nil }