1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package init
6
7import (
8 "errors"
9 "fmt"
10
11 "github.com/charmbracelet/huh"
12
13 "git.secluded.site/lune/internal/config"
14)
15
16func maybeConfigureAreas(cfg *config.Config) error {
17 var configure bool
18
19 err := huh.NewConfirm().
20 Title("Configure areas & goals?").
21 Description("Areas organize your life (Work, Health, etc). Goals belong to areas.").
22 Affirmative("Yes").
23 Negative("Not now").
24 Value(&configure).
25 Run()
26 if err != nil {
27 if errors.Is(err, huh.ErrUserAborted) {
28 return nil
29 }
30
31 return err
32 }
33
34 if configure {
35 return manageAreas(cfg)
36 }
37
38 return nil
39}
40
41func manageAreas(cfg *config.Config) error {
42 for {
43 options := buildAreaOptions(cfg.Areas)
44
45 choice, err := runListSelect("Areas", "Manage your areas and their goals.", options)
46 if err != nil {
47 return err
48 }
49
50 if choice == choiceBack {
51 return nil
52 }
53
54 if choice == choiceAdd {
55 if err := addArea(cfg); err != nil {
56 return err
57 }
58
59 continue
60 }
61
62 idx, ok := parseEditIndex(choice)
63 if !ok {
64 continue
65 }
66
67 if err := manageAreaActions(cfg, idx); err != nil {
68 return err
69 }
70 }
71}
72
73func buildAreaOptions(areas []config.Area) []huh.Option[string] {
74 options := []huh.Option[string]{
75 huh.NewOption("Add new area", choiceAdd),
76 }
77
78 for idx, area := range areas {
79 goalCount := len(area.Goals)
80 label := fmt.Sprintf("%s (%s) - %d goal(s)", area.Name, area.Key, goalCount)
81 options = append(options, huh.NewOption(label, fmt.Sprintf("edit:%d", idx)))
82 }
83
84 options = append(options, huh.NewOption("Back", choiceBack))
85
86 return options
87}
88
89func addArea(cfg *config.Config) error {
90 area, err := editArea(nil, cfg)
91 if err != nil {
92 if errors.Is(err, errUserAborted) {
93 return nil
94 }
95
96 return err
97 }
98
99 cfg.Areas = append(cfg.Areas, *area)
100
101 return maybeManageGoals(cfg, len(cfg.Areas)-1)
102}
103
104func manageAreaActions(cfg *config.Config, idx int) error {
105 if idx < 0 || idx >= len(cfg.Areas) {
106 return fmt.Errorf("%w: area %d", errIndexOutRange, idx)
107 }
108
109 area := &cfg.Areas[idx]
110
111 action, err := runActionSelect(fmt.Sprintf("Area: %s (%s)", area.Name, area.Key), true)
112 if err != nil {
113 return err
114 }
115
116 return handleAreaAction(cfg, idx, area, action)
117}
118
119func handleAreaAction(cfg *config.Config, idx int, area *config.Area, action itemAction) error {
120 switch action {
121 case itemActionEdit:
122 if updated, err := editArea(area, cfg); err != nil && !errors.Is(err, errUserAborted) {
123 return err
124 } else if updated != nil {
125 cfg.Areas[idx] = *updated
126 }
127 case itemActionGoals:
128 return manageGoals(cfg, idx)
129 case itemActionDelete:
130 return deleteArea(cfg, idx)
131 case itemActionNone:
132 // User cancelled or went back
133 }
134
135 return nil
136}
137
138func editArea(existing *config.Area, cfg *config.Config) (*config.Area, error) {
139 area := config.Area{}
140 if existing != nil {
141 area = *existing
142 }
143
144 err := runItemForm(&area.Name, &area.Key, &area.ID, itemFormConfig{
145 itemType: "area",
146 namePlaceholder: "Personal",
147 keyPlaceholder: "personal",
148 keyValidator: validateAreaKey(cfg, existing),
149 })
150 if err != nil {
151 return nil, err
152 }
153
154 return &area, nil
155}
156
157func validateAreaKey(cfg *config.Config, existing *config.Area) func(string) error {
158 return func(input string) error {
159 if err := validateKeyFormat(input); err != nil {
160 return err
161 }
162
163 for idx := range cfg.Areas {
164 if existing != nil && &cfg.Areas[idx] == existing {
165 continue
166 }
167
168 if cfg.Areas[idx].Key == input {
169 return errKeyDuplicate
170 }
171 }
172
173 return nil
174 }
175}
176
177func maybeManageGoals(cfg *config.Config, areaIdx int) error {
178 var manage bool
179
180 err := huh.NewConfirm().
181 Title("Add goals for this area?").
182 Affirmative("Yes").
183 Negative("Not now").
184 Value(&manage).
185 Run()
186 if err != nil {
187 if errors.Is(err, huh.ErrUserAborted) {
188 return nil
189 }
190
191 return err
192 }
193
194 if manage {
195 return manageGoals(cfg, areaIdx)
196 }
197
198 return nil
199}
200
201func manageGoals(cfg *config.Config, areaIdx int) error {
202 if areaIdx < 0 || areaIdx >= len(cfg.Areas) {
203 return fmt.Errorf("%w: area %d", errIndexOutRange, areaIdx)
204 }
205
206 area := &cfg.Areas[areaIdx]
207
208 for {
209 options := buildGoalOptions(area.Goals)
210
211 choice, err := runListSelect(
212 fmt.Sprintf("Goals for: %s (%s)", area.Name, area.Key),
213 "",
214 options,
215 )
216 if err != nil {
217 return err
218 }
219
220 if choice == choiceDone {
221 return nil
222 }
223
224 if choice == choiceAdd {
225 if err := addGoal(area); err != nil {
226 return err
227 }
228
229 continue
230 }
231
232 idx, ok := parseEditIndex(choice)
233 if !ok {
234 continue
235 }
236
237 if err := manageGoalActions(area, idx); err != nil {
238 return err
239 }
240 }
241}
242
243func buildGoalOptions(goals []config.Goal) []huh.Option[string] {
244 options := []huh.Option[string]{
245 huh.NewOption("Add new goal", choiceAdd),
246 }
247
248 for idx, goal := range goals {
249 label := fmt.Sprintf("%s (%s)", goal.Name, goal.Key)
250 options = append(options, huh.NewOption(label, fmt.Sprintf("edit:%d", idx)))
251 }
252
253 options = append(options, huh.NewOption("Done", choiceDone))
254
255 return options
256}
257
258func addGoal(area *config.Area) error {
259 goal, err := editGoal(nil, area)
260 if err != nil {
261 if errors.Is(err, errUserAborted) {
262 return nil
263 }
264
265 return err
266 }
267
268 area.Goals = append(area.Goals, *goal)
269
270 return nil
271}
272
273func manageGoalActions(area *config.Area, idx int) error {
274 if idx < 0 || idx >= len(area.Goals) {
275 return fmt.Errorf("%w: goal %d", errIndexOutRange, idx)
276 }
277
278 goal := &area.Goals[idx]
279
280 action, err := runActionSelect(fmt.Sprintf("Goal: %s (%s)", goal.Name, goal.Key), false)
281 if err != nil {
282 return err
283 }
284
285 switch action {
286 case itemActionEdit:
287 updated, err := editGoal(goal, area)
288 if err != nil && !errors.Is(err, errUserAborted) {
289 return err
290 }
291
292 if updated != nil {
293 area.Goals[idx] = *updated
294 }
295 case itemActionDelete:
296 return deleteGoal(area, idx)
297 case itemActionNone, itemActionGoals:
298 // User cancelled or went back; goals not applicable here
299 }
300
301 return nil
302}
303
304func editGoal(existing *config.Goal, area *config.Area) (*config.Goal, error) {
305 goal := config.Goal{}
306 if existing != nil {
307 goal = *existing
308 }
309
310 err := runItemForm(&goal.Name, &goal.Key, &goal.ID, itemFormConfig{
311 itemType: "goal",
312 namePlaceholder: "Learn Gaelic",
313 keyPlaceholder: "gaelic",
314 keyValidator: validateGoalKey(area, existing),
315 })
316 if err != nil {
317 return nil, err
318 }
319
320 return &goal, nil
321}
322
323func validateGoalKey(area *config.Area, existing *config.Goal) func(string) error {
324 return func(input string) error {
325 if err := validateKeyFormat(input); err != nil {
326 return err
327 }
328
329 for idx := range area.Goals {
330 if existing != nil && &area.Goals[idx] == existing {
331 continue
332 }
333
334 if area.Goals[idx].Key == input {
335 return errKeyDuplicate
336 }
337 }
338
339 return nil
340 }
341}
342
343func deleteArea(cfg *config.Config, idx int) error {
344 if idx < 0 || idx >= len(cfg.Areas) {
345 return fmt.Errorf("%w: area %d", errIndexOutRange, idx)
346 }
347
348 area := cfg.Areas[idx]
349
350 var confirm bool
351
352 err := huh.NewConfirm().
353 Title(fmt.Sprintf("Delete area '%s'?", area.Name)).
354 Description(fmt.Sprintf("This will also remove %d goal(s). This cannot be undone.", len(area.Goals))).
355 Affirmative("Delete").
356 Negative("Cancel").
357 Value(&confirm).
358 Run()
359 if err != nil {
360 if errors.Is(err, huh.ErrUserAborted) {
361 return nil
362 }
363
364 return err
365 }
366
367 if confirm {
368 cfg.Areas = append(cfg.Areas[:idx], cfg.Areas[idx+1:]...)
369 if cfg.Defaults.Area == area.Key {
370 cfg.Defaults.Area = ""
371 }
372 }
373
374 return nil
375}
376
377func deleteGoal(area *config.Area, idx int) error {
378 if idx < 0 || idx >= len(area.Goals) {
379 return fmt.Errorf("%w: goal %d", errIndexOutRange, idx)
380 }
381
382 goal := area.Goals[idx]
383
384 var confirm bool
385
386 err := huh.NewConfirm().
387 Title(fmt.Sprintf("Delete goal '%s'?", goal.Name)).
388 Description("This cannot be undone.").
389 Affirmative("Delete").
390 Negative("Cancel").
391 Value(&confirm).
392 Run()
393 if err != nil {
394 if errors.Is(err, huh.ErrUserAborted) {
395 return nil
396 }
397
398 return err
399 }
400
401 if confirm {
402 area.Goals = append(area.Goals[:idx], area.Goals[idx+1:]...)
403 }
404
405 return nil
406}