@@ -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
}