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		fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Failed to create task"))
 86
 87		return err
 88	}
 89
 90	fmt.Fprintln(cmd.OutOrStdout(), ui.Success.Render("Created task: "+task.ID))
 91
 92	return nil
 93}
 94
 95func resolveName(arg string) (string, error) {
 96	if arg != "-" {
 97		return arg, nil
 98	}
 99
100	scanner := bufio.NewScanner(os.Stdin)
101	if scanner.Scan() {
102		return strings.TrimSpace(scanner.Text()), nil
103	}
104
105	if err := scanner.Err(); err != nil {
106		return "", fmt.Errorf("reading stdin: %w", err)
107	}
108
109	return "", ErrNoInput
110}
111
112func applyAreaAndGoal(cmd *cobra.Command, builder *lunatask.TaskBuilder) error {
113	areaKey, _ := cmd.Flags().GetString("area")
114	goalKey, _ := cmd.Flags().GetString("goal")
115
116	if areaKey == "" && goalKey == "" {
117		return nil
118	}
119
120	if areaKey == "" && goalKey != "" {
121		fmt.Fprintln(cmd.ErrOrStderr(), ui.Warning.Render("Goal specified without area; ignoring"))
122
123		return nil
124	}
125
126	cfg, err := config.Load()
127	if err != nil {
128		if errors.Is(err, config.ErrNotFound) {
129			fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Config not found; run 'lune init' to configure areas"))
130		}
131
132		return err
133	}
134
135	area := cfg.AreaByKey(areaKey)
136	if area == nil {
137		fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Unknown area: "+areaKey))
138
139		return fmt.Errorf("%w: %s", ErrUnknownArea, areaKey)
140	}
141
142	builder.InArea(area.ID)
143
144	if goalKey == "" {
145		return nil
146	}
147
148	goal := area.GoalByKey(goalKey)
149	if goal == nil {
150		fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Unknown goal: "+goalKey))
151
152		return fmt.Errorf("%w: %s", ErrUnknownGoal, goalKey)
153	}
154
155	builder.InGoal(goal.ID)
156
157	return nil
158}
159
160func applyOptionalFlags(cmd *cobra.Command, builder *lunatask.TaskBuilder) error {
161	if status, _ := cmd.Flags().GetString("status"); status != "" {
162		builder.WithStatus(lunatask.TaskStatus(status))
163	}
164
165	if note, _ := cmd.Flags().GetString("note"); note != "" {
166		resolved, err := resolveNote(note)
167		if err != nil {
168			return err
169		}
170
171		builder.WithNote(resolved)
172	}
173
174	if priority, _ := cmd.Flags().GetInt("priority"); priority != 0 {
175		builder.WithPriority(priority)
176	}
177
178	if estimate, _ := cmd.Flags().GetInt("estimate"); estimate != 0 {
179		builder.WithEstimate(estimate)
180	}
181
182	if motivation, _ := cmd.Flags().GetString("motivation"); motivation != "" {
183		builder.WithMotivation(lunatask.Motivation(motivation))
184	}
185
186	applyEisenhower(cmd, builder)
187
188	return applySchedule(cmd, builder)
189}
190
191func applyEisenhower(cmd *cobra.Command, builder *lunatask.TaskBuilder) {
192	if important, _ := cmd.Flags().GetBool("important"); important {
193		builder.Important()
194	} else if notImportant, _ := cmd.Flags().GetBool("not-important"); notImportant {
195		builder.NotImportant()
196	}
197
198	if urgent, _ := cmd.Flags().GetBool("urgent"); urgent {
199		builder.Urgent()
200	} else if notUrgent, _ := cmd.Flags().GetBool("not-urgent"); notUrgent {
201		builder.NotUrgent()
202	}
203}
204
205func applySchedule(cmd *cobra.Command, builder *lunatask.TaskBuilder) error {
206	schedule, _ := cmd.Flags().GetString("schedule")
207	if schedule == "" {
208		return nil
209	}
210
211	date, err := lunatask.ParseDate(schedule)
212	if err != nil {
213		fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Invalid date format: "+schedule))
214
215		return fmt.Errorf("parsing schedule date: %w", err)
216	}
217
218	builder.ScheduledOn(date)
219
220	return nil
221}
222
223func resolveNote(note string) (string, error) {
224	if note != "-" {
225		return note, nil
226	}
227
228	data, err := os.ReadFile("/dev/stdin")
229	if err != nil {
230		return "", fmt.Errorf("reading stdin: %w", err)
231	}
232
233	return strings.TrimSpace(string(data)), nil
234}