add.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5package task
  6
  7import (
  8	"bufio"
  9	"errors"
 10	"fmt"
 11	"os"
 12	"strings"
 13
 14	"git.secluded.site/go-lunatask"
 15	"git.secluded.site/lune/internal/client"
 16	"git.secluded.site/lune/internal/completion"
 17	"git.secluded.site/lune/internal/config"
 18	"git.secluded.site/lune/internal/ui"
 19	"github.com/spf13/cobra"
 20)
 21
 22// ErrUnknownGoal indicates the specified goal key was not found in config.
 23var ErrUnknownGoal = errors.New("unknown goal key")
 24
 25// ErrNoInput indicates no input was provided on stdin.
 26var ErrNoInput = errors.New("no input provided on stdin")
 27
 28// AddCmd creates a new task. Exported for use by the add shortcut.
 29var AddCmd = &cobra.Command{
 30	Use:   "add NAME",
 31	Short: "Create a new task",
 32	Long: `Create a new task in Lunatask.
 33
 34The task name is required. Use flags to set additional properties.
 35Use "-" as NAME to read the task name from stdin.`,
 36	Args: cobra.MinimumNArgs(1),
 37	RunE: runAdd,
 38}
 39
 40func init() {
 41	AddCmd.Flags().StringP("area", "a", "", "Area key (from config)")
 42	AddCmd.Flags().StringP("goal", "g", "", "Goal key (from config)")
 43	AddCmd.Flags().StringP("status", "s", "", "Status: later, next, started, waiting")
 44	AddCmd.Flags().StringP("note", "n", "", "Task note (use - for stdin)")
 45	AddCmd.Flags().IntP("priority", "p", 0, "Priority: -2 to 2")
 46	AddCmd.Flags().IntP("estimate", "e", 0, "Estimate in minutes (0-720)")
 47	AddCmd.Flags().StringP("motivation", "m", "", "Motivation: must, should, want")
 48	AddCmd.Flags().Bool("important", false, "Mark as important (Eisenhower matrix)")
 49	AddCmd.Flags().Bool("not-important", false, "Mark as not important")
 50	AddCmd.Flags().Bool("urgent", false, "Mark as urgent (Eisenhower matrix)")
 51	AddCmd.Flags().Bool("not-urgent", false, "Mark as not urgent")
 52	AddCmd.Flags().String("schedule", "", "Schedule date (YYYY-MM-DD)")
 53
 54	_ = AddCmd.RegisterFlagCompletionFunc("area", completion.Areas)
 55	_ = AddCmd.RegisterFlagCompletionFunc("goal", completion.Goals)
 56	_ = AddCmd.RegisterFlagCompletionFunc("status",
 57		completion.Static("later", "next", "started", "waiting"))
 58	_ = AddCmd.RegisterFlagCompletionFunc("motivation",
 59		completion.Static("must", "should", "want"))
 60}
 61
 62func runAdd(cmd *cobra.Command, args []string) error {
 63	name, err := resolveName(args[0])
 64	if err != nil {
 65		return err
 66	}
 67
 68	apiClient, err := client.New()
 69	if err != nil {
 70		return err
 71	}
 72
 73	builder := apiClient.NewTask(name)
 74
 75	if err := applyAreaAndGoal(cmd, builder); err != nil {
 76		return err
 77	}
 78
 79	if err := applyOptionalFlags(cmd, builder); err != nil {
 80		return err
 81	}
 82
 83	task, err := builder.Create(cmd.Context())
 84	if err != nil {
 85		return err
 86	}
 87
 88	fmt.Fprintln(cmd.OutOrStdout(), ui.Success.Render("Created task: "+task.ID))
 89
 90	return nil
 91}
 92
 93func resolveName(arg string) (string, error) {
 94	if arg != "-" {
 95		return arg, nil
 96	}
 97
 98	scanner := bufio.NewScanner(os.Stdin)
 99	if scanner.Scan() {
100		return strings.TrimSpace(scanner.Text()), nil
101	}
102
103	if err := scanner.Err(); err != nil {
104		return "", fmt.Errorf("reading stdin: %w", err)
105	}
106
107	return "", ErrNoInput
108}
109
110func applyAreaAndGoal(cmd *cobra.Command, builder *lunatask.TaskBuilder) error {
111	areaKey, _ := cmd.Flags().GetString("area")
112	goalKey, _ := cmd.Flags().GetString("goal")
113
114	if areaKey == "" && goalKey == "" {
115		return nil
116	}
117
118	if areaKey == "" && goalKey != "" {
119		fmt.Fprintln(cmd.ErrOrStderr(), ui.Warning.Render("Goal specified without area; ignoring"))
120
121		return nil
122	}
123
124	cfg, err := config.Load()
125	if err != nil {
126		return err
127	}
128
129	area := cfg.AreaByKey(areaKey)
130	if area == nil {
131		return fmt.Errorf("%w: %s", ErrUnknownArea, areaKey)
132	}
133
134	builder.InArea(area.ID)
135
136	if goalKey == "" {
137		return nil
138	}
139
140	goal := area.GoalByKey(goalKey)
141	if goal == nil {
142		return fmt.Errorf("%w: %s", ErrUnknownGoal, goalKey)
143	}
144
145	builder.InGoal(goal.ID)
146
147	return nil
148}
149
150func applyOptionalFlags(cmd *cobra.Command, builder *lunatask.TaskBuilder) error {
151	if status, _ := cmd.Flags().GetString("status"); status != "" {
152		builder.WithStatus(lunatask.TaskStatus(status))
153	}
154
155	if note, _ := cmd.Flags().GetString("note"); note != "" {
156		resolved, err := resolveNote(note)
157		if err != nil {
158			return err
159		}
160
161		builder.WithNote(resolved)
162	}
163
164	if priority, _ := cmd.Flags().GetInt("priority"); priority != 0 {
165		builder.Priority(lunatask.Priority(priority))
166	}
167
168	if estimate, _ := cmd.Flags().GetInt("estimate"); estimate != 0 {
169		builder.WithEstimate(estimate)
170	}
171
172	if motivation, _ := cmd.Flags().GetString("motivation"); motivation != "" {
173		builder.WithMotivation(lunatask.Motivation(motivation))
174	}
175
176	applyEisenhower(cmd, builder)
177
178	return applySchedule(cmd, builder)
179}
180
181func applyEisenhower(cmd *cobra.Command, builder *lunatask.TaskBuilder) {
182	if important, _ := cmd.Flags().GetBool("important"); important {
183		builder.Important()
184	} else if notImportant, _ := cmd.Flags().GetBool("not-important"); notImportant {
185		builder.NotImportant()
186	}
187
188	if urgent, _ := cmd.Flags().GetBool("urgent"); urgent {
189		builder.Urgent()
190	} else if notUrgent, _ := cmd.Flags().GetBool("not-urgent"); notUrgent {
191		builder.NotUrgent()
192	}
193}
194
195func applySchedule(cmd *cobra.Command, builder *lunatask.TaskBuilder) error {
196	schedule, _ := cmd.Flags().GetString("schedule")
197	if schedule == "" {
198		return nil
199	}
200
201	date, err := lunatask.ParseDate(schedule)
202	if err != nil {
203		return fmt.Errorf("parsing schedule date: %w", err)
204	}
205
206	builder.ScheduledOn(date)
207
208	return nil
209}
210
211func resolveNote(note string) (string, error) {
212	if note != "-" {
213		return note, nil
214	}
215
216	data, err := os.ReadFile("/dev/stdin")
217	if err != nil {
218		return "", fmt.Errorf("reading stdin: %w", err)
219	}
220
221	return strings.TrimSpace(string(data)), nil
222}