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